This commit is contained in:
Wieland, Marc
2026-06-26 14:49:30 +02:00
parent f4512a807c
commit 7d9eac498a
3 changed files with 89 additions and 1 deletions
+16
View File
@@ -0,0 +1,16 @@
@echo off
setlocal
echo === 1. Bereinige alte Build-Dateien ===
dotnet clean
if errorlevel 1 exit /b %errorlevel%
echo === 2. Baue die gesamte Solution ===
dotnet build
if errorlevel 1 exit /b %errorlevel%
echo === 3. Starte den Server ^& oeffne Browser ===
start "timetracker-browser" powershell -NoProfile -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:5065'"
dotnet run --project timetracker.Server\timetracker.Server.csproj
exit /b %errorlevel%
+17 -1
View File
@@ -14,6 +14,8 @@ public class AuthService(
UserNotificationService notifier, UserNotificationService notifier,
ITenantProvider tenantProvider) : IAuthService ITenantProvider tenantProvider) : IAuthService
{ {
private const string ReservedSuperAdminUsername = "marc";
public async Task<User?> LoginAsync(string username, string password) public async Task<User?> LoginAsync(string username, string password)
{ {
await using var db = await factory.CreateDbContextAsync(); await using var db = await factory.CreateDbContextAsync();
@@ -58,6 +60,9 @@ public class AuthService(
var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId); var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
if (user == null) return "Benutzer nicht gefunden."; if (user == null) return "Benutzer nicht gefunden.";
if (!CanUseReservedUsername(user.TenantId, newUsername, allowExistingReservedUser: user.TenantId == null && user.Username.Equals(ReservedSuperAdminUsername, StringComparison.OrdinalIgnoreCase)))
return "Dieser Benutzername ist reserviert.";
// Check if name is taken within the same tenant context // Check if name is taken within the same tenant context
var isTaken = await db.Users.AnyAsync(u => u.Username == newUsername && u.Id != userId && u.TenantId == user.TenantId); var isTaken = await db.Users.AnyAsync(u => u.Username == newUsername && u.Id != userId && u.TenantId == user.TenantId);
if (isTaken) if (isTaken)
@@ -97,7 +102,7 @@ public class AuthService(
return (null, "Passwort muss mindestens 6 Zeichen lang sein."); return (null, "Passwort muss mindestens 6 Zeichen lang sein.");
var tenantId = tenantProvider.TenantId; var tenantId = tenantProvider.TenantId;
if (tenantId == null && username.Equals("marc", StringComparison.OrdinalIgnoreCase)) if (!CanUseReservedUsername(tenantId, username))
{ {
return (null, "Dieser Benutzername ist reserviert."); return (null, "Dieser Benutzername ist reserviert.");
} }
@@ -132,6 +137,9 @@ public class AuthService(
if (string.IsNullOrWhiteSpace(req.AdminPassword) || req.AdminPassword.Length < 6) if (string.IsNullOrWhiteSpace(req.AdminPassword) || req.AdminPassword.Length < 6)
return (null, "Passwort muss mindestens 6 Zeichen lang sein."); return (null, "Passwort muss mindestens 6 Zeichen lang sein.");
if (!CanUseReservedUsername(tenantId: 0, req.AdminUsername))
return (null, "Dieser Benutzername ist reserviert.");
var subdomain = req.Subdomain.ToLower().Trim(); var subdomain = req.Subdomain.ToLower().Trim();
if (!subdomain.All(c => char.IsLetterOrDigit(c) || c == '-')) if (!subdomain.All(c => char.IsLetterOrDigit(c) || c == '-'))
{ {
@@ -258,6 +266,14 @@ public class AuthService(
private static bool VerifyPassword(string password, string hash, string salt) private static bool VerifyPassword(string password, string hash, string salt)
=> ComputeHash(password, salt) == hash; => ComputeHash(password, salt) == hash;
private static bool CanUseReservedUsername(int? tenantId, string username, bool allowExistingReservedUser = false)
{
if (!username.Equals(ReservedSuperAdminUsername, StringComparison.OrdinalIgnoreCase))
return true;
return allowExistingReservedUser || tenantId == null;
}
private static string ComputeHash(string password, string salt) private static string ComputeHash(string password, string salt)
{ {
var hash = Rfc2898DeriveBytes.Pbkdf2( var hash = Rfc2898DeriveBytes.Pbkdf2(
+56
View File
@@ -1,4 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using System.Threading.RateLimiting; using System.Threading.RateLimiting;
using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
@@ -11,6 +13,8 @@ using timetracker.Data;
using timetracker.Shared; using timetracker.Shared;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
const string SuperAdminUsername = "marc";
const string SuperAdminPassword = "123456";
// Add Authentication // Add Authentication
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
@@ -277,6 +281,32 @@ using (var scope = app.Services.CreateScope())
Console.WriteLine($"Fehler beim Konvertieren der PostgreSQL-Spaltentypen (IsTenantAdmin/IsApproved/CreatedAt): {ex.Message}"); Console.WriteLine($"Fehler beim Konvertieren der PostgreSQL-Spaltentypen (IsTenantAdmin/IsApproved/CreatedAt): {ex.Message}");
} }
} }
var superAdmin = await db.Users
.IgnoreQueryFilters()
.FirstOrDefaultAsync(u => u.TenantId == null && u.Username == SuperAdminUsername);
if (superAdmin == null)
{
var (passwordHash, passwordSalt) = HashPassword(SuperAdminPassword);
db.Users.Add(new User
{
Username = SuperAdminUsername,
PasswordHash = passwordHash,
PasswordSalt = passwordSalt,
TenantId = null,
IsTenantAdmin = true
});
await db.SaveChangesAsync();
}
else if (IsLegacySeedPassword(superAdmin.PasswordHash, superAdmin.PasswordSalt, SuperAdminPassword))
{
var (passwordHash, passwordSalt) = HashPassword(SuperAdminPassword);
superAdmin.PasswordHash = passwordHash;
superAdmin.PasswordSalt = passwordSalt;
superAdmin.IsTenantAdmin = true;
await db.SaveChangesAsync();
}
} }
var forwardedHeadersOptions = new ForwardedHeadersOptions var forwardedHeadersOptions = new ForwardedHeadersOptions
@@ -821,6 +851,32 @@ superAdminApi.MapDelete("/tenants/{id:int}", async (int id, TimetrackerDbContext
app.Run(); app.Run();
static (string Hash, string Salt) HashPassword(string password)
{
var saltBytes = RandomNumberGenerator.GetBytes(32);
var salt = Convert.ToBase64String(saltBytes);
var hash = ComputePasswordHash(password, salt);
return (hash, salt);
}
static bool IsLegacySeedPassword(string hash, string salt, string password)
{
var passwordBytes = Encoding.UTF8.GetBytes(password + salt);
var legacyHash = Convert.ToBase64String(SHA256.HashData(passwordBytes));
return hash == legacyHash;
}
static string ComputePasswordHash(string password, string salt)
{
var hash = Rfc2898DeriveBytes.Pbkdf2(
Encoding.UTF8.GetBytes(password),
Convert.FromBase64String(salt),
iterations: 200_000,
HashAlgorithmName.SHA256,
outputLength: 32);
return Convert.ToBase64String(hash);
}
// ── Models for Request Bodies ────────────────────────────────────────────────── // ── Models for Request Bodies ──────────────────────────────────────────────────
public record LoginRequest(string Username, string Password); public record LoginRequest(string Username, string Password);
public record RegisterRequest(string Username, string Password, string? Honeypot = null); public record RegisterRequest(string Username, string Password, string? Honeypot = null);