/api/auth/login mintete die JWT-userId bisher aus dem roh eingegebenen Benutzernamen. LiteDBs Standard-Collation vergleicht den eindeutigen Index auf Username aber case-insensitive - ein Login mit nur einmal abweichender Schreibweise auf einem zweiten Gerät authentifiziert erfolgreich, mintet aber eine andere userId. Da EventStore.GetCol(userId) diese direkt als Dateiname für den Server-seitigen Event-Speicher nutzt, entstanden zwei komplett getrennte Datenbestände für ein und dasselbe, aus Nutzersicht einzige Konto - Ursache dafür, dass ein zweites Gerät trotz "since=0" durchgängig 0 Ereignisse erhielt. UserStore.VerifyPassword(bool) durch Authenticate(UserEntry?) ersetzt, das bei Erfolg den kanonisch gespeicherten Nutzereintrag liefert; /api/auth/login mintet Token und userId daraus statt aus der Roheingabe. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
62 lines
2.4 KiB
C#
62 lines
2.4 KiB
C#
using LiteDB;
|
|
|
|
namespace LehrerApp.Api;
|
|
|
|
public class UserStore(string dataPath) : IDisposable
|
|
{
|
|
private readonly LiteDatabase _db = new(Path.Combine(dataPath, "users.db"));
|
|
|
|
private ILiteCollection<UserEntry> Col
|
|
{
|
|
get
|
|
{
|
|
var col = _db.GetCollection<UserEntry>("users");
|
|
col.EnsureIndex(x => x.Username, unique: true);
|
|
return col;
|
|
}
|
|
}
|
|
|
|
public bool CreateUser(string username, string password)
|
|
{
|
|
if (Col.Exists(x => x.Username == username)) return false;
|
|
Col.Insert(new UserEntry { Username = username, PasswordHash = PasswordHasher.Hash(password) });
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Prüft die Anmeldedaten und liefert bei Erfolg den GESPEICHERTEN Nutzereintrag zurück -
|
|
/// nicht bloß true/false. Wichtig: LiteDBs Standard-Collation vergleicht Strings (und damit
|
|
/// auch den eindeutigen Index auf Username) standardmäßig case-insensitive, d.h. ein Login mit
|
|
/// abweichender Groß-/Kleinschreibung authentifiziert erfolgreich. Würde der Aufrufer daraufhin
|
|
/// den roh eingegebenen Benutzernamen als userId für den Server-seitigen Event-Speicher
|
|
/// verwenden (siehe EventStore.GetCol), würde ein einziger andersartig getippter Login auf
|
|
/// einem zweiten Gerät einen komplett getrennten, nicht überlappenden Datenbestand erzeugen,
|
|
/// obwohl es sich aus Nutzersicht um dasselbe Konto handelt. Der kanonisch gespeicherte Name
|
|
/// aus diesem Rückgabewert stellt sicher, dass die userId unabhängig von der beim Login
|
|
/// eingegebenen Schreibweise stets identisch ist.
|
|
/// </summary>
|
|
public UserEntry? Authenticate(string username, string password)
|
|
{
|
|
var user = Col.FindOne(x => x.Username == username);
|
|
return user is not null && PasswordHasher.Verify(password, user.PasswordHash) ? user : null;
|
|
}
|
|
|
|
public void Dispose() => _db.Dispose();
|
|
}
|
|
|
|
public class UserEntry
|
|
{
|
|
public ObjectId Id { get; set; } = ObjectId.NewObjectId();
|
|
public string Username { get; set; } = "";
|
|
public string PasswordHash { get; set; } = "";
|
|
}
|