Ein Konto liess sich anlegen und danach nie wieder ändern: kein Weg zum Administrator, keiner zurück, keiner zum Löschen. Mit Einladungslinks füllte sich der Server also in genau eine Richtung. **Warum es einen Besitzer braucht.** Sobald Administratoren einander das Recht entziehen können, gibt es einen Zustand ohne jeden Administrator — und aus dem führt kein Weg zurück ausser dem an der Datenbank. Ein Konto, das ausgenommen ist, schliesst ihn aus. Es ist das erste, also das, mit dem eingerichtet wurde; die Migration trägt das auf bestehenden Servern nach, und weil die Kennungen zeitlich geordnet vergeben werden, ist der Älteste schlicht der mit der kleinsten. Weitergeben lässt er sich: vom Besitzer, an einen Administrator, mit Rückfrage. Nur an einen Administrator, sonst wäre der Server einen Klick von einem Besitzer entfernt, der die Verwaltung gar nicht sieht. **Drei Türen sind zugemauert**, und jede hat ihren Test: dem Besitzer das Recht nehmen, den Besitzer löschen, sich selbst das eigene Recht nehmen. Das letzte ist die unauffälligste — wer sich im Eifer entmachtet, steht vor einer Verwaltung, die er nicht mehr öffnen kann, und das Zurücknehmen braucht genau die. Gemessen und als Test festgehalten: ein frisch vergebenes Adminrecht gilt **sofort**, ohne neue Anmeldung. Die Rolle wird je Anfrage aus der Datenbank gelesen und nicht aus dem Token — ich hatte das Gegenteil befürchtet. Löschen nimmt mit, was am Konto hing: Geräte, Sehstände, Sehprotokoll, Meldungen, offene Einladungen. Alles hängt mit `Cascade` daran. Zahlen über jemanden zu führen, den es nicht mehr gibt, wäre die schlechtere Hälfte. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
67 lines
2.7 KiB
C#
67 lines
2.7 KiB
C#
namespace Flaro.Core.Users;
|
|
|
|
/// <summary>Ein Benutzerkonto.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// Mehrbenutzer von Anfang an — auch wenn der erste Server nur einen hat.
|
|
/// Fortschritt und Gesehen-Status je Benutzer nachträglich einzuziehen wäre
|
|
/// ein Umbau am Datenmodell und an jeder Abfrage darauf, kein Zusatz.
|
|
/// </remarks>
|
|
public sealed class User
|
|
{
|
|
/// <summary>Zeitlich geordnet, damit der Index nicht zerfasert.</summary>
|
|
public Guid Id { get; init; } = Guid.CreateVersion7();
|
|
|
|
public required string UserName { get; set; }
|
|
|
|
/// <summary>Im PHC-Format, siehe <see cref="PasswordHasher"/>.</summary>
|
|
public required string PasswordHash { get; set; }
|
|
|
|
public bool IsAdministrator { get; set; }
|
|
|
|
/// <summary>Dem Besitzer kann niemand das Adminrecht nehmen.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Damit sich ein Server nicht selbst aussperren kann.</b> Sobald
|
|
/// Administratoren einander das Recht entziehen können, gibt es einen
|
|
/// Zustand ohne jeden Administrator — und aus dem führt kein Weg zurück
|
|
/// ausser dem an der Datenbank. Ein Konto, das ausgenommen ist, schliesst
|
|
/// diesen Zustand aus.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Es ist das erste Konto</b>, also das, mit dem der Server
|
|
/// eingerichtet wurde. Übertragen lässt es sich — an einen anderen
|
|
/// Administrator, und nur vom Besitzer selbst.
|
|
/// </para>
|
|
/// </remarks>
|
|
public bool IsOwner { get; set; }
|
|
|
|
/// <summary>Die bevorzugte Sprache — <c>null</c> heisst: die des Servers.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Und sie ist nicht kostenlos.</b> Metadaten werden in der Sprache des
|
|
/// Servers geholt und gespeichert. Wer eine andere wählt, bekommt, was in
|
|
/// der Übersetzungstabelle liegt — und für den Rest die Serversprache.
|
|
/// Darauf wird beim Umstellen deutlich hingewiesen; es stillschweigend
|
|
/// halb zu übersetzen wäre schlechter als gar nicht.
|
|
/// </remarks>
|
|
public string? Language { get; set; }
|
|
|
|
/// <summary>Das eigene Bild — der Schlüssel aus derselben Ablage wie Plakate.</summary>
|
|
///
|
|
/// <remarks>
|
|
/// <b>Kein eigener Speicher für Profilbilder.</b> Es ist ein Bild wie
|
|
/// jedes andere: derselbe Ordner, dieselbe Adresse, derselbe
|
|
/// Zwischenspeicher im Browser. Ein zweiter Weg hiesse ein zweites Mal
|
|
/// Pfadprüfung, Grössengrenze und Aufräumen — dreimal dieselbe Arbeit für
|
|
/// ein Bild von 200 Pixeln.
|
|
///
|
|
/// <c>null</c> heisst: der Buchstabenkreis. Der ist keine Notlösung,
|
|
/// sondern eine gültige Antwort — niemand muss ein Bild haben.
|
|
/// </remarks>
|
|
public string? AvatarImage { get; set; }
|
|
|
|
public DateTimeOffset CreatedAt { get; init; } = DateTimeOffset.UtcNow;
|
|
}
|