Skip to content

Commit adb0327

Browse files
committed
feat: Allow Member only posts
1 parent 911c8a7 commit adb0327

File tree

22 files changed

+290
-138
lines changed

22 files changed

+290
-138
lines changed

src/LinkDotNet.Blog.Domain/BlogPost.cs

+5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ public sealed partial class BlogPost : Entity
3636

3737
public int ReadingTimeInMinutes { get; private set; }
3838

39+
public bool IsMembersOnly { get; private set; }
40+
3941
public string Slug => GenerateSlug();
4042

4143
private string GenerateSlug()
@@ -89,6 +91,7 @@ public static BlogPost Create(
8991
string content,
9092
string previewImageUrl,
9193
bool isPublished,
94+
bool isMembersOnly,
9295
DateTime? updatedDate = null,
9396
DateTime? scheduledPublishDate = null,
9497
IEnumerable<string>? tags = null,
@@ -113,6 +116,7 @@ public static BlogPost Create(
113116
IsPublished = isPublished,
114117
Tags = tags?.Select(t => t.Trim()).ToImmutableArray() ?? [],
115118
ReadingTimeInMinutes = ReadingTimeCalculator.CalculateReadingTime(content),
119+
IsMembersOnly = isMembersOnly,
116120
};
117121

118122
return blogPost;
@@ -141,6 +145,7 @@ public void Update(BlogPost from)
141145
PreviewImageUrl = from.PreviewImageUrl;
142146
PreviewImageUrlFallback = from.PreviewImageUrlFallback;
143147
IsPublished = from.IsPublished;
148+
IsMembersOnly = from.IsMembersOnly;
144149
Tags = from.Tags;
145150
ReadingTimeInMinutes = from.ReadingTimeInMinutes;
146151
}

src/LinkDotNet.Blog.Web/Authentication/OpenIdConnect/AuthExtensions.cs

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ public static void UseAuthentication(this IServiceCollection services)
5353
};
5454
});
5555

56+
services.AddAuthorization(options =>
57+
{
58+
options.AddPolicy("Admin", policy => policy.RequireRole("Admin"));
59+
options.AddPolicy("Member", policy => policy.RequireRole("Member"));
60+
});
61+
5662
services.AddHttpContextAccessor();
5763
services.AddScoped<ILoginManager, AuthLoginManager>();
5864
}

src/LinkDotNet.Blog.Web/Features/AboutMe/AboutMePage.razor

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030

3131
protected override async Task OnInitializedAsync()
3232
{
33-
var userIdentity = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User.Identity;
34-
isAuthenticated = userIdentity?.IsAuthenticated ?? false;
33+
var principal = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User;
34+
var userIdentity = principal.Identity;
35+
isAuthenticated = (userIdentity?.IsAuthenticated ?? false) && principal.IsInRole("Admin");
3536
}
3637
}

src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewBlogPost.razor

+77-78
Original file line numberDiff line numberDiff line change
@@ -31,85 +31,84 @@
3131
@bind-Value="@model.Content"></MarkdownTextArea>
3232
<ValidationMessage For="() => model.Content"></ValidationMessage>
3333

