494 lines
23 KiB
Plaintext
494 lines
23 KiB
Plaintext
@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; } = "";
|
||
}
|
||
}
|