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:
@@ -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).")]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user