Iceshrimp.NET/Iceshrimp.Frontend/Pages/Login.razor

231 lines
No EOL
9.2 KiB
Text

@page "/login"
@using System.Diagnostics.CodeAnalysis
@using Iceshrimp.Frontend.Components
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Schemas
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Frontend.Localization
@using Iceshrimp.Shared.Schemas.Web
@using Microsoft.Extensions.Localization
@using Iceshrimp.Assets.PhosphorIcons
@inject ApiService Api
@inject SessionService SessionService
@inject NavigationManager Navigation
@inject IStringLocalizer<Localization> Loc;
@inject MetadataService Metadata;
@inject IJSRuntime Js;
@inject ILogger<Login> Logger;
@layout UnauthLayout
<div class="body">
<img class="logo" src="/_content/Iceshrimp.Assets.Branding/splash.png"/>
<span>
<h3>@Loc["Login to {0}", Name ?? "this Iceshrimp.NET Instance."]</h3></span>
<div class="login-form">
<input placeholder="@Loc["Username"]" autocomplete="username" name="username" required="required"
@bind="@Username"/>
<input type="password" placeholder="@Loc["Password"]" autocomplete="current-password" name="current-password"
required="required"
@bind="@Password"/>
<button class="button" @onclick="Submit" disabled="@Loading">@Loc["Login"]</button>
</div>
@if (Loading)
{
<span>Loading!</span>
}
@if (Failure)
{
<span>Authentication Failed</span>
}
@if (SessionService.Users.Count > 0)
{
<h2>@Loc["Existing sessions"]</h2>
<div class="user-picker">
@foreach(var user in SessionService.Users.Values)
{
<div class="user" tabindex="0" @onclick="() => LoginUser(user)">
<UserAvatar User="@user" Size="4rem"/>
<div class="username"><UserDisplayName User="user"></UserDisplayName></div>
</div>
}
</div>
}
@if (Registration != Registrations.Closed)
{
<a class="register-link" href="/register">@Loc["Register a new account"]</a>
}
<dialog class="dialog" @ref="Dialog">
<div class="dialog-content">
<div class="header">
<h2 class="title">@Loc["Two-Factor Code required"]</h2>
<button class="button close" @onclick="() => CloseDialog(Dialog)">
<Icon Name="Icons.X"></Icon>
</button>
</div>
<input class="otp-input" @bind="Otp" autocomplete="one-time-code" autofocus="autofocus"/>
<StateButton ExtraClasses="button" @ref="OtpButton" OnClick="TwoFactorLogin">
<Loading>@Loc["Processing"]</Loading>
<Failed>@Loc["2FA Failed"]</Failed>
<Success>@Loc["Success!"]</Success>
<Initial>
@Loc["Login"]</Initial>
</StateButton>
</div>
<div class="backdrop"></div>
</dialog>
</div>
@code {
[SupplyParameterFromQuery(Name = "rd")]
[SuppressMessage("ReSharper", "UnusedAutoPropertyAccessor.Local")]
private string? Redirect { get; set; }
private string? Password { get; set; }
private string? Username { get; set; }
private bool Loading { get; set; }
private bool Failure { get; set; }
private string? Name { get; set; }
private ElementReference Dialog { get; set; }
private string? Otp { get; set; }
private StateButton OtpButton { get; set; } = null!;
private readonly Lazy<Task<IJSObjectReference>> _moduleTask;
private Registrations Registration { get; set; }
public Login()
{
_moduleTask = new Lazy<Task<IJSObjectReference>>(() =>
Js.InvokeAsync<IJSObjectReference>(
"import",
"./Pages/Settings/Account.razor.js")
.AsTask());
}
private async Task Submit()
{
var module = await _moduleTask.Value;
Loading = true;
try
{
if (Username == null || Password == null)
{
Loading = false;
Failure = true;
StateHasChanged(); // Manually triggering a state update, else component will not re-render.
return;
}
var res = await Api.Auth.LoginAsync(new AuthRequest { Username = Username, Password = Password });
switch (res.Status)
{
case AuthStatusEnum.Authenticated:
SessionService.AddUser(new StoredUser
{ // Token nor user will ever be null on an authenticated response
Id = res.User!.Id,
Username = res.User.Username,
DisplayName = res.User.DisplayName,
AvatarUrl = res.User.AvatarUrl,
BannerUrl = res.User.BannerUrl,
InstanceName = res.User.InstanceName,
InstanceIconUrl = res.User.InstanceIconUrl,
Token = res.Token!,
Host = res.User.Host,
IsAdmin = res.IsAdmin ?? false,
IsModerator = res.IsModerator ?? false,
Emojis = res.User.Emojis,
MovedTo = res.User.MovedTo
});
SessionService.SetSession(res.User.Id);
Navigation.NavigateTo(Uri.TryCreate(Redirect, UriKind.Relative, out _) ? Redirect : "/", true);
break;
case AuthStatusEnum.Guest:
Failure = true;
Loading = false;
break;
case AuthStatusEnum.TwoFactor:
await module.InvokeVoidAsync("openDialog", Dialog);
Api.SetBearerToken(res.Token!);
break;
}
}
catch (ApiException)
{
Loading = false;
Failure = false;
StateHasChanged(); // Manually triggering a state update, else component will not re-render.
}
}
private async Task TwoFactorLogin()
{
var module = await _moduleTask.Value;
if (Otp is null)
{
return;
}
try
{
OtpButton.State = StateButton.StateEnum.Loading;
StateHasChanged();
var res = await Api.Auth.SubmitTwoFactorAsync(new TwoFactorRequest { Code = Otp });
if (res.Status is AuthStatusEnum.Authenticated)
{
SessionService.AddUser(new StoredUser
{
Id = res.User!.Id,
Username = res.User.Username,
DisplayName = res.User.DisplayName,
AvatarUrl = res.User.AvatarUrl,
BannerUrl = res.User.BannerUrl,
InstanceName = res.User.InstanceName,
InstanceIconUrl = res.User.InstanceIconUrl,
Token = res.Token!,
Host = res.User.Host,
IsAdmin = res.IsAdmin ?? false,
IsModerator = res.IsModerator ?? false,
Emojis = res.User.Emojis,
MovedTo = res.User.MovedTo
});
SessionService.SetSession(res.User.Id);
Navigation.NavigateTo(Uri.TryCreate(Redirect, UriKind.Relative, out _) ? Redirect : "/", true);
}
else
{
Failure = true;
Loading = false;
OtpButton.State = StateButton.StateEnum.Failed;
await module.InvokeVoidAsync("closeDialog", Dialog);
OtpButton.State = StateButton.StateEnum.Initial;
}
}
catch (ApiException e)
{
Logger.LogError(e, "2FA enrollment failed");
OtpButton.State = StateButton.StateEnum.Failed;
}
}
private async Task LoginUser(StoredUser user)
{
SessionService.SetSession(user.Id);
await SessionService.UpdateCurrentUserAsync();
Navigation.NavigateTo(Uri.TryCreate(Redirect, UriKind.Relative, out _) ? Redirect : "/", true);
}
protected override async Task OnInitializedAsync()
{
var metadata = await Metadata.Instance.Value;
Name = metadata.Name;
Registration = metadata.Registration;
}
private async Task CloseDialog(ElementReference dialog)
{
var module = await _moduleTask.Value;
await module.InvokeVoidAsync("closeDialog", dialog);
}
}