Updates
This commit is contained in:
@@ -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%
|
||||
@@ -14,6 +14,8 @@ public class AuthService(
|
||||
UserNotificationService notifier,
|
||||
ITenantProvider tenantProvider) : IAuthService
|
||||
{
|
||||
private const string ReservedSuperAdminUsername = "marc";
|
||||
|
||||
public async Task<User?> LoginAsync(string username, string password)
|
||||
{
|
||||
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);
|
||||
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
|
||||
var isTaken = await db.Users.AnyAsync(u => u.Username == newUsername && u.Id != userId && u.TenantId == user.TenantId);
|
||||
if (isTaken)
|
||||
@@ -97,7 +102,7 @@ public class AuthService(
|
||||
return (null, "Passwort muss mindestens 6 Zeichen lang sein.");
|
||||
|
||||
var tenantId = tenantProvider.TenantId;
|
||||
if (tenantId == null && username.Equals("marc", StringComparison.OrdinalIgnoreCase))
|
||||
if (!CanUseReservedUsername(tenantId, username))
|
||||
{
|
||||
return (null, "Dieser Benutzername ist reserviert.");
|
||||
}
|
||||
@@ -132,6 +137,9 @@ public class AuthService(
|
||||
if (string.IsNullOrWhiteSpace(req.AdminPassword) || req.AdminPassword.Length < 6)
|
||||
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();
|
||||
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)
|
||||
=> 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)
|
||||
{
|
||||
var hash = Rfc2898DeriveBytes.Pbkdf2(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.RateLimiting;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
@@ -11,6 +13,8 @@ using timetracker.Data;
|
||||
using timetracker.Shared;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
const string SuperAdminUsername = "marc";
|
||||
const string SuperAdminPassword = "123456";
|
||||
|
||||
// Add Authentication
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -821,6 +851,32 @@ superAdminApi.MapDelete("/tenants/{id:int}", async (int id, TimetrackerDbContext
|
||||
|
||||
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 ──────────────────────────────────────────────────
|
||||
public record LoginRequest(string Username, string Password);
|
||||
public record RegisterRequest(string Username, string Password, string? Honeypot = null);
|
||||
|
||||
Reference in New Issue
Block a user