Multi tenant integration #10

Merged
mrcwlnd merged 5 commits from multi-tenant-integration into main 2026-06-25 18:34:06 +00:00
5 changed files with 188 additions and 59 deletions
Showing only changes of commit 2f95fa0e3e - Show all commits
@@ -67,7 +67,7 @@
</div> </div>
<div class="user-info"> <div class="user-info">
<span class="user-name">@context.User.Identity?.Name</span> <span class="user-name">@context.User.Identity?.Name</span>
<span class="user-status">@_companyName</span> <span class="user-status">@GetUserStatus(context.User.Identity?.Name)</span>
</div> </div>
</div> </div>
</NavLink> </NavLink>
@@ -159,4 +159,12 @@
return parts[0][..Math.Min(2, parts[0].Length)].ToUpper(); return parts[0][..Math.Min(2, parts[0].Length)].ToUpper();
return $"{parts[0][0]}{parts[1][0]}".ToUpper(); return $"{parts[0][0]}{parts[1][0]}".ToUpper();
} }
private string GetUserStatus(string? username)
{
if (string.IsNullOrWhiteSpace(username)) return "";
if (username.Equals("marc", StringComparison.OrdinalIgnoreCase)) return "SuperAdmin";
if (_companyName == "Hauptdomain") return "Einzelnutzer";
return _companyName;
}
} }
@@ -102,7 +102,7 @@ else
else else
{ {
<MudChip T="string" Color="Color.Default" Variant="Variant.Text" Size="Size.Small"> <MudChip T="string" Color="Color.Default" Variant="Variant.Text" Size="Size.Small">
Hauptdomain (SuperUser) @(context.Username == "marc" ? "SuperAdmin" : "Einzelnutzer")
</MudChip> </MudChip>
} }
} }
+171 -50
View File
@@ -91,26 +91,36 @@ else
Zurück zum Login Zurück zum Login
</MudButton> </MudButton>
</MudStack> </MudStack>
} } else {
else @* ── Header / Navigation ── *@
{ @if (_activeTab == 2)
@* ── Tab Navigation ── *@ {
<MudStack Row="true" Justify="Justify.Center" Class="mb-4"> @* ── Back Button for Firm Registration ── *@
<MudButton OnClick="@(() => SetTab(0))" <MudStack Row="true" AlignItems="AlignItems.Center" Class="mb-4">
Variant="@(_activeTab == 0 ? Variant.Filled : Variant.Text)" <MudIconButton Icon="@Icons.Material.Filled.ArrowBack" OnClick="@(() => SetTab(0))" Color="Color.Primary" />
Color="Color.Primary" <MudText Typo="Typo.h6" Style="font-weight:700">Firma registrieren</MudText>
Style="min-width: 120px; border-radius: 20px;"> </MudStack>
Anmelden <MudDivider Class="mb-6" />
</MudButton> }
<MudButton OnClick="@(() => SetTab(1))" else
Variant="@(_activeTab == 1 ? Variant.Filled : Variant.Text)" {
Color="Color.Primary" @* ── Tab Navigation ── *@
Style="min-width: 120px; border-radius: 20px;"> <MudStack Row="true" Justify="Justify.Center" Class="mb-4">
@(_tenantInfo.IsTenant ? "Registrieren" : "Firma registrieren") <MudButton OnClick="@(() => SetTab(0))"
</MudButton> Variant="@(_activeTab == 0 ? Variant.Filled : Variant.Text)"
</MudStack> Color="Color.Primary"
Style="min-width: 120px; border-radius: 20px;">
<MudDivider Class="mb-6" /> Anmelden
</MudButton>
<MudButton OnClick="@(() => SetTab(1))"
Variant="@(_activeTab == 1 ? Variant.Filled : Variant.Text)"
Color="Color.Primary"
Style="min-width: 120px; border-radius: 20px;">
Registrieren
</MudButton>
</MudStack>
<MudDivider Class="mb-6" />
}
@if (_activeTab == 0) @if (_activeTab == 0)
{ {
@@ -156,7 +166,7 @@ else
</EditForm> </EditForm>
</MudStack> </MudStack>
} }
else else if (_activeTab == 1)
{ {
@if (_tenantInfo.IsTenant) @if (_tenantInfo.IsTenant)
{ {
@@ -211,61 +221,41 @@ else
} }
else else
{ {
@* ── Firm Register Form ── *@ @* ── Single User Register Form ── *@
<MudStack Spacing="3"> <MudStack Spacing="3">
@if (_error != null) @if (_error != null)
{ {
<MudAlert Severity="Severity.Error" Dense="true">@_error</MudAlert> <MudAlert Severity="Severity.Error" Dense="true">@_error</MudAlert>
} }
<EditForm Model="@_firmModel" OnValidSubmit="HandleRegisterFirm"> <EditForm Model="@_registerModel" OnValidSubmit="HandleSingleUserRegister">
<DataAnnotationsValidator />
<MudStack Spacing="3"> <MudStack Spacing="3">
<MudTextField T="string"
Label="Firmenname (z.B. Acme GmbH)"
Variant="Variant.Outlined"
@bind-Value="_firmModel.CompanyName"
Required="true" />
<MudTextField T="string"
Label="Subdomain"
Variant="Variant.Outlined"
HelperText="Nur Kleinbuchstaben, Zahlen & Bindestriche (z.B. acme)"
@bind-Value="_firmModel.Subdomain"
Required="true"
Adornment="Adornment.End"
AdornmentText="@GetDomainSuffix()" />
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2" Style="font-weight:700;">Administrator-Konto</MudText>
<MudTextField T="string" <MudTextField T="string"
Label="Benutzername" Label="Benutzername"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Adornment="Adornment.Start" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person" AdornmentIcon="@Icons.Material.Filled.Person"
@bind-Value="_firmModel.AdminUsername" @bind-Value="_registerModel.Username"
Required="true" Required="true"
HelperText="Mindestens 3 Zeichen" /> HelperText="Mindestens 3 Zeichen" />
<MudTextField T="string" <MudTextField T="string"
Label="Passwort" Label="Passwort"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Adornment="Adornment.Start" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock" AdornmentIcon="@Icons.Material.Filled.Lock"
InputType="InputType.Password" InputType="InputType.Password"
@bind-Value="_firmModel.AdminPassword" @bind-Value="_registerModel.Password"
Required="true" Required="true"
HelperText="Mindestens 6 Zeichen" /> HelperText="Mindestens 6 Zeichen" />
<MudTextField T="string" <MudTextField T="string"
Label="Passwort wiederholen" Label="Passwort wiederholen"
Variant="Variant.Outlined" Variant="Variant.Outlined"
Adornment="Adornment.Start" Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock" AdornmentIcon="@Icons.Material.Filled.Lock"
InputType="InputType.Password" InputType="InputType.Password"
@bind-Value="_firmModel.ConfirmPassword" @bind-Value="_confirmPassword"
Required="true" /> Required="true" />
<input type="text" style="display: none;" tabindex="-1" autocomplete="off" @bind="_honeypot" />
<MudCheckBox @bind-Value="_acceptAgb" Color="Color.Secondary" Class="mt-1"> <MudCheckBox @bind-Value="_acceptAgb" Color="Color.Secondary" Class="mt-1">
<MudText Typo="Typo.body2">Ich akzeptiere die <a href="/agb" target="_blank" style="color: var(--mud-palette-secondary); text-decoration: underline; font-weight: 600;">AGB</a>.</MudText> <MudText Typo="Typo.body2">Ich akzeptiere die <a href="/agb" target="_blank" style="color: var(--mud-palette-secondary); text-decoration: underline; font-weight: 600;">AGB</a>.</MudText>
</MudCheckBox> </MudCheckBox>
@@ -275,20 +265,112 @@ else
Color="Color.Secondary" Color="Color.Secondary"
FullWidth="true" FullWidth="true"
Size="Size.Large" Size="Size.Large"
StartIcon="@Icons.Material.Filled.Business" StartIcon="@Icons.Material.Filled.PersonAdd"
Class="mt-2" Class="mt-2"
Disabled="_loading || !_acceptAgb"> Disabled="_loading || !_acceptAgb">
@if (_loading) @if (_loading)
{ {
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" /> <MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
} }
Firma registrieren Konto erstellen
</MudButton> </MudButton>
</MudStack> </MudStack>
</EditForm> </EditForm>
</MudStack> </MudStack>
} }
} }
else if (_activeTab == 2)
{
@* ── Firm Register Form ── *@
<MudStack Spacing="3">
@if (_error != null)
{
<MudAlert Severity="Severity.Error" Dense="true">@_error</MudAlert>
}
<EditForm Model="@_firmModel" OnValidSubmit="HandleRegisterFirm">
<DataAnnotationsValidator />
<MudStack Spacing="3">
<MudTextField T="string"
Label="Firmenname (z.B. Acme GmbH)"
Variant="Variant.Outlined"
@bind-Value="_firmModel.CompanyName"
Required="true" />
<MudTextField T="string"
Label="Subdomain"
Variant="Variant.Outlined"
HelperText="Nur Kleinbuchstaben, Zahlen & Bindestriche (z.B. acme)"
@bind-Value="_firmModel.Subdomain"
Required="true"
Adornment="Adornment.End"
AdornmentText="@GetDomainSuffix()" />
<MudDivider Class="my-2" />
<MudText Typo="Typo.subtitle2" Style="font-weight:700;">Administrator-Konto</MudText>
<MudTextField T="string"
Label="Benutzername"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person"
@bind-Value="_firmModel.AdminUsername"
Required="true"
HelperText="Mindestens 3 Zeichen" />
<MudTextField T="string"
Label="Passwort"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock"
InputType="InputType.Password"
@bind-Value="_firmModel.AdminPassword"
Required="true"
HelperText="Mindestens 6 Zeichen" />
<MudTextField T="string"
Label="Passwort wiederholen"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock"
InputType="InputType.Password"
@bind-Value="_firmModel.ConfirmPassword"
Required="true" />
<MudCheckBox @bind-Value="_acceptAgb" Color="Color.Secondary" Class="mt-1">
<MudText Typo="Typo.body2">Ich akzeptiere die <a href="/agb" target="_blank" style="color: var(--mud-palette-secondary); text-decoration: underline; font-weight: 600;">AGB</a>.</MudText>
</MudCheckBox>
<MudButton ButtonType="ButtonType.Submit"
Variant="Variant.Filled"
Color="Color.Secondary"
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.Business"
Class="mt-2"
Disabled="_loading || !_acceptAgb">
@if (_loading)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
}
Firma registrieren
</MudButton>
</MudStack>
</EditForm>
</MudStack>
}
@if (!_tenantInfo.IsTenant && _activeTab != 2)
{
<MudDivider Class="my-4" />
<MudStack AlignItems="AlignItems.Center">
<MudText Typo="Typo.body2" Color="Color.Secondary">
Sie möchten Timetracker für Ihr Unternehmen nutzen?
</MudText>
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="@(() => SetTab(2))" StartIcon="@Icons.Material.Filled.Business">
Eigene Firma registrieren
</MudButton>
</MudStack>
}
} }
</MudPaper> </MudPaper>
</MudStack> </MudStack>
@@ -303,6 +385,7 @@ else
private string _honeypot = ""; private string _honeypot = "";
private bool _acceptAgb; private bool _acceptAgb;
private bool _firmRegisteredSuccessfully; private bool _firmRegisteredSuccessfully;
private string _confirmPassword = "";
private readonly AuthModel _loginModel = new(); private readonly AuthModel _loginModel = new();
private readonly AuthModel _registerModel = new(); private readonly AuthModel _registerModel = new();
@@ -432,6 +515,44 @@ else
} }
} }
private async Task HandleSingleUserRegister()
{
if (!_acceptAgb)
{
_error = "Du musst die AGB akzeptieren.";
return;
}
if (_registerModel.Password != _confirmPassword)
{
_error = "Passwörter stimmen nicht überein.";
return;
}
_loading = true;
_error = null;
try
{
var (user, error) = await AuthService.RegisterAsync(_registerModel.Username, _registerModel.Password, _honeypot);
if (user != null)
{
await JSRuntime.InvokeVoidAsync("localStorage.setItem", "showOnboarding", "true");
Nav.NavigateTo("/", forceLoad: true);
}
else
{
_error = error ?? "Registrierung fehlgeschlagen.";
}
}
catch (Exception ex)
{
_error = $"Registrierungs Fehler: {ex.Message}";
}
finally
{
_loading = false;
}
}
private async Task HandleRegisterFirm() private async Task HandleRegisterFirm()
{ {
if (!_acceptAgb) if (!_acceptAgb)
+5 -5
View File
@@ -97,15 +97,15 @@ 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) if (tenantId == null && username.Equals("marc", StringComparison.OrdinalIgnoreCase))
{ {
return (null, "Mitarbeiter-Registrierung ist nur über die Firmen-Subdomain möglich."); return (null, "Dieser Benutzername ist reserviert.");
} }
await using var db = await factory.CreateDbContextAsync(); await using var db = await factory.CreateDbContextAsync();
// Filter handles checking uniqueness within the same tenant // Filter handles checking uniqueness within the same tenant context
if (await db.Users.AnyAsync(u => u.Username == username)) if (await db.Users.AnyAsync(u => u.Username == username))
return (null, "Benutzername in dieser Firma bereits vergeben."); return (null, tenantId.HasValue ? "Benutzername in dieser Firma bereits vergeben." : "Benutzername bereits vergeben.");
var (hash, salt) = HashPassword(password); var (hash, salt) = HashPassword(password);
var user = new User var user = new User
@@ -113,7 +113,7 @@ public class AuthService(
Username = username, Username = username,
PasswordHash = hash, PasswordHash = hash,
PasswordSalt = salt, PasswordSalt = salt,
TenantId = tenantId.Value TenantId = tenantId
}; };
db.Users.Add(user); db.Users.Add(user);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
+2 -2
View File
@@ -219,9 +219,9 @@ app.MapPost("/api/auth/login", async (HttpContext ctx, [FromBody] LoginRequest r
} }
else else
{ {
if (user.Username != "marc") if (user.Username != "marc" && user.TenantId != null)
{ {
return Results.BadRequest("Anmeldung auf der Hauptdomain nur für Administratoren."); return Results.BadRequest("Benutzer gehört zu einer Firma. Bitte melden Sie sich über Ihre Firmen-Subdomain an.");
} }
} }