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,
|
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(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user