feat: User-Management mit Token-basierter Authentifizierung
Backend: - UserRecord Model + Users-Tabelle in DB - AuthService: SHA256-Passwort-Hashing, Token-Verwaltung (ConcurrentDictionary) - AuthMiddleware: Schützt alle /api/-Routes (außer /api/auth/login) - AuthEndpoint: Login, Logout, Me - Default-User admin/admin wird automatisch erstellt Frontend: - Login-Page mit Formular (Enter-Key-Support) - Globaler Fetch-Interceptor: 401 → automatisch Login-Redirect - Auth-Check beim App-Start und bei Hash-Änderungen - Logout-Link im Dashboard-Header - Login-CSS: Zentrierte Login-Box
This commit is contained in:
1 parent
a1c54bf376
commit
9a59ae686d
13 files changed
+446
-3
No files matched your search
@@ -0,0 +1,97 @@
|
||||
using System.Text.Json;
|
||||
using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
public static class AuthEndpoint
|
||||
{
|
||||
public static void MapAuthEndpoints(this WebApplication app)
|
||||
{
|
||||
app.MapPost("/api/auth/login", HandleLogin);
|
||||
app.MapPost("/api/auth/logout", HandleLogout);
|
||||
app.MapGet("/api/auth/me", HandleMe);
|
||||
}
|
||||
|
||||
private static async System.Threading.Tasks.Task HandleLogin(HttpContext ctx, DatabaseService db, AuthService auth)
|
||||
{
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
var username = body.TryGetProperty("username", out var u) ? u.GetString() : null;
|
||||
var password = body.TryGetProperty("password", out var p) ? p.GetString() : null;
|
||||
|
||||
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(password))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Username and password required" });
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await db.GetUserByUsername(username);
|
||||
if (user == null || !auth.VerifyPassword(password, user.PasswordHash))
|
||||
{
|
||||
ctx.Response.StatusCode = 401;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Invalid credentials" });
|
||||
return;
|
||||
}
|
||||
|
||||
var token = auth.CreateToken(user.UserId);
|
||||
|
||||
ctx.Response.Cookies.Append("gott_token", token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Path = "/",
|
||||
MaxAge = System.TimeSpan.FromDays(365)
|
||||
});
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
userId = user.UserId,
|
||||
username = user.Username,
|
||||
displayName = user.DisplayName
|
||||
});
|
||||
}
|
||||
|
||||
private static async System.Threading.Tasks.Task HandleLogout(HttpContext ctx, AuthService auth)
|
||||
{
|
||||
var token = ctx.Request.Cookies["gott_token"];
|
||||
auth.RevokeToken(token);
|
||||
|
||||
ctx.Response.Cookies.Delete("gott_token", new CookieOptions
|
||||
{
|
||||
Path = "/"
|
||||
});
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new { ok = true });
|
||||
}
|
||||
|
||||
private static async System.Threading.Tasks.Task HandleMe(HttpContext ctx, DatabaseService db, AuthService auth)
|
||||
{
|
||||
var token = ctx.Request.Cookies["gott_token"];
|
||||
var userId = auth.ValidateToken(token);
|
||||
|
||||
if (userId == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 401;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Not authenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
var user = await db.GetUser(userId);
|
||||
if (user == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 401;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "User not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
userId = user.UserId,
|
||||
username = user.Username,
|
||||
displayName = user.DisplayName
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user