Skip to content

Commit

Permalink
Merge branch 'master' into vue
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-lerch committed Dec 7, 2023
2 parents 565a4f2 + 1c85ff8 commit 198c028
Show file tree
Hide file tree
Showing 39 changed files with 1,228 additions and 581 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x
dotnet-version: 8.0.x
- name: Restore NuGet packages
run: dotnet restore
- name: Build
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ RUN npm install
COPY frontend ./
RUN npm run build

FROM mcr.microsoft.com/dotnet/sdk:6.0 AS backend
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS backend
WORKDIR /app

# Copy csproj and restore as distinct layers
Expand All @@ -25,7 +25,7 @@ COPY . ./
RUN dotnet publish --no-restore -c Release -o /app/out src/TravelBlog/TravelBlog.csproj

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:6.0
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=backend /app/out .
COPY --from=frontend /app/dist wwwroot/
Expand Down
8 changes: 4 additions & 4 deletions src/TravelBlog/Configuration/MailingOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ namespace TravelBlog.Configuration;
public class MailingOptions
{
public bool EnableMailing { get; set; }
[Required] public string? SenderName { get; set; }
[Required, EmailAddress] public string? SenderAddress { get; set; }
public string? AuthorName { get; set; }
[EmailAddress] public string? AuthorAddress { get; set; }
[Required] public required string SenderName { get; set; }
[Required, EmailAddress] public required string SenderAddress { get; set; }
[Required] public required string AuthorName { get; set; }
[Required, EmailAddress] public required string AuthorAddress { get; set; }

[Required] public string? SmtpUsername { get; set; }
[Required] public string? SmtpPassword { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/TravelBlog/Configuration/SiteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace TravelBlog.Configuration;

public class SiteOptions
{
[Required] public string? BlogName { get; set; }
[Required] public required string BlogName { get; set; }
[Required] public string? AdminPassword { get; set; }
public bool EnableDebugFeatures { get; set; }
}
52 changes: 17 additions & 35 deletions src/TravelBlog/Controllers/BlogPostController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,34 +6,32 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using TravelBlog.Configuration;
using MimeKit;
using TravelBlog.Database;
using TravelBlog.Database.Entities;
using TravelBlog.Extensions;
using TravelBlog.Models;
using TravelBlog.Services;
using TravelBlog.Services.LightJobManager;
using UAParser;

namespace TravelBlog.Controllers;

[Route("~/post/{id?}/{action=Index}")]
public class BlogPostController : Controller
{
private readonly IOptions<SiteOptions> options;
private readonly DatabaseContext database;
private readonly JobSchedulerService<MailJob, MailJobContext> scheduler;
private readonly EmailDeliveryService deliveryService;
private readonly MimeMessageCreationService mimeMessageCreation;
private readonly AuthenticationService authentication;
private readonly MarkdownService markdown;

public BlogPostController(IOptions<SiteOptions> options, DatabaseContext database,
JobSchedulerService<MailJob, MailJobContext> scheduler, AuthenticationService authentication, MarkdownService markdown)
public BlogPostController(DatabaseContext database, EmailDeliveryService deliveryService,
MimeMessageCreationService mimeMessageCreation, AuthenticationService authentication, MarkdownService markdown)
{
this.options = options;
this.database = database;
this.scheduler = scheduler;
this.deliveryService = deliveryService;
this.mimeMessageCreation = mimeMessageCreation;
this.authentication = authentication;
this.markdown = markdown;
}
Expand Down Expand Up @@ -138,19 +136,6 @@ public async Task<IActionResult> Draft(string title, string? content, bool liste
return Redirect("~/post/" + post.Id);
}

[HttpPost("~/post/create")]
[Authorize(Roles = Constants.AdminRole)]
public async Task<IActionResult> Create(string title, string? content, bool listed)
{
var post = new BlogPost(id: default, title, content ?? string.Empty, publishTime: DateTime.Now, modifyTime: default, listed);
database.BlogPosts.Add(post);
await database.SaveChangesAsync();

await NotifySubscribers(post);

return Redirect("~/post/" + post.Id);
}

[HttpGet]
[Authorize(Roles = Constants.AdminRole)]
public async Task<IActionResult> Edit(int id)
Expand Down Expand Up @@ -181,7 +166,7 @@ public async Task<IActionResult> Edit(int id, string title, string? content, boo

[HttpPost]
[Authorize(Roles = Constants.AdminRole)]
public async Task<IActionResult> Publish(int id, string title, string? content, bool listed)
public async Task<IActionResult> Publish(int id, string title, string? content, bool listed, string? preview)
{
BlogPost? post = await database.BlogPosts.SingleOrDefaultAsync(p => p.Id == id);
if (post is null)
Expand All @@ -194,25 +179,22 @@ public async Task<IActionResult> Publish(int id, string title, string? content,
post.Listed = listed;
await database.SaveChangesAsync();

await NotifySubscribers(post);
await NotifySubscribers(post, preview ?? string.Empty);

return Redirect("~/post/" + id);
}

private async Task NotifySubscribers(BlogPost post)
private async Task NotifySubscribers(BlogPost post, string preview)
{
List<Subscriber> subscribers = await database.Subscribers
.Where(s => s.ConfirmationTime != default && s.DeletionTime == default).ToListAsync();
.Where(s => s.MailAddress != null && s.ConfirmationTime != default && s.DeletionTime == default).ToListAsync();

await scheduler.Enqueue(subscribers.Select(s =>
await deliveryService.Enqueue(subscribers.Select(subscriber =>
{
string postUrl = Url.ContentLink($"~/post/{post.Id}/auth?token={s.Token}");
string unsubscribeUrl = Url.ContentLink("~/unsubscribe?token=" + s.Token);
string message = $"Hey {s.GivenName},\r\n" +
$"es wurde etwas neues auf {options.Value.BlogName} gepostet:\r\n" +
$"{postUrl}\r\n\r\n" +
$"Du kannst dich von diesem Blog jederzeit hier abmelden: {unsubscribeUrl}";
return new MailJob(id: default, s.Id, subject: "Neuer Post", message);
}));
MimeMessage mimeMessage =
mimeMessageCreation.CreatePostNotification(post, subscriber, preview, Url);
return (subscriber.MailAddress!, mimeMessage);
}), post.Id);
}
}
15 changes: 11 additions & 4 deletions src/TravelBlog/Database/DatabaseContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ public class DatabaseContext : DbContext
public DbSet<Subscriber> Subscribers => Set<Subscriber>();
public DbSet<BlogPost> BlogPosts => Set<BlogPost>();
public DbSet<PostRead> PostReads => Set<PostRead>();
public DbSet<MailJob> MailJobs => Set<MailJob>();
public DbSet<OutboxEmail> OutboxEmails => Set<OutboxEmail>();
public DbSet<SentEmail> SentEmails => Set<SentEmail>();

public DatabaseContext(IOptions<DatabaseOptions> options)
{
Expand Down Expand Up @@ -47,8 +48,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
postRead.HasOne(r => r.Post).WithMany(p => p!.Reads).HasForeignKey(r => r.PostId);
postRead.HasOne(r => r.Subscriber).WithMany(s => s!.Reads).HasForeignKey(r => r.SubscriberId);

var mailJob = modelBuilder.Entity<MailJob>();
mailJob.HasKey(t => t.Id);
mailJob.HasOne(t => t.Subscriber).WithMany().HasForeignKey(t => t.SubscriberId);
var outboxEmail = modelBuilder.Entity<OutboxEmail>();
outboxEmail.HasKey(e => e.Id);
outboxEmail.HasOne(e => e.BlogPost).WithMany().HasForeignKey(e => e.BlogPostId);

var sentEmail = modelBuilder.Entity<SentEmail>();
sentEmail.HasKey(e => e.Id);
sentEmail.HasOne(e => e.BlogPost).WithMany().HasForeignKey(e => e.BlogPostId);
sentEmail.HasIndex(e => e.DeliveryTime);
sentEmail.Property(e => e.Id).ValueGeneratedNever();
}
}
20 changes: 0 additions & 20 deletions src/TravelBlog/Database/Entities/MailJob.cs

This file was deleted.

12 changes: 12 additions & 0 deletions src/TravelBlog/Database/Entities/OutboxEmail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace TravelBlog.Database.Entities;

public class OutboxEmail
{
public int Id { get; set; }

public int? BlogPostId { get; set; }
public BlogPost? BlogPost { get; set; }

public required string EmailAddress { get; set; }
public required byte[] Content { get; set; }
}
16 changes: 16 additions & 0 deletions src/TravelBlog/Database/Entities/SentEmail.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;

namespace TravelBlog.Database.Entities;

public class SentEmail
{
public int Id { get; set; }

public int? BlogPostId { get; set; }
public BlogPost? BlogPost { get; set; }

public required string EmailAddress { get; set; }
public required int ContentSize { get; set; }
public string? ErrorMessage { get; set; }
public required DateTime DeliveryTime { get; set; }
}
27 changes: 27 additions & 0 deletions src/TravelBlog/Extensions/AsyncExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Nito.AsyncEx;
using System.Threading.Tasks;
using System.Threading;
using System;

namespace TravelBlog.Extensions;

public static class AsyncExtensions
{
// Inspired by https://github.com/StephenCleary/AsyncEx/issues/212#issuecomment-653765593
public static async Task<bool> WaitAsync(this AsyncAutoResetEvent mEvent, TimeSpan timeout, CancellationToken token = default)
{
using var timeOut = new CancellationTokenSource(timeout);
using var combined = CancellationTokenSource.CreateLinkedTokenSource(timeOut.Token, token);

try
{
await mEvent.WaitAsync(combined.Token).ConfigureAwait(false);
return true;
}
// Don't catch the OperationCanceledException from external Token
catch (OperationCanceledException) when (!token.IsCancellationRequested)
{
return false; //Here the OperationCanceledException was raised by Timeout
}
}
}
8 changes: 8 additions & 0 deletions src/TravelBlog/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.

using System.Diagnostics.CodeAnalysis;

[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "Primary constructors do not support readonly values in .NET 8.0")]
Loading

0 comments on commit 198c028

Please sign in to comment.