Files
timetracker/timetracker.Client/Components/Pages/Login.razor
T
2026-06-24 23:48:04 +02:00

494 lines
23 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@page "/login"
@rendermode InteractiveWebAssembly
@attribute [AllowAnonymous]
@using System.Net.Http.Json
@inject IAuthService AuthService
@inject NavigationManager Nav
@inject IJSRuntime JSRuntime
@inject HttpClient Http
<PageTitle>@GetPageTitle()</PageTitle>
@if (_tenantInfo == null)
{
@* Loading indicator while resolving Tenant *@
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16 text-center">
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large" />
<MudText Class="mt-4" Color="Color.Secondary">Lade Timetracker…</MudText>
</MudContainer>
}
else if (_tenantInfo.IsTenant && !_tenantInfo.IsApproved)
{
@* Pending Approval Screen *@
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16">
<MudStack AlignItems="AlignItems.Center" Spacing="4">
<MudStack AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccessTime" Style="font-size:4rem; color:#EAB308" />
<MudText Typo="Typo.h4" Style="font-weight:700; color:#EAB308">Timetracker</MudText>
</MudStack>
<MudPaper Elevation="4" Class="pa-8 rounded-xl text-center" Style="width:100%; border-top: 4px solid var(--mud-palette-warning);">
<MudIcon Icon="@Icons.Material.Filled.HourglassEmpty"
Style="font-size: 5rem; animation: pulse 2s infinite;"
Color="Color.Warning"
Class="mb-4" />
<MudText Typo="Typo.h5" Style="font-weight: 700;" Class="mb-2">Freischaltung ausstehend</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-6">
Die Firma <strong>@_tenantInfo.Name</strong> wurde erfolgreich registriert.
Der Zugang wird derzeit von unserem SuperAdmin geprüft und in Kürze freigeschaltet.
</MudText>
<MudDivider Class="my-4" />
<MudText Typo="Typo.caption" Color="Color.Secondary">
Bitte laden Sie diese Seite in Kürze neu, sobald Ihr Account freigegeben wurde.
</MudText>
</MudPaper>
</MudStack>
</MudContainer>
<style>
@@keyframes pulse {
0% { opacity: 0.6; transform: scale(1); }
50% { opacity: 1; transform: scale(1.05); }
100% { opacity: 0.6; transform: scale(1); }
}
</style>
}
else
{
@* Main login/registration container *@
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-16">
<MudStack AlignItems="AlignItems.Center" Spacing="4">
@* ── Logo / Header ── *@
<MudStack AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.AccessTime"
Style="font-size:4rem; color:#0EA5E9" />
<MudText Typo="Typo.h4" Style="font-weight:700; color:#0EA5E9">
@(_tenantInfo.IsTenant ? _tenantInfo.Name : "Timetracker SaaS")
</MudText>
@if (!_tenantInfo.IsTenant)
{
<MudText Typo="Typo.caption" Color="Color.Secondary">Zentrales Portal</MudText>
}
</MudStack>
<MudPaper Elevation="4" Class="pa-6 rounded-xl" Style="width:100%">
@if (_firmRegisteredSuccessfully)
{
@* Success feedback after firm registration *@
<MudStack Spacing="4" AlignItems="AlignItems.Center" Class="text-center pa-4">
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Style="font-size:5rem;" />
<MudText Typo="Typo.h5" Style="font-weight:700;">Registrierung erfolgreich!</MudText>
<MudText Typo="Typo.body1" Color="Color.Secondary">
Ihre Firma <strong>@_firmModel.CompanyName</strong> wurde erfolgreich registriert.
</MudText>
<MudAlert Severity="Severity.Info" Class="rounded-lg">
Ihr Zugang muss nun durch den SuperAdmin freigegeben werden. Danach können Sie sich unter Ihrer Subdomain anmelden:
<br /><br />
<strong>@GetTenantUrl()</strong>
</MudAlert>
<MudButton Variant="Variant.Filled" Color="Color.Primary" Class="mt-4" OnClick="ResetRegistrationFlow">
Zurück zum Login
</MudButton>
</MudStack>
}
else
{
@* ── Tab Navigation ── *@
<MudStack Row="true" Justify="Justify.Center" Class="mb-4">
<MudButton OnClick="@(() => SetTab(0))"
Variant="@(_activeTab == 0 ? Variant.Filled : Variant.Text)"
Color="Color.Primary"
Style="min-width: 120px; border-radius: 20px;">
Anmelden
</MudButton>
<MudButton OnClick="@(() => SetTab(1))"
Variant="@(_activeTab == 1 ? Variant.Filled : Variant.Text)"
Color="Color.Primary"
Style="min-width: 120px; border-radius: 20px;">
@(_tenantInfo.IsTenant ? "Registrieren" : "Firma registrieren")
</MudButton>
</MudStack>
<MudDivider Class="mb-6" />
@if (_activeTab == 0)
{
@* ── Login Form ── *@
<MudStack Spacing="3">
@if (_error != null)
{
<MudAlert Severity="Severity.Error" Dense="true">@_error</MudAlert>
}
<EditForm Model="@_loginModel" OnValidSubmit="HandleLogin">
<MudStack Spacing="3">
<MudTextField T="string"
Label="Benutzername"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person"
@bind-Value="_loginModel.Username"
Required="true"
AutoFocus="true" />
<MudTextField T="string"
Label="Passwort"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Lock"
InputType="InputType.Password"
@bind-Value="_loginModel.Password"
Required="true" />
<MudButton ButtonType="ButtonType.Submit"
Variant="Variant.Filled"
Color="Color.Primary"
FullWidth="true"
Size="Size.Large"
StartIcon="@Icons.Material.Filled.Login"
Class="mt-2"
Disabled="_loading">
@if (_loading)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
}
Anmelden
</MudButton>
</MudStack>
</EditForm>
</MudStack>
}
else
{
@if (_tenantInfo.IsTenant)
{
@* ── Employee Register Form ── *@
<MudStack Spacing="3">
@if (_error != null)
{
<MudAlert Severity="Severity.Error" Dense="true">@_error</MudAlert>
}
<EditForm Model="@_registerModel" OnValidSubmit="HandleRegister">
<MudStack Spacing="3">
<MudTextField T="string"
Label="Benutzername"
Variant="Variant.Outlined"
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Person"
@bind-Value="_registerModel.Username"
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="_registerModel.Password"
Required="true"
HelperText="Mindestens 6 Zeichen" />
<input type="text" style="display: none;" tabindex="-1" autocomplete="off" @bind="_honeypot" />
<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.PersonAdd"
Class="mt-2"
Disabled="_loading || !_acceptAgb">
@if (_loading)
{
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
}
Konto erstellen
</MudButton>
</MudStack>
</EditForm>
</MudStack>
}
else
{
@* ── 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>
}
}
}
</MudPaper>
</MudStack>
</MudContainer>
}
@code {
private TenantInfo? _tenantInfo;
private int _activeTab = 0;
private string? _error;
private bool _loading;
private string _honeypot = "";
private bool _acceptAgb;
private bool _firmRegisteredSuccessfully;
private readonly AuthModel _loginModel = new();
private readonly AuthModel _registerModel = new();
private readonly FirmModel _firmModel = new();
[SupplyParameterFromQuery(Name = "error")]
public string? ErrorParam { get; set; }
[SupplyParameterFromQuery(Name = "tab")]
public string? TabParam { get; set; }
protected override async Task OnInitializedAsync()
{
try
{
_tenantInfo = await Http.GetFromJsonAsync<TenantInfo>("api/tenant/current");
}
catch (Exception ex)
{
Console.WriteLine($"Fehler beim Abrufen der TenantInfo: {ex.Message}");
// Fallback
_tenantInfo = new TenantInfo { IsTenant = false };
}
}
protected override void OnParametersSet()
{
_error = ErrorParam switch
{
"invalid" => "Benutzername oder Passwort falsch.",
not null => Uri.UnescapeDataString(ErrorParam),
_ => null
};
_activeTab = TabParam == "register" ? 1 : 0;
}
private void SetTab(int tab)
{
_activeTab = tab;
_error = null;
}
private string GetPageTitle()
{
if (_tenantInfo == null) return "Lade…";
return _tenantInfo.IsTenant ? $"Anmelden bei {_tenantInfo.Name} Timetracker" : "Anmelden Timetracker SaaS";
}
private string GetDomainSuffix()
{
var uri = new Uri(Nav.BaseUri);
var host = uri.Host;
return $".{host}";
}
private string GetTenantUrl()
{
var uri = new Uri(Nav.BaseUri);
var scheme = uri.Scheme;
var port = uri.Port == 80 || uri.Port == 443 ? "" : $":{uri.Port}";
var host = uri.Host;
return $"{scheme}://{_firmModel.Subdomain.ToLower().Trim()}.{host}{port}";
}
private void ResetRegistrationFlow()
{
_firmRegisteredSuccessfully = false;
_activeTab = 0;
_error = null;
}
private async Task HandleLogin()
{
_loading = true;
_error = null;
try
{
var user = await AuthService.LoginAsync(_loginModel.Username, _loginModel.Password);
if (user != null)
{
Nav.NavigateTo("/", forceLoad: true);
}
else
{
_error = "Benutzername oder Passwort falsch.";
}
}
catch (Exception ex)
{
_error = $"Login Fehler: {ex.Message}";
}
finally
{
_loading = false;
}
}
private async Task HandleRegister()
{
if (!_acceptAgb)
{
_error = "Du musst die AGB akzeptieren.";
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()
{
if (!_acceptAgb)
{
_error = "Du musst die AGB akzeptieren.";
return;
}
if (_firmModel.AdminPassword != _firmModel.ConfirmPassword)
{
_error = "Passwörter stimmen nicht überein.";
return;
}
_loading = true;
_error = null;
try
{
var req = new RegisterFirmRequest(
_firmModel.CompanyName,
_firmModel.Subdomain,
_firmModel.AdminUsername,
_firmModel.AdminPassword
);
var (tenant, error) = await AuthService.RegisterFirmAsync(req);
if (tenant != null)
{
_firmRegisteredSuccessfully = true;
}
else
{
_error = error ?? "Registrierung der Firma fehlgeschlagen.";
}
}
catch (Exception ex)
{
_error = $"Registrierungs Fehler: {ex.Message}";
}
finally
{
_loading = false;
}
}
private class AuthModel
{
public string Username { get; set; } = "";
public string Password { get; set; } = "";
}
private class FirmModel
{
public string CompanyName { get; set; } = "";
public string Subdomain { get; set; } = "";
public string AdminUsername { get; set; } = "";
public string AdminPassword { get; set; } = "";
public string ConfirmPassword { get; set; } = "";
}
}