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
|
# Archief-edities: bestaande PDF's tonen én pas gedrukte kranten
|
||||||
# worden hier bewaard (resources/archive)
|
# worden hier bewaard (resources/archive)
|
||||||
- ./resources/archive:/app/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:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -53,3 +57,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql-data:
|
mysql-data:
|
||||||
|
app-dataprotection:
|
||||||
|
|||||||
@@ -77,6 +77,23 @@
|
|||||||
}
|
}
|
||||||
_artikel = await Artikelen.FindAsync(Id);
|
_artikel = await Artikelen.FindAsync(Id);
|
||||||
_gebruikers = await Gebruikers.GetAllAsync();
|
_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()
|
private async Task Verifieer()
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
@page "/indienen/bewerken"
|
@page "/indienen/bewerken"
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
|
|
||||||
<PageTitle>Artikel bewerken — ProostenKrant</PageTitle>
|
<PageTitle>Artikel bewerken — ProostenKrant</PageTitle>
|
||||||
|
|
||||||
@@ -35,10 +36,16 @@
|
|||||||
|
|
||||||
private string? _fout;
|
private string? _fout;
|
||||||
|
|
||||||
protected override Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Input ??= new();
|
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
|
private sealed class PaszinInput
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
@page "/indienen"
|
@page "/indienen"
|
||||||
@using System.ComponentModel.DataAnnotations
|
@using System.ComponentModel.DataAnnotations
|
||||||
|
@inject UserService Gebruikers
|
||||||
|
@inject AuthenticationStateProvider AuthStateProvider
|
||||||
|
|
||||||
<PageTitle>Artikel indienen — ProostenKrant</PageTitle>
|
<PageTitle>Artikel indienen — ProostenKrant</PageTitle>
|
||||||
|
|
||||||
@@ -38,10 +40,23 @@
|
|||||||
|
|
||||||
private string? _fout;
|
private string? _fout;
|
||||||
|
|
||||||
protected override Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Input ??= new();
|
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
|
private sealed class PaszinInput
|
||||||
|
|||||||
@@ -191,8 +191,7 @@
|
|||||||
await Artikelen.UpdateAsync(_bewerkArtikel);
|
await Artikelen.UpdateAsync(_bewerkArtikel);
|
||||||
|
|
||||||
var artikelId = _bewerkArtikel.Id;
|
var artikelId = _bewerkArtikel.Id;
|
||||||
await HttpContext.SignOutAsync(KrantAuth.Scheme);
|
await RondAf(artikelId);
|
||||||
Nav.NavigateTo($"/indienen/bevestiging?artikel={artikelId}", forceLoad: true);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -208,11 +207,33 @@
|
|||||||
Status = "draft",
|
Status = "draft",
|
||||||
};
|
};
|
||||||
var nieuwId = await Artikelen.CreateAsync(artikel);
|
var nieuwId = await Artikelen.CreateAsync(artikel);
|
||||||
await HttpContext.SignOutAsync(KrantAuth.Scheme);
|
await RondAf(nieuwId);
|
||||||
Nav.NavigateTo($"/indienen/bevestiging?artikel={nieuwId}", forceLoad: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <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
|
private sealed class IndienInput
|
||||||
{
|
{
|
||||||
[Required(ErrorMessage = "Geef het artikel een titel (minstens 5 tekens).")]
|
[Required(ErrorMessage = "Geef het artikel een titel (minstens 5 tekens).")]
|
||||||
|
|||||||
@@ -38,6 +38,22 @@ builder.Services.AddScoped<ProostenKrant.Services.KrantInfo>();
|
|||||||
|
|
||||||
var app = builder.Build();
|
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())
|
if (!app.Environment.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/fout", createScopeForErrors: true);
|
app.UseExceptionHandler("/fout", createScopeForErrors: true);
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ public static class KrantAuth
|
|||||||
public const string Scheme = "Krant";
|
public const string Scheme = "Krant";
|
||||||
public const int PaszinValidityMinutes = 30;
|
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);
|
var identity = new ClaimsIdentity(Scheme);
|
||||||
identity.AddClaim(new Claim("uid", user.Id.ToString()));
|
identity.AddClaim(new Claim("uid", user.Id.ToString()));
|
||||||
identity.AddClaim(new Claim("naam", user.DisplayName));
|
identity.AddClaim(new Claim("naam", user.DisplayName));
|
||||||
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
|
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
|
||||||
|
if (editArticleId is int id)
|
||||||
|
{
|
||||||
|
identity.AddClaim(new Claim("bewerk", id.ToString()));
|
||||||
|
}
|
||||||
return new ClaimsPrincipal(identity);
|
return new ClaimsPrincipal(identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,5 +50,11 @@ public static class KrantAuth
|
|||||||
return int.TryParse(claim, out var id) ? id : null;
|
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;
|
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