multi-tenant implementierung
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using timetracker.Shared;
|
||||
|
||||
namespace timetracker.Data;
|
||||
|
||||
public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNotificationService notifier) : IAuthService
|
||||
public class AuthService(
|
||||
IDbContextFactory<TimetrackerDbContext> factory,
|
||||
UserNotificationService notifier,
|
||||
ITenantProvider tenantProvider) : IAuthService
|
||||
{
|
||||
public async Task<User?> LoginAsync(string username, string password)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
// Global query filter automatically restricts lookup to current tenant context
|
||||
var user = await db.Users
|
||||
.FirstOrDefaultAsync(u => u.Username == username);
|
||||
if (user == null) return null;
|
||||
@@ -19,7 +27,9 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
|
||||
public async Task<List<User>> GetAllUsersAsync()
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
// Superadmin bypasses query filters to manage all users globally
|
||||
return await db.Users
|
||||
.IgnoreQueryFilters()
|
||||
.OrderBy(u => u.Username)
|
||||
.ToListAsync();
|
||||
}
|
||||
@@ -27,7 +37,7 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
|
||||
public async Task DeleteUserAsync(int userId)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var user = await db.Users.FindAsync(userId);
|
||||
var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user != null)
|
||||
{
|
||||
db.Users.Remove(user);
|
||||
@@ -43,12 +53,16 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
|
||||
return "Benutzername muss mindestens 3 Zeichen lang sein.";
|
||||
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
if (await db.Users.AnyAsync(u => u.Username == newUsername && u.Id != userId))
|
||||
return "Benutzername bereits vergeben.";
|
||||
|
||||
var user = await db.Users.FindAsync(userId);
|
||||
|
||||
// Find user ignoring filters so we can rename global admin too if needed
|
||||
var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null) return "Benutzer nicht gefunden.";
|
||||
|
||||
// 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)
|
||||
return "Benutzername in dieser Firma bereits vergeben.";
|
||||
|
||||
user.Username = newUsername;
|
||||
await db.SaveChangesAsync();
|
||||
await notifier.NotifyUsersChangedAsync();
|
||||
@@ -61,7 +75,7 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
|
||||
return "Passwort muss mindestens 6 Zeichen lang sein.";
|
||||
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var user = await db.Users.FindAsync(userId);
|
||||
var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null) return "Benutzer nicht gefunden.";
|
||||
|
||||
if (!VerifyPassword(currentPassword, user.PasswordHash, user.PasswordSalt))
|
||||
@@ -75,24 +89,164 @@ public class AuthService(IDbContextFactory<TimetrackerDbContext> factory, UserNo
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<(User? User, string? Error)> RegisterAsync(string username, string password, string? honeypot = null) {
|
||||
public async Task<(User? User, string? Error)> RegisterAsync(string username, string password, string? honeypot = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || username.Length < 3)
|
||||
return (null, "Benutzername muss mindestens 3 Zeichen lang sein.");
|
||||
if (string.IsNullOrWhiteSpace(password) || password.Length < 6)
|
||||
return (null, "Passwort muss mindestens 6 Zeichen lang sein.");
|
||||
|
||||
var tenantId = tenantProvider.TenantId;
|
||||
if (tenantId == null)
|
||||
{
|
||||
return (null, "Mitarbeiter-Registrierung ist nur über die Firmen-Subdomain möglich.");
|
||||
}
|
||||
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
// Filter handles checking uniqueness within the same tenant
|
||||
if (await db.Users.AnyAsync(u => u.Username == username))
|
||||
return (null, "Benutzername bereits vergeben.");
|
||||
return (null, "Benutzername in dieser Firma bereits vergeben.");
|
||||
|
||||
var (hash, salt) = HashPassword(password);
|
||||
var user = new User { Username = username, PasswordHash = hash, PasswordSalt = salt };
|
||||
var user = new User
|
||||
{
|
||||
Username = username,
|
||||
PasswordHash = hash,
|
||||
PasswordSalt = salt,
|
||||
TenantId = tenantId.Value
|
||||
};
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
await notifier.NotifyUsersChangedAsync();
|
||||
return (user, null);
|
||||
}
|
||||
|
||||
public async Task<(Tenant? Tenant, string? Error)> RegisterFirmAsync(RegisterFirmRequest req)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.CompanyName) || req.CompanyName.Length < 3)
|
||||
return (null, "Firmenname muss mindestens 3 Zeichen lang sein.");
|
||||
if (string.IsNullOrWhiteSpace(req.Subdomain) || req.Subdomain.Length < 3)
|
||||
return (null, "Subdomain muss mindestens 3 Zeichen lang sein.");
|
||||
if (string.IsNullOrWhiteSpace(req.AdminUsername) || req.AdminUsername.Length < 3)
|
||||
return (null, "Administrator-Benutzername muss mindestens 3 Zeichen lang sein.");
|
||||
if (string.IsNullOrWhiteSpace(req.AdminPassword) || req.AdminPassword.Length < 6)
|
||||
return (null, "Passwort muss mindestens 6 Zeichen lang sein.");
|
||||
|
||||
var subdomain = req.Subdomain.ToLower().Trim();
|
||||
if (!subdomain.All(c => char.IsLetterOrDigit(c) || c == '-'))
|
||||
{
|
||||
return (null, "Subdomain darf nur Buchstaben, Zahlen und Bindestriche enthalten.");
|
||||
}
|
||||
|
||||
var reservedSubdomains = new[] { "www", "api", "admin", "superadmin", "localhost", "mail", "portal", "app", "dev", "test" };
|
||||
if (reservedSubdomains.Contains(subdomain))
|
||||
{
|
||||
return (null, "Diese Subdomain ist reserviert und kann nicht verwendet werden.");
|
||||
}
|
||||
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
|
||||
// Tenant checks are global, no filters applied to Tenants table
|
||||
var exists = await db.Tenants.AnyAsync(t => t.Subdomain == subdomain);
|
||||
if (exists)
|
||||
{
|
||||
return (null, "Diese Subdomain ist bereits vergeben.");
|
||||
}
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
var tenant = new Tenant
|
||||
{
|
||||
Name = req.CompanyName,
|
||||
Subdomain = subdomain,
|
||||
IsApproved = false,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
db.Tenants.Add(tenant);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
var (hash, salt) = HashPassword(req.AdminPassword);
|
||||
var user = new User
|
||||
{
|
||||
Username = req.AdminUsername,
|
||||
PasswordHash = hash,
|
||||
PasswordSalt = salt,
|
||||
TenantId = tenant.Id,
|
||||
IsTenantAdmin = true
|
||||
};
|
||||
db.Users.Add(user);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
await notifier.NotifyUsersChangedAsync();
|
||||
return (tenant, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return (null, $"Registrierungsfehler: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> AssignTenantAsync(int userId, int? tenantId)
|
||||
{
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
|
||||
var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null) return "Benutzer nicht gefunden.";
|
||||
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
var tenantExists = await db.Tenants.AnyAsync(t => t.Id == tenantId.Value);
|
||||
if (!tenantExists) return "Firma existiert nicht.";
|
||||
}
|
||||
|
||||
await using var transaction = await db.Database.BeginTransactionAsync();
|
||||
try
|
||||
{
|
||||
user.TenantId = tenantId;
|
||||
|
||||
var settings = await db.AppSettings.IgnoreQueryFilters().Where(s => s.UserId == userId).ToListAsync();
|
||||
foreach (var s in settings) s.TenantId = tenantId;
|
||||
|
||||
var workDays = await db.WorkDays.IgnoreQueryFilters().Where(w => w.UserId == userId).ToListAsync();
|
||||
foreach (var w in workDays) w.TenantId = tenantId;
|
||||
|
||||
var vacationDays = await db.VacationDays.IgnoreQueryFilters().Where(v => v.UserId == userId).ToListAsync();
|
||||
foreach (var v in vacationDays) v.TenantId = tenantId;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
|
||||
await notifier.NotifyUsersChangedAsync();
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await transaction.RollbackAsync();
|
||||
return $"Fehler beim Verschieben des Benutzers: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> ResetPasswordAsync(int userId, string newPassword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newPassword) || newPassword.Length < 6)
|
||||
return "Passwort muss mindestens 6 Zeichen lang sein.";
|
||||
|
||||
await using var db = await factory.CreateDbContextAsync();
|
||||
var user = await db.Users.IgnoreQueryFilters().FirstOrDefaultAsync(u => u.Id == userId);
|
||||
if (user == null) return "Benutzer nicht gefunden.";
|
||||
|
||||
var (hash, salt) = HashPassword(newPassword);
|
||||
user.PasswordHash = hash;
|
||||
user.PasswordSalt = salt;
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return null;
|
||||
}
|
||||
|
||||
private static (string hash, string salt) HashPassword(string password)
|
||||
{
|
||||
var saltBytes = RandomNumberGenerator.GetBytes(32);
|
||||
|
||||
Reference in New Issue
Block a user