using Microsoft.Data.Sqlite; using MoShiro.Backend.Application; namespace MoShiro.Backend.Storage.Sqlite; public sealed class SqliteAuthStore : IAuthStore { private const string UserColumns = "id, username, password_hash, password_salt, iterations, created_at"; private const string TokenColumns = "id, name, scope, token_sha256, created_at, last_used_at, revoked_at"; private readonly SqliteDatabase _db; public SqliteAuthStore(SqliteDatabase db) { _db = db; } public ValueTask CountUsersAsync(CancellationToken ct) => _db.ReadAsync(c => (int)c.Scalar("SELECT COUNT(*) FROM users"), ct); public ValueTask GetUserByNameAsync(string username, CancellationToken ct) => _db.ReadAsync(c => c.QueryOne($"SELECT {UserColumns} FROM users WHERE username = @u", ReadUser, ("@u", username)), ct); public ValueTask GetUserAsync(string id, CancellationToken ct) => _db.ReadAsync(c => c.QueryOne($"SELECT {UserColumns} FROM users WHERE id = @id", ReadUser, ("@id", id)), ct); public ValueTask> ListUsersAsync(CancellationToken ct) => _db.ReadAsync>(c => c.Query($"SELECT {UserColumns} FROM users ORDER BY created_at, id", ReadUser), ct); public ValueTask CreateUserAsync(UserRecord user, CancellationToken ct) => _db.WriteAsync( c => c.Exec( "INSERT OR IGNORE INTO users (id, username, password_hash, password_salt, iterations, created_at) VALUES (@id, @u, @h, @s, @i, @at)", ("@id", user.Id), ("@u", user.Username), ("@h", user.PasswordHash), ("@s", user.PasswordSalt), ("@i", user.Iterations), ("@at", user.CreatedAtMs)) == 1, ct); public ValueTask UpdatePasswordAsync(string userId, byte[] hash, byte[] salt, int iterations, CancellationToken ct) => _db.ExecuteAsync( c => { c.Exec("UPDATE users SET password_hash = @h, password_salt = @s, iterations = @i WHERE id = @id", ("@id", userId), ("@h", hash), ("@s", salt), ("@i", iterations)); c.Exec("DELETE FROM sessions WHERE user_id = @id", ("@id", userId)); }, ct); public ValueTask CreateSessionAsync(SessionRecord session, CancellationToken ct) => _db.ExecuteAsync( c => c.Exec( "INSERT INTO sessions (id_sha256, user_id, csrf_token, created_at, expires_at) VALUES (@id, @u, @csrf, @c, @e)", ("@id", session.IdSha256), ("@u", session.UserId), ("@csrf", session.CsrfToken), ("@c", session.CreatedAtMs), ("@e", session.ExpiresAtMs)), ct); public ValueTask GetSessionAsync(byte[] idSha256, CancellationToken ct) => _db.ReadAsync( c => c.QueryOne("SELECT id_sha256, user_id, csrf_token, created_at, expires_at FROM sessions WHERE id_sha256 = @id", r => new SessionRecord(r.Bytes(0), r.GetString(1), r.Bytes(2), r.GetInt64(3), r.GetInt64(4)), ("@id", idSha256)), ct); public ValueTask DeleteSessionAsync(byte[] idSha256, CancellationToken ct) => _db.ExecuteAsync(c => c.Exec("DELETE FROM sessions WHERE id_sha256 = @id", ("@id", idSha256)), ct); public ValueTask DeleteExpiredSessionsAsync(long nowMs, CancellationToken ct) => _db.ExecuteAsync(c => c.Exec("DELETE FROM sessions WHERE expires_at <= @now", ("@now", nowMs)), ct); public ValueTask CreateApiTokenAsync(ApiTokenRecord token, CancellationToken ct) => _db.ExecuteAsync( c => c.Exec( "INSERT INTO api_tokens (id, name, scope, token_sha256, created_at) VALUES (@id, @n, @s, @h, @at)", ("@id", token.Id), ("@n", token.Name), ("@s", token.Scope), ("@h", token.TokenSha256), ("@at", token.CreatedAtMs)), ct); public ValueTask FindApiTokenAsync(byte[] tokenSha256, CancellationToken ct) => _db.ReadAsync(c => c.QueryOne($"SELECT {TokenColumns} FROM api_tokens WHERE token_sha256 = @h", ReadToken, ("@h", tokenSha256)), ct); public ValueTask> ListApiTokensAsync(CancellationToken ct) => _db.ReadAsync>(c => c.Query($"SELECT {TokenColumns} FROM api_tokens ORDER BY created_at DESC, id", ReadToken), ct); public ValueTask RevokeApiTokenAsync(string id, long nowMs, CancellationToken ct) => _db.WriteAsync(c => c.Exec("UPDATE api_tokens SET revoked_at = @now WHERE id = @id AND revoked_at IS NULL", ("@id", id), ("@now", nowMs)) == 1, ct); public ValueTask TouchApiTokenAsync(string id, long nowMs, CancellationToken ct) => _db.ExecuteAsync(c => c.Exec("UPDATE api_tokens SET last_used_at = @now WHERE id = @id", ("@id", id), ("@now", nowMs)), ct); private static UserRecord ReadUser(SqliteDataReader r) => new(r.GetString(0), r.GetString(1), r.Bytes(2), r.Bytes(3), r.GetInt32(4), r.GetInt64(5)); private static ApiTokenRecord ReadToken(SqliteDataReader r) => new(r.GetString(0), r.GetString(1), r.GetString(2), r.Bytes(3), r.GetInt64(4), r.NullableLong(5), r.NullableLong(6)); }