@page "/login" @inherits LocalizableComponentBase @Localizer["Login"] @* One sign-in for both of PrivaPub's APIs: the root signs in here, then picks the persona to act as when it has more than one (CascadingState.LoadSession picks the last one used, or the only one). A server without an administrator offers its setup (Setup); anyone else joins through a group's invitation (Join). A forgotten password is recovered through the login's verified address (PasswordRecovery). With two-factor sign-in on, PrivaPub answers a right password with a 428, and the same form goes again with the authenticator app's six digits or a recovery code (LoginForm.OneTimeCode). The heading names the server as its administrator calls it (/api/v2/instance, read without credentials); About tells the rest. *@
@if (!CascadingState.IsSignedIn) {
@if (setupPending) { @Localizer["This server has no administrator yet"] @Localizer["Set it up: the first login made becomes its administrator."] }

@Localizer["Sign in to {0}", serverTitle ?? "PrivaPub"]

@if (needsCode) { @* The name and the password stay in the form, out of sight, and go again with the code. *@

@Localizer["Enter the code from your authenticator app, or a recovery code."]

@if (useRecoveryCode) { } else {
@Localizer["Code from the authenticator app"]
} @(useRecoveryCode ? Localizer["Use the authenticator app instead"] : Localizer["Use a recovery code instead"]) } else {

@Localizer["One login for all of your personas: each one is signed in separately and never linked to the others."]

} @if (isWorking) { } @Localizer["Login"] @if (needsCode) { @Localizer["Back"] } else { @Localizer["Forgot your password?"] }

@Localizer["Have an invitation?"] @Localizer["Join with an invitation"]

@Localizer["About this server"]

} else if (CascadingState.Personas.Count == 0) {

@Localizer["No persona yet"]

@Localizer["This login has no persona to act as yet: make one in Settings."]

@Localizer["Settings"] @Localizer["Logout"]
} else {

@Localizer["Who are you today?"]

@Localizer["Each persona has its own token; PrivaPub never tells anyone they share a login."]

@foreach (var persona in CascadingState.Personas) { }
}
@code { [Inject] AuthService AuthService { get; set; } [Inject] AboutService AboutService { get; set; } readonly LoginForm form = new(); string error; bool isWorking; bool setupPending; // the server's own name, once read string serverTitle; /// PrivaPub asked for a code (its 428): the password was right, and two-factor sign-in is on. bool needsCode; /// The code asked for is a recovery code (two groups of five letters and digits), not the app's six digits. bool useRecoveryCode; [SupplyParameterFromQuery] public string ReturnUrl { get; set; } protected override async Task OnInitializedAsync() { if (CascadingState.IsOffline) { WhenOnline(LoadData); StartOnlinePolling(); IsLoading = false; return; } await LoadData(); IsLoading = false; } async Task LoadData() { serverTitle = (await AboutService.Instance())?.Title; setupPending = !CascadingState.IsSignedIn && await AuthService.SetupPending(); } async Task SignIn() { error = default; isWorking = true; form.OneTimeCode = needsCode ? form.OneTimeCode?.Trim() : default; var result = await AuthService.Login(form); if (AuthService.NeedsOneTimeCode(result)) { needsCode = true; form.OneTimeCode = default; isWorking = false; return; } if (result.IsInvalid) { // a wrong code is PrivaPub's 400, like a wrong password, with its own message; the code is typed again error = result.ErrorMessage; form.OneTimeCode = default; isWorking = false; return; } await CascadingState.SignedIn(result.Data as JwtUser); isWorking = false; needsCode = false; useRecoveryCode = false; form.OneTimeCode = default; if (CascadingState.Personas.Count == 1 && CascadingState.Persona is not null) Continue(); } async Task Choose(string personaId) { error = default; isWorking = true; var result = await CascadingState.SwitchPersona(personaId); isWorking = false; if (result.IsInvalid) { error = result.ErrorMessage; return; } Continue(); } void ToggleRecoveryCode() { useRecoveryCode = !useRecoveryCode; form.OneTimeCode = default; error = default; } /// Back to the name and the password, for another login: the password is typed again. void Back() { needsCode = false; useRecoveryCode = false; form.OneTimeCode = default; form.Password = default; error = default; } async Task SignOut() => await CascadingState.SignOut(); /// Back where the reader was sent from, never off the app. void Continue() { var target = ReturnUrl is { Length: > 0 } url && !url.Contains("://") && !url.StartsWith("//") && !url.StartsWith("login") ? url : ""; Navigation.NavigateTo(target); } }