diff --git a/LehrerApp.Api.Tests/UserStoreTests.cs b/LehrerApp.Api.Tests/UserStoreTests.cs index 89e4da7..073d015 100644 --- a/LehrerApp.Api.Tests/UserStoreTests.cs +++ b/LehrerApp.Api.Tests/UserStoreTests.cs @@ -43,6 +43,27 @@ public sealed class UserStoreTests Assert.False(temp.Store.VerifyPassword("unbekannt", "irgendwas")); } + [Fact] + public void SetPassword_VorhandenerNutzer_AktualisiertPasswortUndAltesFunktioniertNichtMehr() + { + using var temp = new TempUserStore(); + temp.Store.CreateUser("sebastian", "altesPasswort123"); + + var updated = temp.Store.SetPassword("sebastian", "neuesPasswort123"); + + Assert.True(updated); + Assert.True(temp.Store.VerifyPassword("sebastian", "neuesPasswort123")); + Assert.False(temp.Store.VerifyPassword("sebastian", "altesPasswort123")); + } + + [Fact] + public void SetPassword_UnbekannterNutzer_GibtFalseZurueck() + { + using var temp = new TempUserStore(); + + Assert.False(temp.Store.SetPassword("unbekannt", "irgendeinPasswort")); + } + private sealed class TempUserStore : IDisposable { private readonly string _directory = Path.Combine( diff --git a/LehrerApp.Api/Cli.cs b/LehrerApp.Api/Cli.cs index 1d3898c..0c15fcc 100644 --- a/LehrerApp.Api/Cli.cs +++ b/LehrerApp.Api/Cli.cs @@ -11,19 +11,9 @@ internal static class Cli Console.Error.WriteLine("Verwendung: create-user [--password ]"); return Task.FromResult(1); } - var username = args[1]; - string? password = null; - for (var i = 2; i < args.Length - 1; i++) - if (args[i] == "--password") password = args[i + 1]; - - password ??= ReadPassword("Passwort (mind. 12 Zeichen): "); - - if (string.IsNullOrWhiteSpace(password) || password.Length < 12) - { - Console.Error.WriteLine("Passwort muss mindestens 12 Zeichen lang sein."); - return Task.FromResult(1); - } + var password = ParsePassword(args); + if (password is null) return Task.FromResult(1); Directory.CreateDirectory(dataPath); using var store = new UserStore(dataPath); @@ -37,6 +27,45 @@ internal static class Cli return Task.FromResult(0); } + public static Task RunSetPasswordAsync(string dataPath, string[] args) + { + if (args.Length < 2) + { + Console.Error.WriteLine("Verwendung: set-password [--password ]"); + return Task.FromResult(1); + } + var username = args[1]; + var password = ParsePassword(args); + if (password is null) return Task.FromResult(1); + + Directory.CreateDirectory(dataPath); + using var store = new UserStore(dataPath); + if (!store.SetPassword(username, password)) + { + Console.Error.WriteLine($"Nutzer '{username}' existiert nicht."); + return Task.FromResult(1); + } + + Console.WriteLine($"Passwort für '{username}' aktualisiert."); + return Task.FromResult(0); + } + + private static string? ParsePassword(string[] args) + { + string? password = null; + for (var i = 2; i < args.Length - 1; i++) + if (args[i] == "--password") password = args[i + 1]; + + password ??= ReadPassword("Passwort (mind. 12 Zeichen): "); + + if (string.IsNullOrWhiteSpace(password) || password.Length < 12) + { + Console.Error.WriteLine("Passwort muss mindestens 12 Zeichen lang sein."); + return null; + } + return password; + } + // docker exec ohne -it liefert kein TTY -> ReadKey wäre nicht möglich, dann normal lesen. private static string ReadPassword(string prompt) { diff --git a/LehrerApp.Api/Program.cs b/LehrerApp.Api/Program.cs index a585c32..09494e7 100644 --- a/LehrerApp.Api/Program.cs +++ b/LehrerApp.Api/Program.cs @@ -20,6 +20,8 @@ var data = builder.Configuration["Api:DataPath"] ?? "./data"; if (args.Length > 0 && args[0] == "create-user") return await Cli.RunCreateUserAsync(data, args); +if (args.Length > 0 && args[0] == "set-password") + return await Cli.RunSetPasswordAsync(data, args); var secret = builder.Configuration["JWT_SECRET"] ?? throw new InvalidOperationException("JWT_SECRET nicht konfiguriert."); diff --git a/LehrerApp.Api/UserStore.cs b/LehrerApp.Api/UserStore.cs index 1c99120..fa45920 100644 --- a/LehrerApp.Api/UserStore.cs +++ b/LehrerApp.Api/UserStore.cs @@ -23,6 +23,15 @@ public class UserStore(string dataPath) : IDisposable return true; } + public bool SetPassword(string username, string password) + { + var user = Col.FindOne(x => x.Username == username); + if (user is null) return false; + user.PasswordHash = PasswordHasher.Hash(password); + Col.Update(user); + return true; + } + public bool VerifyPassword(string username, string password) { var user = Col.FindOne(x => x.Username == username); diff --git a/docker/README.md b/docker/README.md index 0a97e41..40064bb 100644 --- a/docker/README.md +++ b/docker/README.md @@ -29,6 +29,16 @@ docker compose exec api dotnet LehrerApp.Api.dll create-user --pa Der Befehl beendet sich danach sofort wieder, ohne den API-Dienst zu starten – für den eigentlichen Serverbetrieb läuft `docker compose up` unverändert weiter. +## Passwort zurücksetzen + +Analog zu `create-user`, aber für einen bereits existierenden Nutzer (schlägt fehl, wenn der +Nutzer nicht existiert – zum Anlegen `create-user` verwenden): + +```bash +docker compose exec api dotnet LehrerApp.Api.dll set-password +docker compose exec api dotnet LehrerApp.Api.dll set-password --password "" +``` + ## Backup `backup.sh` sichert `./data` (Ereignis-Logs, Snapshots, Anhänge, Nutzer) als komprimiertes