Skip to content

Commit

Permalink
feat: Allow Member only posts
Browse files Browse the repository at this point in the history
  • Loading branch information
linkdotnet committed Nov 27, 2024
1 parent 391fd5d commit 0a080bd
Show file tree
Hide file tree
Showing 22 changed files with 290 additions and 138 deletions.
5 changes: 5 additions & 0 deletions src/LinkDotNet.Blog.Domain/BlogPost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public sealed partial class BlogPost : Entity

public int ReadingTimeInMinutes { get; private set; }

public bool IsMembersOnly { get; private set; }

public string Slug => GenerateSlug();

private string GenerateSlug()
Expand Down Expand Up @@ -89,6 +91,7 @@ public static BlogPost Create(
string content,
string previewImageUrl,
bool isPublished,
bool isMembersOnly,
DateTime? updatedDate = null,
DateTime? scheduledPublishDate = null,
IEnumerable<string>? tags = null,
Expand All @@ -113,6 +116,7 @@ public static BlogPost Create(
IsPublished = isPublished,
Tags = tags?.Select(t => t.Trim()).ToImmutableArray() ?? [],
ReadingTimeInMinutes = ReadingTimeCalculator.CalculateReadingTime(content),
IsMembersOnly = isMembersOnly,
};

return blogPost;
Expand Down Expand Up @@ -141,6 +145,7 @@ public void Update(BlogPost from)
PreviewImageUrl = from.PreviewImageUrl;
PreviewImageUrlFallback = from.PreviewImageUrlFallback;
IsPublished = from.IsPublished;
IsMembersOnly = from.IsMembersOnly;
Tags = from.Tags;
ReadingTimeInMinutes = from.ReadingTimeInMinutes;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ public static void UseAuthentication(this IServiceCollection services)
};
});

services.AddAuthorization(options =>
{
options.AddPolicy("Admin", policy => policy.RequireRole("Admin"));
options.AddPolicy("Member", policy => policy.RequireRole("Member"));
});

services.AddHttpContextAccessor();
services.AddScoped<ILoginManager, AuthLoginManager>();
}
Expand Down
5 changes: 3 additions & 2 deletions src/LinkDotNet.Blog.Web/Features/AboutMe/AboutMePage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@

