198 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
		
			8.4 KiB
		
	
	
	
		
			Plaintext
		
	
	
	
	
	
@page "/article/new"
 | 
						|
@page "/article/{id:guid}/edit"
 | 
						|
@using Wave.Data
 | 
						|
@using Microsoft.EntityFrameworkCore
 | 
						|
@using System.ComponentModel.DataAnnotations
 | 
						|
@using Microsoft.AspNetCore.Identity
 | 
						|
@using Wave.Utilities
 | 
						|
 | 
						|
@rendermode InteractiveServer
 | 
						|
@attribute [Authorize(Policy = "ArticleEditOrReviewPermissions")]
 | 
						|
@inject IDbContextFactory<ApplicationDbContext> ContextFactory
 | 
						|
@inject NavigationManager Navigation
 | 
						|
@inject UserManager<ApplicationUser> UserManager
 | 
						|
@inject IStringLocalizer<ArticleEditor> Localizer
 | 
						|
 | 
						|
@if (Article is not null) {
 | 
						|
    <PageTitle>@(TitlePrefix + Localizer["PageTitle_Edit"]) | @Article.Title</PageTitle>
 | 
						|
} else {
 | 
						|
    <PageTitle>@(TitlePrefix + Localizer["PageTitle_New"])</PageTitle>
 | 
						|
}
 | 
						|
@if (Busy) {
 | 
						|
    <div class="flex place-content-center">
 | 
						|
        <span class="loading loading-spinner loading-lg"></span>
 | 
						|
    </div>
 | 
						|
} else {
 | 
						|
    <ErrorBoundary>
 | 
						|
        <ChildContent>
 | 
						|
            <h1 class="text-3xl lg:text-5xl font-light mb-6">@Localizer["EditorTitle"]</h1>
 | 
						|
 | 
						|
            <div class="w-full">
 | 
						|
                <ul class="steps w-full max-w-xs">
 | 
						|
                    <li class="step @(Article?.Status >= ArticleStatus.Draft ? "step-primary": "")">@Localizer["Draft"]</li>
 | 
						|
                    <li class="step @(Article?.Status >= ArticleStatus.InReview ? "step-primary": "")">@Localizer["InReview"]</li>
 | 
						|
                    <li class="step @(Article?.Status >= ArticleStatus.Published ? "step-primary": "")">@Localizer["Published"]</li>
 | 
						|
                </ul>
 | 
						|
            </div>
 | 
						|
 | 
						|
            <EditForm method="post" FormName="article-editor" Model="@Model" OnValidSubmit="OnValidSubmit">
 | 
						|
                <DataAnnotationsValidator />
 | 
						|
                <input type="hidden" @bind-value="@Model.Id" />
 | 
						|
 | 
						|
                <InputLabelComponent LabelText="@Localizer["Title_Label"]" For="() => Model.Title">
 | 
						|
                    <InputText class="input input-bordered w-full" maxlength="256" required aria-required
 | 
						|
                               @bind-Value="@Model.Title" placeholder="@Localizer["Title_Placeholder"]" autocomplete="off" />
 | 
						|
                </InputLabelComponent>
 | 
						|
                <InputLabelComponent LabelText="@Localizer["PublishDate_Label"]" For="() => Model.PublishDate">
 | 
						|
                    @if (Article?.Status is null or ArticleStatus.Draft)
 | 
						|
                    {
 | 
						|
                        <InputDate class="input input-bordered w-full" Type="InputDateType.DateTimeLocal"
 | 
						|
                                   @bind-Value="@Model.PublishDate" placeholder="@Localizer["PublishDate_Placeholder"]" autocomplete="off" />
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        <input class="input input-bordered w-full"
 | 
						|
                               type="datetime-local" readonly value="@Article?.PublishDate.ToString("yyyy-MM-dd\\THH:mm:ss")" />
 | 
						|
                    }
 | 
						|
                </InputLabelComponent>
 | 
						|
 | 
						|
                <AdvancedMarkdownEditor Title="@Model.Title" MarkdownCallback="() => Model.Body">
 | 
						|
                    <InputLabelComponent LabelText="@Localizer["Body_Label"]" For="() => Model.Body">
 | 
						|
                        <textarea class="textarea textarea-bordered w-full min-h-96 h-full" required aria-required
 | 
						|
                                  @bind="@Model.Body" @bind:event="oninput" placeholder="@Localizer["Body_Placeholder"]" autocomplete="off"></textarea>
 | 
						|
                    </InputLabelComponent>
 | 
						|
                </AdvancedMarkdownEditor>
 | 
						|
 | 
						|
                <div class="flex gap-2 flex-wrap mt-3">
 | 
						|
                    <button type="submit" class="btn btn-primary w-full sm:btn-wide">
 | 
						|
                        @Localizer["EditorSubmit"]
 | 
						|
                    </button>
 | 
						|
                    @if (Article is not null)
 | 
						|
                    {
 | 
						|
                        <a class="btn w-full sm:btn-wide" href="/article/@(Article.Id)">
 | 
						|
                            @Localizer["ViewArticle_Label"]
 | 
						|
                        </a>
 | 
						|
                    }
 | 
						|
                </div>
 | 
						|
            </EditForm>
 | 
						|
        </ChildContent>
 | 
						|
        <ErrorContent>
 | 
						|
            <h1 class="text-3xl lg:text-5xl font-light mb-6">Not found</h1>
 | 
						|
        </ErrorContent>
 | 
						|
    </ErrorBoundary>
 | 
						|
}
 | 
						|
 | 
						|