34-
<div class="btn-group position-absolute bottom-0 end-0 m-5 extra-buttons">
35-
<button class="btn btn-primary btn-outlined btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
36-
More
37-
</button>
38-
<ul class="dropdown-menu">
39-
@if (shortCodes.Count > 0)
40-
{
41-
<li>
42-
<button type="button" @onclick="OpenShortCodeDialog" class="dropdown-item">
43-
<span>Get shortcode</span>
44-
</button>
45-
</li>
46-
}
47-
<li><button type="button" class="dropdown-item" @onclick="FeatureDialog.Open">Experimental Features</button></li>
34+
<div class="btn-group position-absolute bottom-0 end-0 m-5 extra-buttons">
35+
<button class="btn btn-primary btn-outlined btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
36+
More
37+
</button>
38+
<ul class="dropdown-menu">
39+
@if (shortCodes.Count > 0)
40+
{
41+
<li>
42+
<button type="button" @onclick="OpenShortCodeDialog" class="dropdown-item">
43+
<span>Get shortcode</span>
44+
</button>
45+
</li>
46+
}
47+
<li><button type="button" class="dropdown-item" @onclick="FeatureDialog.Open">Experimental Features</button></li>
4848
<li><button id="convert" type="button" class="dropdown-item" @onclick="ConvertContent">@ConvertLabel <i class="lab"></i></button></li>
49-
</ul>
50-
</div>
51-
</div>
52-
<div class="form-floating mb-3">
53-
<InputText type="url" class="form-control" id="preview" placeholder="Preview-Url" @bind-Value="model.PreviewImageUrl" />
54-
<label for="preview">Preview-Url</label>
55-
<small for="preview" class="form-text text-body-secondary">The primary image which will be used.</small>
56-
<ValidationMessage For="() => model.PreviewImageUrl"></ValidationMessage>
57-
</div>
58-
<div class="form-floating mb-3">
59-
<InputText type="url" class="form-control" id="fallback-preview" placeholder="Fallback Preview-Url" @bind-Value="model.PreviewImageUrlFallback" />
60-
<label for="fallback-preview">Fallback Preview-Url</label>
61-
<small for="fallback-preview" class="form-text text-body-secondary">
62-
Optional: Used as a fallback if the preview image can't be used by the browser.
63-
<br>For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.
64-
</small>
65-
<ValidationMessage For="() => model.PreviewImageUrlFallback"></ValidationMessage>
66-
</div>
67-
<div class="form-floating mb-3">
68-
<InputDate Type="InputDateType.DateTimeLocal" class="form-control" id="scheduled"
69-
placeholder="Scheduled Publish Date" @bind-Value="model.ScheduledPublishDate"
70-
@bind-Value:after="@(() => model.IsPublished &= !IsScheduled)" />
71-
<label for="scheduled">Scheduled Publish Date</label>
72-
<small for="scheduled" class="form-text text-body-secondary">
73-
If set the blog post will be published at the given date.
74-
A blog post with a schedule date can't be set to published.
75-
</small>
76-
<ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage>
77-
</div>
78-
<div class="form-check form-switch mb-3">
79-
<InputCheckbox class="form-check-input" id="published" @bind-Value="model.IsPublished" />
80-
<label class="form-check-label" for="published">Publish</label><br />
81-
<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>
82-
<ValidationMessage For="() => model.IsPublished"></ValidationMessage>
83-
</div>
84-
<div class="form-floating mb-3">
85-
<InputText type="text" class="form-control" id="tags" placeholder="Tags" @bind-Value="model.Tags" />
86-
<label for="tags">Tags (Comma separated)</label>
87-
</div>
88-
@if (BlogPost is not null && !IsScheduled)
89-
{
90-
<div class="form-check form-switch mb-3">
91-
<InputCheckbox class="form-check-input" id="updatedate" @bind-Value="model.ShouldUpdateDate" />
92-
<label class="form-check-label" for="updatedate">Update Publish Date</label><br />
93-
<small for="updatedate" class="form-text text-body-secondary">
94-
If set the publish date is set to now,
95-
otherwise its original date.
96-
</small>
97-
</div>
98-
}
99-
<div class="mb-3">
100-
<button class="btn btn-primary position-relative" type="submit" disabled="@(!canSubmit)">Submit</button>
101-
<div class="alert alert-info text-muted form-text mt-3 mb-3">
102-
The first page of the blog is cached. Therefore, the blog post is not immediately visible.
103-
Head over to <a href="/settings">settings</a> to invalidate the cache or enable the checkmark.
104-
<br>
105-
The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page.
106-
</div>
107-
<div class="form-check form-switch mb-3">
108-
<InputCheckbox class="form-check-input" id="invalidate-cache" @bind-Value="model.ShouldInvalidateCache" />
109-
<label class="form-check-label" for="invalidate-cache">Make it visible immediately</label><br />
110-
</div>
111-
</div>
112-
</EditForm>
49+
</ul>
50+
</div>
51+
</div>
52+
<div class="form-floating mb-3">
53+
<InputText type="url" class="form-control" id="preview" placeholder="Preview-Url" @bind-Value="model.PreviewImageUrl"/>
54+
<label for="preview">Preview-Url</label>
55+
<small for="preview" class="form-text text-body-secondary">The primary image which will be used.</small>
56+
<ValidationMessage For="() => model.PreviewImageUrl"></ValidationMessage>
57+
</div>
58+
<div class="form-floating mb-3">
59+
<InputText type="url" class="form-control" id="fallback-preview" placeholder="Fallback Preview-Url" @bind-Value="model.PreviewImageUrlFallback"/>
60+
<label for="fallback-preview">Fallback Preview-Url</label>
61+
<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.
62+
<br>For example using a jpg or png as fallback for avif which is not supported in Safari or Edge.</small>
63+
<ValidationMessage For="() => model.PreviewImageUrlFallback"></ValidationMessage>
64+
</div>
65+
<div class="form-floating mb-3">
66+
<InputDate Type="InputDateType.DateTimeLocal" class="form-control" id="scheduled"
67+
placeholder="Scheduled Publish Date" @bind-Value="model.ScheduledPublishDate"
68+
@bind-Value:after="@(() => model.IsPublished &= !IsScheduled)"/>
69+
<label for="scheduled">Scheduled Publish Date</label>
70+
<small for="scheduled" class="form-text text-body-secondary">If set the blog post will be published at the given date.
71+
A blog post with a schedule date can't be set to published.</small>
72+
<ValidationMessage For="() => model.ScheduledPublishDate"></ValidationMessage>
73+
</div>
74+
<div class="form-check form-switch mb-3">
75+
<InputCheckbox class="form-check-input" id="published" @bind-Value="model.IsPublished"/>
76+
<label class="form-check-label" for="published">Publish</label><br/>
77+
<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>
78+
<ValidationMessage For="() => model.IsPublished"></ValidationMessage>
79+
</div>
80+
<div class="form-floating mb-3">
81+
<InputText type="text" class="form-control" id="tags" placeholder="Tags" @bind-Value="model.Tags"/>
82+
<label for="tags">Tags</label>
83+
</div>
84+
<div class="form-check form-switch mb-3">
85+
<InputCheckbox class="form-check-input" id="members-only" @bind-Value="model.IsMembersOnly" />
86+
<label class="form-check-label" for="members-only">Members only?</label><br/>
87+
<small for="updatedate" class="form-text text-body-secondary">The blog post can only be read by members.</small>
88+
</div>
89+
@if (BlogPost is not null && !IsScheduled)
90+
{
91+
<div class="form-check form-switch mb-3">
92+
<InputCheckbox class="form-check-input" id="updatedate" @bind-Value="model.ShouldUpdateDate" />
93+
<label class="form-check-label" for="updatedate">Update Publish Date</label><br/>
94+
<small for="updatedate" class="form-text text-body-secondary">If set the publish date is set to now,
95+
otherwise its original date.</small>
96+
</div>
97+
}
98+
<div class="mb-3">
99+
<button class="btn btn-primary position-relative" type="submit" disabled="@(!canSubmit)">Submit</button>
100+
<div class="alert alert-info text-muted form-text mt-3 mb-3">
101+
The first page of the blog is cached. Therefore, the blog post is not immediately visible.
102+
Head over to <a href="/settings">settings</a> to invalidate the cache or enable the checkmark.
103+
<br>
104+
The option should be enabled if you want to publish the blog post immediately and it should be visible on the first page.
105+
</div>
106+
<div class="form-check form-switch mb-3">
107+
<InputCheckbox class="form-check-input" id="invalidate-cache" @bind-Value="model.ShouldInvalidateCache"/>
108+
<label class="form-check-label" for="invalidate-cache">Make it visible immediately</label><br/>
109+
</div>
110+
</div>
111+
</EditForm>
113112
</div>
114113