protected override async Task OnInitializedAsync()
{
var userIdentity = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User.Identity;
isAuthenticated = userIdentity?.IsAuthenticated ?? false;
var principal = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User;
var userIdentity = principal.Identity;
isAuthenticated = (userIdentity?.IsAuthenticated ?? false) && principal.IsInRole("Admin");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,85 +31,84 @@
@bind-Value="@model.Content"></MarkdownTextArea>
<ValidationMessage For="() => model.Content"></ValidationMessage>

<div class="btn-group position-absolute bottom-0 end-0 m-5 extra-buttons">
<button class="btn btn-primary btn-outlined btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
More
</button>
<ul class="dropdown-menu">
@if (shortCodes.Count > 0)
{
<li>
<button type="button" @onclick="OpenShortCodeDialog" class="dropdown-item">
<span>Get shortcode</span>
</button>
</li>
}
<li><button type="button" class="dropdown-item" @onclick="FeatureDialog.Open">Experimental Features</button></li>
<div class="btn-group position-absolute bottom-0 end-0 m-5 extra-buttons">
<button class="btn btn-primary btn-outlined btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
More
</button>
<ul class="dropdown-menu">
@if (shortCodes.Count > 0)
{
<li>
<button type="button" @onclick="OpenShortCodeDialog" class="dropdown-item">
<span>Get shortcode</span>
</button>
</li>
}
<li><button type="button" class="dropdown-item" @onclick="FeatureDialog.Open">Experimental Features</button></li>
<li><button id="convert" type="button" class="dropdown-item" @onclick="ConvertContent">@ConvertLabel <i class="lab"></i></button></li>
</ul>
</div>
</div>
<div class="form-floating mb-3">
<InputText type="url" class="form-control" id="preview" placeholder="Preview-Url" @bind-Value="model.PreviewImageUrl" />
<label for="preview">Preview-Url</label>
<small for="preview" class="form-text text-body-secondary">The primary image which will be used.</small>
<ValidationMessage For="() => model.PreviewImageUrl"></ValidationMessage>
</div>
<div class="form-floating mb-3">
<InputText type="url" class="form-control" id="fallback-preview" placeholder="Fallback Preview-Url" @bind-Value="model.PreviewImageUrlFallback" />
<label for="fallback-preview">Fallback Preview-Url</label>
<small for="fallback-preview" class="form-text text-body-secondary">
Optional: Used as a fallback if the preview image can't be used by the browser.
<br>For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.
</small>
<ValidationMessage For="() => model.PreviewImageUrlFallback"></ValidationMessage>
</div>
<div class="form-floating mb-3">
<InputDate Type="InputDateType.DateTimeLocal" class="form-control" id="scheduled"
placeholder="Scheduled Publish Date" @bind-Value="model.ScheduledPublishDate"
@bind-Value:after="@(() => model.IsPublished &= !IsScheduled)" />
<label for="scheduled">Scheduled Publish Date</label>
<small for="scheduled" class="form-text text-body-secondary">
If set the blog post will be published at the given date.
A blog post with a schedule date can't be set to published.
</small>
<ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage>
</div>
<div class="form-check form-switch mb-3">
<InputCheckbox class="form-check-input" id="published" @bind-Value="model.IsPublished" />
<label class="form-check-label" for="published">Publish</label><br />
<small for="published" class="form-text text-body-secondary">If this blog post is only draft or it will be scheduled, uncheck the box.</small>
<ValidationMessage For="() => model.IsPublished"></ValidationMessage>
</div>
<div class="form-floating mb-3">
<InputText type="text" class="form-control" id="tags" placeholder="Tags" @bind-Value="model.Tags" />
<label for="tags">Tags (Comma separated)</label>
</div>
@if (BlogPost is not null && !IsScheduled)
{
<div class="form-check form-switch mb-3">
<InputCheckbox class="form-check-input" id="updatedate" @bind-Value="model.ShouldUpdateDate" />
<label class="form-check-label" for="updatedate">Update Publish Date</label><br />
<small for="updatedate" class="form-text text-body-secondary">
If set the publish date is set to now,
otherwise its original date.
</small>
</div>
}
<div class="mb-3">
<button class="btn btn-primary position-relative" type="submit" disabled="@(!canSubmit)">Submit</button>
<div class="alert alert-info text-muted form-text mt-3 mb-3">
The first page of the blog is cached. Therefore, the blog post is not immediately visible.
Head over to <a href="/settings">settings</a> to invalidate the cache or enable the checkmark.
<br>
The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page.
</div>
<div class="form-check form-switch mb-3">
<InputCheckbox class="form-check-input" id="invalidate-cache" @bind-Value="model.ShouldInvalidateCache" />
<label class="form-check-label" for="invalidate-cache">Make it visible immediately</label><br />
</div>
</div>
</EditForm>
</ul>
</div>
</div>
<div class="form-floating mb-3">
<InputText type="url" class="form-control" id="preview" placeholder="Preview-Url" @bind-Value="model.PreviewImageUrl"/>
<label for="preview">Preview-Url</label>
<small for="preview" class="form-text text-body-secondary">The primary image which will be used.</small>
<ValidationMessage For="() => model.PreviewImageUrl"></ValidationMessage>
</div>
<div class="form-floating mb-3">
<InputText type="url" class="form-control" id="fallback-preview" placeholder="Fallback Preview-Url" @bind-Value="model.PreviewImageUrlFallback"/>
<label for="fallback-preview">Fallback Preview-Url</label>
<small for="fallback-preview" class="form-text text-body-secondary">Optional: Used as a fallback if the preview image can't be used by the browser.
<br>For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.</small>
<ValidationMessage For="() => model.PreviewImageUrlFallback"></ValidationMessage>
</div>
<div class="form-floating mb-3">
<InputDate Type="InputDateType.DateTimeLocal" class="form-control" id="scheduled"
placeholder="Scheduled Publish Date" @bind-Value="model.ScheduledPublishDate"
@bind-Value:after="@(() => model.IsPublished &= !IsScheduled)"/>
<label for="scheduled">Scheduled Publish Date</label>
<small for="scheduled" class="form-text text-body-secondary">If set the blog post will be published at the given date.
A blog post with a schedule date can't be set to published.</small>
<ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage>
</div>
<div class="form-check form-switch mb-3">
<InputCheckbox class="form-check-input" id="published" @bind-Value="model.IsPublished"/>
<label class="form-check-label" for="published">Publish</label><br/>
<small for="published" class="form-text text-body-secondary">If this blog post is only draft or it will be scheduled, uncheck the box.</small>
<ValidationMessage For="() => model.IsPublished"></ValidationMessage>
</div>
<div class="form-floating mb-3">
<InputText type="text" class="form-control" id="tags" placeholder="Tags" @bind-Value="model.Tags"/>
<label for="tags">Tags</label>
</div>
<div class="form-check form-switch mb-3">
<InputCheckbox class="form-check-input" id="members-only" @bind-Value="model.IsMembersOnly" />
<label class="form-check-label" for="members-only">Members only?</label><br/>
<small for="updatedate" class="form-text text-body-secondary">The blog post can only be read by members.</small>
</div>
@if (BlogPost is not null && !IsScheduled)
{
<div class="form-check form-switch mb-3">
<InputCheckbox class="form-check-input" id="updatedate" @bind-Value="model.ShouldUpdateDate" />
<label class="form-check-label" for="updatedate">Update Publish Date</label><br/>
<small for="updatedate" class="form-text text-body-secondary">If set the publish date is set to now,
otherwise its original date.</small>
</div>
}
<div class="mb-3">
<button class="btn btn-primary position-relative" type="submit" disabled="@(!canSubmit)">Submit</button>
<div class="alert alert-info text-muted form-text mt-3 mb-3">
The first page of the blog is cached. Therefore, the blog post is not immediately visible.
Head over to <a href="/settings">settings</a> to invalidate the cache or enable the checkmark.
<br>
The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page.
</div>
<div class="form-check form-switch mb-3">
<InputCheckbox class="form-check-input" id="invalidate-cache" @bind-Value="model.ShouldInvalidateCache"/>
<label class="form-check-label" for="invalidate-cache">Make it visible immediately</label><br/>
</div>
</div>
</EditForm>
</div>

<FeatureInfoDialog @ref="FeatureDialog"></FeatureInfoDialog>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public sealed class CreateNewModel
private string tags = string.Empty;
private string previewImageUrlFallback = string.Empty;
private DateTime? scheduledPublishDate;
private bool isMembersOnly;

[Required]
[MaxLength(256)]
Expand Down Expand Up @@ -64,6 +65,12 @@ public bool ShouldUpdateDate
set => SetProperty(out shouldUpdateDate, value);
}

