diff --git a/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs b/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs index 136e769..6c505ec 100644 --- a/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs +++ b/rubberduckvba.Server/Api/Features/FeatureEditViewModel.cs @@ -1,4 +1,5 @@ using rubberduckvba.Server.Model; +using rubberduckvba.Server.Model.Entity; using rubberduckvba.Server.Services; namespace rubberduckvba.Server.Api.Features; @@ -36,7 +37,8 @@ public Feature ToFeature() ShortDescription = ShortDescription, Description = Description, IsHidden = IsHidden, - IsNew = IsNew + IsNew = IsNew, + Links = Links }; } @@ -56,6 +58,8 @@ public FeatureEditViewModel(Feature model, FeatureOptionViewModel[] features, Re Features = features; Repositories = repositories; + + Links = model.Links; } public int? Id { get; init; } @@ -72,4 +76,6 @@ public FeatureEditViewModel(Feature model, FeatureOptionViewModel[] features, Re public FeatureOptionViewModel[] Features { get; init; } = []; public RepositoryOptionViewModel[] Repositories { get; init; } = []; + + public BlogLink[] Links { get; init; } = []; } diff --git a/rubberduckvba.Server/Api/Features/FeaturesController.cs b/rubberduckvba.Server/Api/Features/FeaturesController.cs index 42352a0..6728f37 100644 --- a/rubberduckvba.Server/Api/Features/FeaturesController.cs +++ b/rubberduckvba.Server/Api/Features/FeaturesController.cs @@ -244,11 +244,13 @@ public async Task Delete([FromBody] Feature model) public MarkdownContentViewModel FormatMarkdown([FromBody] MarkdownContentViewModel model) { var markdown = model.Content; - var formatted = markdownService.FormatMarkdownDocument(markdown, withSyntaxHighlighting: true); - return new MarkdownContentViewModel + if (!cache.TryGetHtml(markdown, out var html)) { - Content = formatted - }; + html = markdownService.FormatMarkdownDocument(markdown, withSyntaxHighlighting: true); + cache.CacheHtml(markdown, html); + } + + return new MarkdownContentViewModel { Content = html! }; } private InspectionsFeatureViewModel GetInspections() diff --git a/rubberduckvba.Server/Services/CacheService.cs b/rubberduckvba.Server/Services/CacheService.cs index 1e97775..2c887a7 100644 --- a/rubberduckvba.Server/Services/CacheService.cs +++ b/rubberduckvba.Server/Services/CacheService.cs @@ -4,6 +4,8 @@ using rubberduckvba.Server.Api.Tags; using rubberduckvba.Server.Data; using rubberduckvba.Server.Model; +using System.Security.Cryptography; +using System.Text; namespace rubberduckvba.Server.Services; @@ -41,6 +43,9 @@ private void GetCurrentJobState() XmldocJobState = state.TryGetValue(XmldocJobName, out var xmldocsJobState) ? xmldocsJobState : null; } + public bool TryGetHtml(string markdown, out string? cached) => TryReadFromCache($"md:{Encoding.UTF8.GetString(SHA256.HashData(Encoding.UTF8.GetBytes(markdown)))}", out cached); + public void CacheHtml(string markdown, string html) => Write($"md:{Encoding.UTF8.GetString(SHA256.HashData(Encoding.UTF8.GetBytes(markdown)))}", html); + public bool TryGetLatestTags(out LatestTagsViewModel? cached) => TryReadFromTagsCache("tags/latest", out cached); public bool TryGetAvailableDownloads(out AvailableDownload[]? cached) => TryReadFromTagsCache("downloads", out cached); public bool TryGetFeatures(out FeatureViewModel[]? cached) => TryReadXmldocCache("features", out cached); @@ -177,4 +182,10 @@ private bool TryReadXmldocCache(string key, out T? cached) return result; } + + private bool TryReadFromCache(string key, out string? cached) + { + var result = _cache.TryGetValue(key, out cached); + return result; + } } \ No newline at end of file diff --git a/rubberduckvba.Server/Services/GitHubClientService.cs b/rubberduckvba.Server/Services/GitHubClientService.cs index c7c1d27..e6ed0c5 100644 --- a/rubberduckvba.Server/Services/GitHubClientService.cs +++ b/rubberduckvba.Server/Services/GitHubClientService.cs @@ -42,51 +42,52 @@ private class ReleaseComparer : IEqualityComparer var credentials = new Credentials(token); var client = new GitHubClient(new ProductHeaderValue(config.UserAgent), new InMemoryCredentialStore(credentials)); - var user = await client.User.Current(); - var orgs = await client.Organization.GetAllForCurrent(); - - var org = orgs.SingleOrDefault(e => e.Id == RDConstants.Org.OrganisationId); - var isOrgMember = org is Organization rdOrg; - + var (name, role) = await DetermineUserRole(client); var claims = new List { - new(ClaimTypes.Name, user.Login), + new(ClaimTypes.Name, name), + new(ClaimTypes.Role, role), new(ClaimTypes.Authentication, token), new("access_token", token) }; - if (isOrgMember && !user.Suspended) + return new ClaimsPrincipal(new ClaimsIdentity(claims, "github")); + } + + private static async Task<(string name, string role)> DetermineUserRole(GitHubClient client) + { + var role = RDConstants.Roles.ReaderRole; + + var user = await client.User.Current(); + if (!user.Suspended) { - var teams = await client.Organization.Team.GetAllForCurrent(); + // only authenticated GitHub users in good standing can submit edits + role = RDConstants.Roles.WriterRole; - var adminTeam = teams.SingleOrDefault(e => e.Name == RDConstants.Org.WebAdminTeam); - if (adminTeam is not null) + var orgs = await client.Organization.GetAllForCurrent(); + if (orgs.SingleOrDefault(e => e.Id == RDConstants.Org.OrganisationId) is not null) { - // authenticated members of the org who are in the admin team can manage the site and approve their own changes - claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.AdminRole)); - } - else - { - var contributorsTeam = teams.SingleOrDefault(e => e.Name == RDConstants.Org.ContributorsTeam); - if (contributorsTeam is not null) - { - // members of the contributors team can review/approve/reject suggested changes - claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.ReviewerRole)); - } - else + var teams = await client.Organization.Team.GetAllForCurrent(); + + // members of the Rubberduck organization are welcome to review/approve/reject suggested changes + // NOTE: opportunity for eventual distinction between members and contributors + role = RDConstants.Roles.ReviewerRole; + + //// members of the contributors team can review/approve/reject suggested changes + //if (teams.SingleOrDefault(e => e.Name == RDConstants.Org.ContributorsTeam) is not null) + //{ + // role = RDConstants.Roles.ReviewerRole; + //} + + // authenticated org members in the WebAdmin team can manage the site and approve their own changes + if (teams.SingleOrDefault(e => e.Name == RDConstants.Org.WebAdminTeam) is not null) { - // authenticated members of the org can submit edits - claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.WriterRole)); + role = RDConstants.Roles.AdminRole; } } } - else - { - claims.Add(new Claim(ClaimTypes.Role, RDConstants.Roles.ReaderRole)); - } - var identity = new ClaimsIdentity(claims, "github"); - return new ClaimsPrincipal(identity); + return (user.Login, role); } public async Task> GetAllTagsAsync() diff --git a/rubberduckvba.client/src/app/app.module.ts b/rubberduckvba.client/src/app/app.module.ts index 8b7c8f9..7645773 100644 --- a/rubberduckvba.client/src/app/app.module.ts +++ b/rubberduckvba.client/src/app/app.module.ts @@ -48,6 +48,7 @@ import { AuditFeatureDeleteComponent } from './components/audits/feature-delete. import { UserProfileComponent } from './routes/profile/user-profile.component'; import { AuditItemComponent } from './routes/audits/audit-item/audit-item.component'; import { AuthService } from './services/auth.service'; +import { EditBlogLinkComponent } from './components/edit-feature/edit-bloglink/edit-bloglink.component'; /** * https://stackoverflow.com/a/39560520 @@ -96,7 +97,8 @@ export class LowerCaseUrlSerializer extends DefaultUrlSerializer { AnnotationComponent, QuickFixComponent, AboutComponent, - EditFeatureComponent + EditFeatureComponent, + EditBlogLinkComponent ], bootstrap: [AppComponent], imports: [ diff --git a/rubberduckvba.client/src/app/components/edit-feature/edit-bloglink/edit-bloglink.component.html b/rubberduckvba.client/src/app/components/edit-feature/edit-bloglink/edit-bloglink.component.html new file mode 100644 index 0000000..828198d --- /dev/null +++ b/rubberduckvba.client/src/app/components/edit-feature/edit-bloglink/edit-bloglink.component.html @@ -0,0 +1,23 @@ +
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
diff --git a/rubberduckvba.client/src/app/components/edit-feature/edit-bloglink/edit-bloglink.component.ts b/rubberduckvba.client/src/app/components/edit-feature/edit-bloglink/edit-bloglink.component.ts new file mode 100644 index 0000000..963f4c2 --- /dev/null +++ b/rubberduckvba.client/src/app/components/edit-feature/edit-bloglink/edit-bloglink.component.ts @@ -0,0 +1,34 @@ +import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { BlogLink } from "../../../model/feature.model"; + +@Component({ + selector: 'edit-blog-link', + templateUrl: './edit-bloglink.component.html' +}) +export class EditBlogLinkComponent implements OnInit { + + private _blogLink: BlogLink = null!; + + constructor() { + + } + + ngOnInit(): void { + } + + @Output() + public removeLink = new EventEmitter(); + + @Input() + public set blogLink(value: BlogLink) { + this._blogLink = value; + } + + public get blogLink(): BlogLink { + return this._blogLink; + } + + public onRemove(): void { + this.removeLink.emit(this.blogLink); + } +} diff --git a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html index 15a99a0..09297cf 100644 --- a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html +++ b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.html @@ -10,6 +10,12 @@ + + + + +
{{feature.description.length}} diff --git a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts index aa4676d..9caa357 100644 --- a/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts +++ b/rubberduckvba.client/src/app/components/edit-feature/edit-feature.component.ts @@ -1,6 +1,6 @@ import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, TemplateRef, ViewChild, inject, input } from "@angular/core"; import { BehaviorSubject } from "rxjs"; -import { EditSubFeatureViewModelClass, MarkdownContent, SubFeatureViewModel, SubFeatureViewModelClass, UserViewModel } from "../../model/feature.model"; +import { BlogLink, BlogLinkViewModelClass, EditSubFeatureViewModelClass, MarkdownContent, SubFeatureViewModel, SubFeatureViewModelClass, UserViewModel } from "../../model/feature.model"; import { FaIconLibrary } from "@fortawesome/angular-fontawesome"; import { fas } from "@fortawesome/free-solid-svg-icons"; import { NgbModal } from "@ng-bootstrap/ng-bootstrap"; @@ -9,6 +9,7 @@ import { ApiClientService } from "../../services/api-client.service"; export enum AdminAction { Edit = 'edit', EditSummary = 'summary', + EditLinks = 'links', Create = 'create', Delete = 'delete', } @@ -98,6 +99,7 @@ export class EditFeatureComponent implements OnInit { featureTitle: parentTitle, isCollapsed: false, isDetailsCollapsed: true, + links: [] }); } @@ -105,7 +107,7 @@ export class EditFeatureComponent implements OnInit { } public onConfirmChanges(): void { - this.modal.dismissAll(); + this.modal.dismissAll(); this.api.saveFeature(this.feature).subscribe(saved => { window.location.reload(); }); @@ -138,4 +140,17 @@ export class EditFeatureComponent implements OnInit { window.location.reload(); }); } + + public onRemoveLink(link: BlogLink): void { + this.feature.links = this.feature.links!.filter(e => e.name.length > 0 && e.url.length > 0 && (e.name != link.name || e.url != link.url)); + } + + public onAddLink(): void { + this.feature.links.push(new BlogLinkViewModelClass({ + name: 'Title', + author: 'Author', + published: new Date().toISOString().substring(0, 10), + url: 'https://rubberduckvba.blog/...' + })); + } } diff --git a/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html b/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html index 9bada5f..40eacdc 100644 --- a/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html +++ b/rubberduckvba.client/src/app/components/feature-box/feature-box.component.html @@ -14,7 +14,8 @@

{{feature.title}}

-
+
+
-
-
+
+
+
+
+