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
@@ -155,6 +155,18 @@ public class DatabaseService
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_skills_agent ON agent_skills(agent_id);
|
||||
");
|
||||
|
||||
conn.Execute(@"
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id TEXT NOT NULL UNIQUE,
|
||||
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
password_hash TEXT NOT NULL,
|
||||
display_name TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
");
|
||||
}
|
||||
|
||||
private void TryAddColumn(SqliteConnection conn, string table, string column, string type)
|
||||
@@ -638,4 +650,58 @@ public class DatabaseService
|
||||
"DELETE FROM agent_skills WHERE agent_id = @agentId AND skill_slug = @skillSlug",
|
||||
new { agentId, skillSlug });
|
||||
}
|
||||
|
||||
// ── Users ──
|
||||
|
||||
public void EnsureDefaultUser(AuthService auth)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
var count = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM users");
|
||||
if (count == 0)
|
||||
{
|
||||
var now = DateTime.UtcNow.ToString("o");
|
||||
conn.Execute(@"
|
||||
INSERT INTO users (user_id, username, password_hash, display_name, created_at, updated_at)
|
||||
VALUES (@userId, @username, @hash, @displayName, @now, @now)",
|
||||
new
|
||||
{
|
||||
userId = Guid.NewGuid().ToString("N"),
|
||||
username = "admin",
|
||||
hash = auth.HashPassword("admin"),
|
||||
displayName = "Admin",
|
||||
now
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<UserRecord> GetUserByUsername(string username)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QuerySingleOrDefaultAsync<UserRecord>(
|
||||
"SELECT * FROM users WHERE username = @username COLLATE NOCASE",
|
||||
new { username });
|
||||
}
|
||||
|
||||
public async Task<UserRecord> GetUser(string userId)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QuerySingleOrDefaultAsync<UserRecord>(
|
||||
"SELECT * FROM users WHERE user_id = @userId",
|
||||
new { userId });
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<UserRecord>> GetUsers()
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
return await conn.QueryAsync<UserRecord>(
|
||||
"SELECT * FROM users ORDER BY username");
|
||||
}
|
||||
|
||||
public async Task UpdateUserPassword(string userId, string passwordHash)
|
||||
{
|
||||
using var conn = CreateConnection();
|
||||
await conn.ExecuteAsync(
|
||||
"UPDATE users SET password_hash = @passwordHash, updated_at = @now WHERE user_id = @userId",
|
||||
new { userId, passwordHash, now = DateTime.UtcNow.ToString("o") });
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user