public bool IsMembersOnly
{
get => isMembersOnly;
set => SetProperty(out isMembersOnly, value);
}

[FutureDateValidation]
public DateTime? ScheduledPublishDate
{
Expand Down Expand Up @@ -128,6 +135,7 @@ public BlogPost ToBlogPost()
Content,
PreviewImageUrl,
IsPublished,
IsMembersOnly,
updatedDate,
scheduledPublishDate,
tagList,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@page "/create"
@attribute [Authorize]
@attribute [Authorize(Roles = "Admin")]
@using LinkDotNet.Blog.Domain
@using LinkDotNet.Blog.Infrastructure.Persistence
@using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components
Expand Down
14 changes: 7 additions & 7 deletions src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/SitemapPage.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@page "/Sitemap"
@using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services
@inject ISitemapService SitemapService
@attribute [Authorize]
@attribute [Authorize(Roles = "Admin")]
<div class="container">
<h3>Sitemap</h3>
<div class="row px-2">
Expand All @@ -11,12 +11,12 @@
If you get a 404 there is currently no sitemap.xml</p>
<button class="btn btn-primary" @onclick="CreateSitemap" disabled="@isGenerating">Create Sitemap</button>

@if (isGenerating)
{
<Loading></Loading>
}
@if (sitemapUrlSet is not null)
{
@if (isGenerating)
{
<Loading></Loading>
}
@if (sitemapUrlSet is not null)
{
<table class="table table-striped table-hover h-50">
<thead>
<tr>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@using LinkDotNet.Blog.Domain

<article>
<div class="blog-card @AltCssClass">
<div class="blog-card @AltCssClass @(BlogPost.IsMembersOnly ? "border border-warning" : "")">
<div class="meta">
<div class="photo">
<PreviewImage PreviewImageUrl="@BlogPost.PreviewImageUrl"
Expand Down Expand Up @@ -37,7 +37,7 @@
<h2></h2>
<p>@MarkdownConverter.ToMarkupString(BlogPost.ShortDescription)</p>
<p class="read-more">
<a href="/blogPost/@BlogPost.Id/@BlogPost.Slug" aria-label="@BlogPost.Title">Read the whole article</a>
<a href="/blogPost/@BlogPost.Id/@BlogPost.Slug" aria-label="@BlogPost.Title">Read the whole article</a>
</p>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,33 @@
<AuthorizeView>
<Authorized>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="user-tie"></i> Admin
</a>
<ul class="dropdown-menu ps-0" aria-labelledby="navbarDropdown">
<li><h6 class="dropdown-header">Blog posts</h6></li>
<li><a class="dropdown-item" href="create">Create new</a></li>
<li><a class="dropdown-item" href="draft">Show drafts</a></li>
<li><a class="dropdown-item" href="settings">Show settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Analytics</h6></li>
<li><a class="dropdown-item" href="dashboard">Dashboard</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Others</h6></li>
<li><a class="dropdown-item" href="short-codes">Shortcodes</a></li>
<li><a class="dropdown-item" href="Sitemap">Sitemap</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" target="_blank" href="https://github.com/linkdotnet/Blog/releases" rel="noreferrer">Releases</a></li>
</ul>
</li>
<li class="nav-item"><a class="nav-link" href="logout?redirectUri=@CurrentUri"><i class="lock"></i> Log out</a></li>
</Authorized>
<NotAuthorized>
<li class="nav-item"><a class="nav-link" href="login?redirectUri=@CurrentUri" rel="nofollow"><i class="unlocked"></i> Log in</a></li>
</NotAuthorized>
<AuthorizeView Roles="Admin,Member">
<Authorized>
<AuthorizeView Roles="Admin" Context="AdminContext">
<li class="nav-item dropdown" id="admin-actions">
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
<i class="user-tie"></i> Admin
</a>
<ul class="dropdown-menu ps-0" aria-labelledby="navbarDropdown">
<li><h6 class="dropdown-header">Blog posts</h6></li>
<li><a class="dropdown-item" href="create">Create new</a></li>
<li><a class="dropdown-item" href="draft">Show drafts</a></li>
<li><a class="dropdown-item" href="settings">Show settings</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Analytics</h6></li>
<li><a class="dropdown-item" href="dashboard">Dashboard</a></li>
<li><hr class="dropdown-divider"></li>
<li><h6 class="dropdown-header">Others</h6></li>
<li><a class="dropdown-item" href="short-codes">Shortcodes</a></li>
<li><a class="dropdown-item" href="Sitemap">Sitemap</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" target="_blank" href="https://github.com/linkdotnet/Blog/releases" rel="noreferrer">Releases</a></li>
</ul>
</li>
</AuthorizeView>
<li class="nav-item"><a class="nav-link" href="logout?redirectUri=@CurrentUri"><i class="lock"></i> Log out</a></li>
</Authorized>
<NotAuthorized>
<li class="nav-item"><a class="nav-link" href="login?redirectUri=@CurrentUri" rel="nofollow"><i class="unlocked"></i> Log in</a></li>
</NotAuthorized>
</AuthorizeView>

@code {
Expand Down
2 changes: 1 addition & 1 deletion src/LinkDotNet.Blog.Web/Features/Home/Index.razor
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
AbsolutePreviewImageUrl="@ImageUrl"
Description="@(Markdown.ToPlainText(Introduction.Value.Description))"></OgData>
<section>
<IntroductionCard></IntroductionCard>
<IntroductionCard></IntroductionCard>
</section>

<section>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@inject IRepository<BlogPost> BlogPostRepository
@inject IInstantJobRegistry InstantJobRegistry

<AuthorizeView>
<AuthorizeView Roles="Admin">
<div class="d-flex justify-content-start gap-2">
<a id="edit-blogpost" type="button" class="btn btn-primary d-flex align-items-center gap-2" href="update/@BlogPostId" aria-label="edit">
<i class="pencil"></i>
Expand Down
Loading

0 comments on commit 0a080bd

Please sign in to comment.