@code {
 | 
						|
    [CascadingParameter(Name = "TitlePrefix")]
 | 
						|
    private string TitlePrefix { get; set; } = default!;
 | 
						|
    [CascadingParameter]
 | 
						|
    private Task<AuthenticationState>? AuthenticationState { get; set; }
 | 
						|
 | 
						|
    [Parameter]
 | 
						|
    public Guid? Id { get; set; }
 | 
						|
    [SupplyParameterFromForm]
 | 
						|
    private InputModel Model { get; set; } = new();
 | 
						|
 | 
						|
    private ApplicationUser? User { get; set; }
 | 
						|
    private Article? Article { get; set; }
 | 
						|
    private bool Busy { get; set; } = true;
 | 
						|
 | 
						|
    protected override async Task OnInitializedAsync() {
 | 
						|
        if (AuthenticationState is null) throw new ApplicationException("???");
 | 
						|
        var state = await AuthenticationState;
 | 
						|
        var user = await UserManager.GetUserAsync(state.User);
 | 
						|
        User = user ?? throw new ApplicationException("???2");
 | 
						|
 | 
						|
        if (Id is not null) {
 | 
						|
            await using var context = await ContextFactory.CreateDbContextAsync();
 | 
						|
            Article = await context.Set<Article>()
 | 
						|
                .Include(a => a.Author)
 | 
						|
                .Include(a => a.Reviewer)
 | 
						|
                .FirstAsync(a => a.Id == Id);
 | 
						|
            if (Article is null) throw new ApplicationException("Article not found.");
 | 
						|
 | 
						|
            // Check permissions
 | 
						|
            if (state.User.IsInRole("Admin")) {
 | 
						|
                // Admins always have access
 | 
						|
            } else if (Article.Status is ArticleStatus.Draft && Article.Author.Id == User.Id) {
 | 
						|
                // It's our draft
 | 
						|
            } else if (Article.Status is ArticleStatus.InReview && state.User.IsInRole("Reviewer")) {
 | 
						|
                // It's in reviewer, and we are reviewer
 | 
						|
            } else if (Article.Status is ArticleStatus.Published && state.User.IsInRole("Moderator")) {
 | 
						|
                // It's published, and we are moderator
 | 
						|
            } else {
 | 
						|
                throw new ApplicationException("You do not have permissions to edit this article");
 | 
						|
            }
 | 
						|
        }
 | 
						|
        Model.Id ??= Article?.Id;
 | 
						|
        Model.Title ??= Article?.Title;
 | 
						|
        Model.Body ??= Article?.Body;
 | 
						|
        Model.PublishDate ??= Article?.PublishDate.LocalDateTime;
 | 
						|
        Busy = false;
 | 
						|
    }
 | 
						|
 | 
						|
    private async Task OnValidSubmit() {
 | 
						|
        if (User is null) return;
 | 
						|
        Busy = true;
 | 
						|
 | 
						|
        await using var context = await ContextFactory.CreateDbContextAsync();
 | 
						|
        context.Entry(User).State = EntityState.Unchanged;
 | 
						|
 | 
						|
        Article article;
 | 
						|
        if (Model.Id is not null) {
 | 
						|
            article = await context.Set<Article>()
 | 
						|
                .Include(a => a.Author)
 | 
						|
                .Include(a => a.Reviewer)
 | 
						|
                .FirstAsync(a => a.Id == Model.Id);
 | 
						|
            article.Title = Model.Title!;
 | 
						|
            article.Body = Model.Body!;
 | 
						|
        } else {
 | 
						|
            article = new Article {
 | 
						|
                Title = Model.Title!,
 | 
						|
                Body = Model.Body!,
 | 
						|
                Author = User
 | 
						|
            };
 | 
						|
            await context.AddAsync(article);
 | 
						|
        }
 | 
						|
        if (Model.PublishDate is not null) article.PublishDate = Model.PublishDate.Value;
 | 
						|
 | 
						|
        await HandleRoles(article, User);
 | 
						|
        article.LastModified = DateTimeOffset.UtcNow;
 | 
						|
        article.BodyHtml = MarkdownUtilities.Parse(article.Body);
 | 
						|
        
 | 
						|
        await context.SaveChangesAsync();
 | 
						|
        Navigation.NavigateTo($"/article/{article.Id}");
 | 
						|
    }
 | 
						|
 | 
						|
    private async Task HandleRoles(Article article, ApplicationUser me) {
 | 
						|
        // it's our draft
 | 
						|
        if (article.Status is ArticleStatus.Draft && article.Author.Id == me.Id) return;
 | 
						|
 | 
						|
        var roles = await UserManager.GetRolesAsync(me);
 | 
						|
 | 
						|
        // reviewers and admins can review articles
 | 
						|
        if (article.Status is ArticleStatus.InReview && roles.Any(r => r is "Admin" or "Reviewer")) {
 | 
						|
            article.Reviewer = me;
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        // published articles may only be edited my admins or moderators 
 | 
						|
        if (article.Status is ArticleStatus.Published && roles.Any(r => r is "Admin" or "Reviewer")) {
 | 
						|
            article.Reviewer = me; // TODO replace with editor or something?
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        throw new ApplicationException("You do not have permissions to edit this article");
 | 
						|
    }
 | 
						|
 | 
						|
    private sealed class InputModel {
 | 
						|
        public Guid? Id { get; set; }
 | 
						|
        [Required(AllowEmptyStrings = false), MaxLength(256)]
 | 
						|
        public string? Title { get; set; }
 | 
						|
        [Required(AllowEmptyStrings = false)]
 | 
						|
        public string? Body { get; set; }
 | 
						|
 | 
						|
        public DateTimeOffset? PublishDate { get; set; }
 | 
						|
    }
 | 
						|
}
 |