115114
<FeatureInfoDialog @ref="FeatureDialog"></FeatureInfoDialog>

src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/Components/CreateNewModel.cs

+8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public sealed class CreateNewModel
1818
private string tags = string.Empty;
1919
private string previewImageUrlFallback = string.Empty;
2020
private DateTime? scheduledPublishDate;
21+
private bool isMembersOnly;
2122

2223
[Required]
2324
[MaxLength(256)]
@@ -64,6 +65,12 @@ public bool ShouldUpdateDate
6465
set => SetProperty(out shouldUpdateDate, value);
6566
}
6667

68+
public bool IsMembersOnly
69+
{
70+
get => isMembersOnly;
71+
set => SetProperty(out isMembersOnly, value);
72+
}
73+
6774
[FutureDateValidation]
6875
public DateTime? ScheduledPublishDate
6976
{
@@ -128,6 +135,7 @@ public BlogPost ToBlogPost()
128135
Content,
129136
PreviewImageUrl,
130137
IsPublished,
138+
IsMembersOnly,
131139
updatedDate,
132140
scheduledPublishDate,
133141
tagList,

src/LinkDotNet.Blog.Web/Features/Admin/BlogPostEditor/CreateBlogPost.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
@page "/create"
2-
@attribute [Authorize]
2+
@attribute [Authorize(Roles = "Admin")]
33
@using LinkDotNet.Blog.Domain
44
@using LinkDotNet.Blog.Infrastructure.Persistence
55
@using LinkDotNet.Blog.Web.Features.Admin.BlogPostEditor.Components

src/LinkDotNet.Blog.Web/Features/Admin/Sitemap/SitemapPage.razor

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@page "/Sitemap"
22
@using LinkDotNet.Blog.Web.Features.Admin.Sitemap.Services
33
@inject ISitemapService SitemapService
4-
@attribute [Authorize]
4+
@attribute [Authorize(Roles = "Admin")]
55
<div class="container">
66
<h3>Sitemap</h3>
77
<div class="row px-2">
@@ -11,12 +11,12 @@
1111
If you get a 404 there is currently no sitemap.xml</p>
1212
<button class="btn btn-primary" @onclick="CreateSitemap" disabled="@isGenerating">Create Sitemap</button>
1313

14-
@if (isGenerating)
15-
{
16-
<Loading></Loading>
17-
}
18-
@if (sitemapUrlSet is not null)
19-
{
14+
@if (isGenerating)
15+
{
16+
<Loading></Loading>
17+
}
18+
@if (sitemapUrlSet is not null)
19+
{
2020
<table class="table table-striped table-hover h-50">
2121
<thead>
2222
<tr>

src/LinkDotNet.Blog.Web/Features/Components/ShortBlogPost.razor

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@using LinkDotNet.Blog.Domain
22

33
<article>
4-
<div class="blog-card @AltCssClass">
4+
<div class="blog-card @AltCssClass @(BlogPost.IsMembersOnly ? "border border-warning" : "")">
55
<div class="meta">
66
<div class="photo">
77
<PreviewImage PreviewImageUrl="@BlogPost.PreviewImageUrl"
@@ -37,7 +37,7 @@
3737
<h2></h2>
3838
<p>@MarkdownConverter.ToMarkupString(BlogPost.ShortDescription)</p>
3939
<p class="read-more">
40-
<a href="/blogPost/@BlogPost.Id/@BlogPost.Slug" aria-label="@BlogPost.Title">Read the whole article</a>
40+
<a href="/blogPost/@BlogPost.Id/@BlogPost.Slug" aria-label="@BlogPost.Title">Read the whole article</a>
4141
</p>
4242
</div>
4343
</div>

src/LinkDotNet.Blog.Web/Features/Home/Components/AccessControl.razor

+30-28
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,33 @@
1-
<AuthorizeView>
2-
<Authorized>
3-
<li class="nav-item dropdown">
4-
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
5-
aria-expanded="false">
6-
<i class="user-tie"></i> Admin
7-
</a>
8-
<ul class="dropdown-menu ps-0" aria-labelledby="navbarDropdown">
9-
<li><h6 class="dropdown-header">Blog posts</h6></li>
10-
<li><a class="dropdown-item" href="create">Create new</a></li>
11-
<li><a class="dropdown-item" href="draft">Show drafts</a></li>
12-
<li><a class="dropdown-item" href="settings">Show settings</a></li>
13-
<li><hr class="dropdown-divider"></li>
14-
<li><h6 class="dropdown-header">Analytics</h6></li>
15-
<li><a class="dropdown-item" href="dashboard">Dashboard</a></li>
16-
<li><hr class="dropdown-divider"></li>
17-
<li><h6 class="dropdown-header">Others</h6></li>
18-
<li><a class="dropdown-item" href="short-codes">Shortcodes</a></li>
19-
<li><a class="dropdown-item" href="Sitemap">Sitemap</a></li>
20-
<li><hr class="dropdown-divider"></li>
21-
<li><a class="dropdown-item" target="_blank" href="https://github.com/linkdotnet/Blog/releases" rel="noreferrer">Releases</a></li>
22-
</ul>
23-
</li>
24-
<li class="nav-item"><a class="nav-link" href="logout?redirectUri=@CurrentUri"><i class="lock"></i> Log out</a></li>
25-
</Authorized>
26-
<NotAuthorized>
27-
<li class="nav-item"><a class="nav-link" href="login?redirectUri=@CurrentUri" rel="nofollow"><i class="unlocked"></i> Log in</a></li>
28-
</NotAuthorized>
1+
<AuthorizeView Roles="Admin,Member">
2+
<Authorized>
3+
<AuthorizeView Roles="Admin" Context="AdminContext">
4+
<li class="nav-item dropdown" id="admin-actions">
5+
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown"
6+
aria-expanded="false">
7+
<i class="user-tie"></i> Admin
8+
</a>
9+
<ul class="dropdown-menu ps-0" aria-labelledby="navbarDropdown">
10+
<li><h6 class="dropdown-header">Blog posts</h6></li>
11+
<li><a class="dropdown-item" href="create">Create new</a></li>
12+
<li><a class="dropdown-item" href="draft">Show drafts</a></li>
13+
<li><a class="dropdown-item" href="settings">Show settings</a></li>
14+
<li><hr class="dropdown-divider"></li>
15+
<li><h6 class="dropdown-header">Analytics</h6></li>
16+
<li><a class="dropdown-item" href="dashboard">Dashboard</a></li>
17+
<li><hr class="dropdown-divider"></li>
18+
<li><h6 class="dropdown-header">Others</h6></li>
19+
<li><a class="dropdown-item" href="short-codes">Shortcodes</a></li>
20+
<li><a class="dropdown-item" href="Sitemap">Sitemap</a></li>
21+
<li><hr class="dropdown-divider"></li>
22+
<li><a class="dropdown-item" target="_blank" href="https://github.com/linkdotnet/Blog/releases" rel="noreferrer">Releases</a></li>
23+
</ul>
24+
</li>
25+
</AuthorizeView>
26+
<li class="nav-item"><a class="nav-link" href="logout?redirectUri=@CurrentUri"><i class="lock"></i> Log out</a></li>
27+
</Authorized>
28+
<NotAuthorized>
29+
<li class="nav-item"><a class="nav-link" href="login?redirectUri=@CurrentUri" rel="nofollow"><i class="unlocked"></i> Log in</a></li>
30+
</NotAuthorized>
2931
</AuthorizeView>
3032

3133
@code {

src/LinkDotNet.Blog.Web/Features/Home/Index.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
AbsolutePreviewImageUrl="@ImageUrl"
2020
Description="@(Markdown.ToPlainText(Introduction.Value.Description))"></OgData>
2121
<section>
22-
<IntroductionCard></IntroductionCard>
22+
<IntroductionCard></IntroductionCard>
2323
</section>
2424

2525
<section>

src/LinkDotNet.Blog.Web/Features/ShowBlogPost/Components/BlogPostAdminActions.razor

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
@inject IRepository<BlogPost> BlogPostRepository
77
@inject IInstantJobRegistry InstantJobRegistry
88

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

0 commit comments

Comments
 (0)