From fa81b64558a2d104203f11f272616f62041f51c9 Mon Sep 17 00:00:00 2001 From: douwe Date: Fri, 9 Oct 2026 14:21:51 +0200 Subject: [PATCH] Persist login sessions, seed default users, and let editors skip the paszin - compose: keep DataProtection keys in a named volume so redeploying no longer logs everybody out - seed default family members at startup (init.sql only runs on a fresh MySQL volume, existing databases never got new members like Mattis) - editors no longer need the paszin to submit an article or edit their own article; the editor session now survives submitting/editing (previously the paszin flow replaced or signed out the editor cookie) --- compose.yml | 5 +++ .../Components/Pages/BewerkenArtikel.razor | 17 +++++++ .../Components/Pages/BewerkenPaszin.razor | 11 ++++- .../Components/Pages/Indienen.razor | 19 +++++++- .../Components/Pages/IndienenFormulier.razor | 29 ++++++++++-- src/ProostenKrant/Program.cs | 16 +++++++ src/ProostenKrant/Services/KrantAuth.cs | 12 ++++- src/ProostenKrant/Services/Seed.cs | 44 +++++++++++++++++++ 8 files changed, 144 insertions(+), 9 deletions(-) create mode 100644 src/ProostenKrant/Services/Seed.cs diff --git a/compose.yml b/compose.yml index 1b94f35..0b86ad8 100644 --- a/compose.yml +++ b/compose.yml @@ -26,6 +26,10 @@ services: # Archief-edities: bestaande PDF's tonen én pas gedrukte kranten # worden hier bewaard (resources/archive) - ./resources/archive:/app/archive + # DataProtection-sleutels (versleuteling van login-cookies en + # antiforgery-tokens). Zonder dit volume worden alle sessies + # ongeldig bij elke herdeploy van de app-container. + - app-dataprotection:/root/.aspnet/DataProtection-Keys depends_on: mysql: condition: service_healthy @@ -53,3 +57,4 @@ services: volumes: mysql-data: + app-dataprotection: diff --git a/src/ProostenKrant/Components/Pages/BewerkenArtikel.razor b/src/ProostenKrant/Components/Pages/BewerkenArtikel.razor index 772c0cf..196091f 100644 --- a/src/ProostenKrant/Components/Pages/BewerkenArtikel.razor +++ b/src/ProostenKrant/Components/Pages/BewerkenArtikel.razor @@ -77,6 +77,23 @@ } _artikel = await Artikelen.FindAsync(Id); _gebruikers = await Gebruikers.GetAllAsync(); + + // Een ingelogd redactielid dat zelf de schrijver is, hoeft zich niet + // te verifiëren: direct door naar het formulier, met behoud van de + // redactiesessie. Artikelen van een andere schrijver volgen de + // gewone controle hieronder. + if (KrantAuth.IsEditor(state.User) && KrantAuth.Uid(state.User) is int uid && + _artikel is not null && uid == _artikel.AuthorId) + { + var gebruiker = await Gebruikers.FindAsync(uid); + if (gebruiker is not null) + { + await HttpContext.SignInAsync(KrantAuth.Scheme, + KrantAuth.EditorPrincipal(gebruiker, editArticleId: _artikel.Id)); + Nav.NavigateTo("/indienen/formulier", forceLoad: true); + return; + } + } } private async Task Verifieer() diff --git a/src/ProostenKrant/Components/Pages/BewerkenPaszin.razor b/src/ProostenKrant/Components/Pages/BewerkenPaszin.razor index d08bdbf..d08817a 100644 --- a/src/ProostenKrant/Components/Pages/BewerkenPaszin.razor +++ b/src/ProostenKrant/Components/Pages/BewerkenPaszin.razor @@ -1,5 +1,6 @@ @page "/indienen/bewerken" @using System.ComponentModel.DataAnnotations +@inject AuthenticationStateProvider AuthStateProvider Artikel bewerken — ProostenKrant @@ -35,10 +36,16 @@ private string? _fout; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { Input ??= new(); - return Task.CompletedTask; + + // Ingelogde redactie hoeft geen paszin in te voeren. + var state = await AuthStateProvider.GetAuthenticationStateAsync(); + if (KrantAuth.IsEditor(state.User)) + { + Nav.NavigateTo("/indienen/bewerken/lijst", forceLoad: true); + } } private sealed class PaszinInput diff --git a/src/ProostenKrant/Components/Pages/Indienen.razor b/src/ProostenKrant/Components/Pages/Indienen.razor index 9011d2a..f32936c 100644 --- a/src/ProostenKrant/Components/Pages/Indienen.razor +++ b/src/ProostenKrant/Components/Pages/Indienen.razor @@ -1,5 +1,7 @@ @page "/indienen" @using System.ComponentModel.DataAnnotations +@inject UserService Gebruikers +@inject AuthenticationStateProvider AuthStateProvider Artikel indienen — ProostenKrant @@ -38,10 +40,23 @@ private string? _fout; - protected override Task OnInitializedAsync() + protected override async Task OnInitializedAsync() { Input ??= new(); - return Task.CompletedTask; + + // Een ingelogd redactielid heeft de paszin niet nodig: direct naar + // het formulier. Daarbij een eventueel achtergebleven bewerk-grant + // wissen zodat het formulier met een leeg artikel begint. + var state = await AuthStateProvider.GetAuthenticationStateAsync(); + if (KrantAuth.IsEditor(state.User) && KrantAuth.Uid(state.User) is int uid) + { + var gebruiker = await Gebruikers.FindAsync(uid); + if (gebruiker is not null) + { + await HttpContext.SignInAsync(KrantAuth.Scheme, KrantAuth.EditorPrincipal(gebruiker)); + } + Nav.NavigateTo("/indienen/formulier", forceLoad: true); + } } private sealed class PaszinInput diff --git a/src/ProostenKrant/Components/Pages/IndienenFormulier.razor b/src/ProostenKrant/Components/Pages/IndienenFormulier.razor index 650c292..f0c66ef 100644 --- a/src/ProostenKrant/Components/Pages/IndienenFormulier.razor +++ b/src/ProostenKrant/Components/Pages/IndienenFormulier.razor @@ -191,8 +191,7 @@ await Artikelen.UpdateAsync(_bewerkArtikel); var artikelId = _bewerkArtikel.Id; - await HttpContext.SignOutAsync(KrantAuth.Scheme); - Nav.NavigateTo($"/indienen/bevestiging?artikel={artikelId}", forceLoad: true); + await RondAf(artikelId); } else { @@ -208,11 +207,33 @@ Status = "draft", }; var nieuwId = await Artikelen.CreateAsync(artikel); - await HttpContext.SignOutAsync(KrantAuth.Scheme); - Nav.NavigateTo($"/indienen/bevestiging?artikel={nieuwId}", forceLoad: true); + await RondAf(nieuwId); } } + /// + /// Sessie afronden na het opslaan. Een ingelogd redactielid blijft + /// ingelogd (alleen de bewerk-grant wordt gewist); een paszin-sessie + /// wordt zoals voorheen afgesloten. + /// + private async Task RondAf(int artikelId) + { + var state = await AuthStateProvider.GetAuthenticationStateAsync(); + if (KrantAuth.Uid(state.User) is int uid) + { + var gebruiker = await Gebruikers.FindAsync(uid); + if (gebruiker is not null) + { + await HttpContext.SignInAsync(KrantAuth.Scheme, KrantAuth.EditorPrincipal(gebruiker)); + } + } + else + { + await HttpContext.SignOutAsync(KrantAuth.Scheme); + } + Nav.NavigateTo($"/indienen/bevestiging?artikel={artikelId}", forceLoad: true); + } + private sealed class IndienInput { [Required(ErrorMessage = "Geef het artikel een titel (minstens 5 tekens).")] diff --git a/src/ProostenKrant/Program.cs b/src/ProostenKrant/Program.cs index 8446848..030ef10 100644 --- a/src/ProostenKrant/Program.cs +++ b/src/ProostenKrant/Program.cs @@ -38,6 +38,22 @@ builder.Services.AddScoped(); var app = builder.Build(); +// Ontbrekende gezinsleden aanvullen op databases die niet vers met +// deploy/mysql/init.sql zijn aangemaakt. Faalt de database nog niet op, +// dan start de app gewoon door. +try +{ + using (var scope = app.Services.CreateScope()) + { + await ProostenKrant.Services.Seed.EnsureDefaultsAsync( + scope.ServiceProvider.GetRequiredService()); + } +} +catch (Exception ex) +{ + Console.WriteLine($"Seed overslaan: {ex.Message}"); +} + if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/fout", createScopeForErrors: true); diff --git a/src/ProostenKrant/Services/KrantAuth.cs b/src/ProostenKrant/Services/KrantAuth.cs index e421019..45aff68 100644 --- a/src/ProostenKrant/Services/KrantAuth.cs +++ b/src/ProostenKrant/Services/KrantAuth.cs @@ -8,12 +8,16 @@ public static class KrantAuth public const string Scheme = "Krant"; public const int PaszinValidityMinutes = 30; - public static ClaimsPrincipal EditorPrincipal(User user) + public static ClaimsPrincipal EditorPrincipal(User user, int? editArticleId = null) { var identity = new ClaimsIdentity(Scheme); identity.AddClaim(new Claim("uid", user.Id.ToString())); identity.AddClaim(new Claim("naam", user.DisplayName)); identity.AddClaim(new Claim(ClaimTypes.Name, user.Username)); + if (editArticleId is int id) + { + identity.AddClaim(new Claim("bewerk", id.ToString())); + } return new ClaimsPrincipal(identity); } @@ -46,5 +50,11 @@ public static class KrantAuth return int.TryParse(claim, out var id) ? id : null; } + public static int? Uid(ClaimsPrincipal user) + { + var claim = user.FindFirst("uid")?.Value; + return int.TryParse(claim, out var id) ? id : null; + } + public static string? EditorName(ClaimsPrincipal user) => user.FindFirst("naam")?.Value; } diff --git a/src/ProostenKrant/Services/Seed.cs b/src/ProostenKrant/Services/Seed.cs new file mode 100644 index 0000000..18ec9ec --- /dev/null +++ b/src/ProostenKrant/Services/Seed.cs @@ -0,0 +1,44 @@ +using Dapper; + +namespace ProostenKrant.Services; + +/// +/// Zorgt dat de vaste gezinsleden bestaan, ook op een database die niet +/// vers met deploy/mysql/init.sql is aangemaakt (init.sql draait alleen op +/// een lege MySQL-volume). Ontbrekende namen worden toegevoegd; bestaande +/// rijen blijven ongemoeerd. +/// +public static class Seed +{ + // Zelfde lijst als deploy/mysql/init.sql; houd beide synchroon. + private static readonly (string Username, string DisplayName)[] Gezinsleden = + { + ("douwe", "Douwe"), + ("pauline", "Pauline"), + ("diede", "Diede"), + ("dien", "Dien"), + ("chris", "Chris"), + ("geert", "Geert"), + ("annick", "Annick"), + ("cedric", "Cedric"), + ("gunter", "Gunter"), + ("daan", "Daan"), + ("kyala", "Kyala"), + ("michel", "Michel"), + ("dries", "Dries"), + ("pieter", "Pieter"), + ("mathilde", "Mathilde"), + ("kevin", "Kevin"), + ("luc", "Luc"), + ("tilde", "Tilde"), + ("mattis", "Mattis"), + }; + + public static async Task EnsureDefaultsAsync(Db db) + { + await using var con = await db.OpenAsync(); + await con.ExecuteAsync( + "INSERT IGNORE INTO users (username, display_name) VALUES (@Username, @DisplayName)", + Gezinsleden.Select(l => new { l.Username, l.DisplayName })); + } +}