diff --git a/Wave/Components/ArticleComponent.razor b/Wave/Components/ArticleComponent.razor new file mode 100644 index 0000000..6ea103f --- /dev/null +++ b/Wave/Components/ArticleComponent.razor @@ -0,0 +1,72 @@ +@using Wave.Data +@using Humanizer + +@inject IStringLocalizer Localizer + +

+ @Article.Title +

+

+ + + @if (Article.LastModified is not null) { + + } + @if (Article.Status <= ArticleStatus.Published) { + + @Article.Status.Humanize() + + } + +

+ + + Edit + + + +
+ +
+ @Content +
+ +
+ +@if (!string.IsNullOrWhiteSpace(Article.Author.AboutTheAuthor)) { +
+

About The Author

+
+
+ +
+
+

@Article.Author.Name

+

+ @Article.Author.AboutTheAuthor +

+
+
+
+} + +
+ @if (string.IsNullOrWhiteSpace(Article.Author.AboutTheAuthor)) { + + } + @if (Article.Reviewer is not null && Article.Reviewer.Id != Article.Author.Id) { + + } +
+ +@code { + [Parameter] + public required Article Article { get; set; } + private MarkupString Content => new(Article.BodyHtml); +} diff --git a/Wave/Components/Pages/ArticleView.razor b/Wave/Components/Pages/ArticleView.razor index bf648df..02e87f5 100644 --- a/Wave/Components/Pages/ArticleView.razor +++ b/Wave/Components/Pages/ArticleView.razor @@ -1,69 +1,35 @@ @page "/article/{id:guid}" @using Microsoft.EntityFrameworkCore @using Wave.Data -@using Humanizer -@using System.Globalization +@using System.Security.Claims +@using Microsoft.AspNetCore.Identity +@using System.Diagnostics.CodeAnalysis @inject IDbContextFactory ContextFactory; +@inject RoleManager RoleManager @inject IStringLocalizer Localizer @(TitlePrefix + Article.Title) -

@Article.Title

-

- - - @if (Article.LastModified is not null) { - + + + + + + + + + + + + +

@Localizer["NotFound_Title"]

+

@Localizer["NotFound_Description"]

+ @if (Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLower() == "development") { +

@context.Message

} -
-

- - - Edit - - - -
- -
- @Content -
- -
- -@if (!string.IsNullOrWhiteSpace(Article.Author.AboutTheAuthor)) { -
-

About The Author

-
-
- -
-
-

@Article.Author.Name

-

- @Article.Author.AboutTheAuthor -

-
-
-
-} - -
- @if (string.IsNullOrWhiteSpace(Article.Author.AboutTheAuthor)) { - - } - @if (Article.Reviewer is not null && Article.Reviewer.Id != Article.Author.Id) { - - } -
- + + @code { [CascadingParameter(Name = "TitlePrefix")] @@ -71,9 +37,39 @@ [Parameter] public Guid Id { get; set; } + private Article Article { get; set; } = default!; - private Article Article { get; set; } = null!; - private MarkupString Content => new(Article.BodyHtml); + private Article GetArticlePublic() { + if (Article.Status >= ArticleStatus.Published && Article.PublishDate <= DateTimeOffset.UtcNow) { + return Article; + } + throw new ApplicationException("Article is not public."); + } + + [SuppressMessage("ReSharper", "ConvertIfStatementToSwitchStatement")] + private Article GetArticleProtected(ClaimsPrincipal principal) { + // Admins always get access + if (principal.IsInRole("Admin")) { + return Article; + } + + // You cannot access your own drafts + if (Article.Status is ArticleStatus.Draft) { + if (Article.Author.Id == principal.FindFirst("Id")!.Value) { + return Article; + } + throw new ApplicationException("Cannot access draft article without being author or admin."); + } + // InReview Articles can only be accessed by reviewers + if (Article.Status is ArticleStatus.InReview) { + if (principal.IsInRole("Reviewer")) { + return Article; + } + throw new ApplicationException("Cannot access in-review article without being a reviewer or admin."); + } + + throw new ApplicationException("User does not have access to this article."); + } protected override async Task OnInitializedAsync() { // We need blocking calls here, bc otherwise Blazor will execute Render in parallel, @@ -86,7 +82,7 @@ Article = context.Set
() .Include(a => a.Author) .Include(a => a.Reviewer) - .Where(a => a.Status >= ArticleStatus.Published && a.PublishDate <= now) + // .Where(a => a.Status >= ArticleStatus.Published && a.PublishDate <= now) .First(a => a.Id == Id); if (Article is null) throw new ApplicationException("Article not found."); diff --git a/Wave/Program.cs b/Wave/Program.cs index cdacae3..4b7e5e3 100644 --- a/Wave/Program.cs +++ b/Wave/Program.cs @@ -36,7 +36,9 @@ .AddPolicy("ArticleEditPermissions", p => p.RequireRole("Author", "Admin")) .AddPolicy("ArticleReviewPermissions", p => p.RequireRole("Reviewer", "Admin")) .AddPolicy("ArticleDeletePermissions", p => p.RequireRole("Moderator", "Admin")) - .AddPolicy("RoleAssignPermissions", p => p.RequireRole("Admin")); + .AddPolicy("RoleAssignPermissions", p => p.RequireRole("Admin")) + + .AddPolicy("ArticleEditOrReviewPermissions", p => p.RequireRole("Author", "Reviewer", "Admin")); builder.Services.AddAuthentication(options => { options.DefaultScheme = IdentityConstants.ApplicationScheme; options.DefaultSignInScheme = IdentityConstants.ExternalScheme; diff --git a/Wave/Utilities/ClaimsPrincipalUtilities.cs b/Wave/Utilities/ClaimsPrincipalUtilities.cs new file mode 100644 index 0000000..339a8a7 --- /dev/null +++ b/Wave/Utilities/ClaimsPrincipalUtilities.cs @@ -0,0 +1,11 @@ +using System.Security.Claims; + +namespace Wave.Utilities; + +public static class ClaimsPrincipalUtilities { + public static bool IsInRole(this ClaimsPrincipal principal, string roleName) { + return principal.Claims.Any( + c => c.Type == ClaimTypes.Role && + string.Equals(c.Value, roleName,StringComparison.CurrentCultureIgnoreCase)); + } +} \ No newline at end of file