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)
This commit is contained in:
douwe
2026-10-09 14:21:51 +02:00
parent a2a9a773f4
commit fa81b64558
8 changed files with 144 additions and 9 deletions
+5
View File
@@ -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:
@@ -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()
@@ -1,5 +1,6 @@
@page "/indienen/bewerken"
@using System.ComponentModel.DataAnnotations
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>Artikel bewerken — ProostenKrant</PageTitle>
@@ -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
@@ -1,5 +1,7 @@
@page "/indienen"
@using System.ComponentModel.DataAnnotations
@inject UserService Gebruikers
@inject AuthenticationStateProvider AuthStateProvider
<PageTitle>Artikel indienen — ProostenKrant</PageTitle>
@@ -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
@@ -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);
}
}
/// <summary>
/// Sessie afronden na het opslaan. Een ingelogd redactielid blijft
/// ingelogd (alleen de bewerk-grant wordt gewist); een paszin-sessie
/// wordt zoals voorheen afgesloten.
/// </summary>
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).")]
+16
View File
@@ -38,6 +38,22 @@ builder.Services.AddScoped<ProostenKrant.Services.KrantInfo>();
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<ProostenKrant.Services.Db>());
}
}
catch (Exception ex)
{
Console.WriteLine($"Seed overslaan: {ex.Message}");
}
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/fout", createScopeForErrors: true);
+11 -1
View File
@@ -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;
}
+44
View File
@@ -0,0 +1,44 @@
using Dapper;
namespace ProostenKrant.Services;
/// <summary>
/// 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.
/// </summary>
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 }));
}
}