[frontend] Add basic login flow and session management.

This commit is contained in:
Lilian 2024-04-08 04:27:11 +02:00
parent 1d262ab326
commit adb9267cd2
No known key found for this signature in database
GPG key ID: 007CA12D692829E1
13 changed files with 241 additions and 5 deletions

View file

@ -1,6 +1,13 @@
<Router AppAssembly="@typeof(App).Assembly">
@using Microsoft.AspNetCore.Components.Authorization
@using Iceshrimp.Frontend.Components
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<RedirectToLogin/>
</NotAuthorized>
</AuthorizeRouteView>
@* <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/> *@
<FocusOnNavigate RouteData="@routeData" Selector="h1"/>
</Found>
<NotFound>

View file

@ -0,0 +1,5 @@
<h3>Note</h3>
@code {
}

View file

@ -0,0 +1,10 @@
@* FixMe! Make this actually re-direct back to the content! *@
@inject NavigationManager Navigation
<h3>RedirectToLogin</h3>
@code {
protected override void OnInitialized()
{
Navigation.NavigateTo("/login");
}
}

View file

@ -0,0 +1,9 @@
using System.Text.Json.Serialization;
using Iceshrimp.Shared.Schemas;
namespace Iceshrimp.Frontend.Core.Schemas;
public class StoredUser() : UserResponse
{
[JsonPropertyName("token")] public required string Token { get; set; }
}

View file

@ -0,0 +1,22 @@
using System.Security.Claims;
namespace Iceshrimp.Frontend.Core.Services;
using Microsoft.AspNetCore.Components.Authorization;
internal class CustomAuthStateProvider (SessionService sessionService) : AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
if (sessionService.Current != null) {
var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, sessionService.Current.Username), }, "Custom Authentication");
var user = new ClaimsPrincipal(identity);
return Task.FromResult(new AuthenticationState(user));
}
else
{
var identity = new ClaimsIdentity();
var user = new ClaimsPrincipal(identity);
return Task.FromResult(new AuthenticationState(user));
}
}
}

View file

@ -0,0 +1,71 @@
using Blazored.LocalStorage;
using Iceshrimp.Frontend.Core.Schemas;
using Iceshrimp.Shared.Schemas;
using Microsoft.AspNetCore.Components;
namespace Iceshrimp.Frontend.Core.Services;
internal class SessionService
{
[Inject] public ISyncLocalStorageService LocalStorage { get; }
[Inject] public ApiService ApiService { get; }
public Dictionary<string, StoredUser> Users { get; }
public UserResponse? Current { get; private set; }
public SessionService(ApiService apiService, ISyncLocalStorageService localStorage)
{
ApiService = apiService;
LocalStorage = localStorage;
Users = LocalStorage.GetItem<Dictionary<string, StoredUser>>("Users") ?? [];
var lastUser = LocalStorage.GetItem<string?>("last_user");
if (lastUser != null)
{
SetSession(lastUser);
}
}
private void WriteUsers()
{
LocalStorage.SetItem("Users", Users);
}
public void AddUser(StoredUser user)
{
try
{
Users.Add(user.Id, user);
}
catch( ArgumentException ) // Update user if it already exists.
{
Users[user.Id] = user;
}
WriteUsers();
}
public void DeleteUser(string id)
{
if (id == Current?.Id) throw new ArgumentException("Cannot remove current user.");
Users.Remove(id);
WriteUsers();
}
private StoredUser? GetUserById(string id)
{
var user = Users[id];
return user;
}
public void EndSession()
{
Current = null;
LocalStorage.RemoveItem("last_user");
}
public void SetSession(string id)
{
var user = GetUserById(id);
if (user == null) throw new Exception("Did not find User in Local Storage");
ApiService.SetBearerToken(user.Token);
Current = user;
LocalStorage.SetItem("last_user", user.Id);
}
}

View file

@ -23,6 +23,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0"/>
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.6" />

View file

@ -1,4 +1,4 @@
@page "/"
@page "/home"
<PageTitle>Home</PageTitle>

View file

@ -1,8 +1,72 @@
@page "/login"
@using Iceshrimp.Frontend.Core.Miscellaneous
@using Iceshrimp.Frontend.Core.Schemas
@using Iceshrimp.Frontend.Core.Services
@using Iceshrimp.Shared.Schemas
@inject ApiService Api
@inject SessionService SessionService
@inject NavigationManager Navigation
<h3>Login</h3>
<div>
<input
@bind="@Username" />
<input
@bind="@Password" />
<button @onclick="Submit" disabled="@Loading">Login</button>
</div>
@if (Loading)
{
<span>Loading!</span>
}
@if (Failure)
{
<span>Authentication Failed</span>
}
<p>A login page is being constructed here.</p>
@code {
private string? Password { get; set; }
private string? Username { get; set; }
private bool Loading { get; set; }
private bool Failure { get; set; }
private async void Submit()
{
Loading = true;
try
{
var res = await Api.Auth.Login(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!
});
SessionService.SetSession(res.User.Id);
Navigation.NavigateTo("/");
break;
case AuthStatusEnum.Guest:
Failure = true;
Loading = false;
break;
case AuthStatusEnum.TwoFactor:
//FixMe: Implement Two Factor
break;
}
}
catch (ApiException)
{
Loading = false;
Failure = true;
}
}
}

View file

View file

@ -0,0 +1,39 @@
@page "/"
@attribute [Authorize]
@using Iceshrimp.Frontend.Core.Services
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@inject SessionService Session
@inject NavigationManager Navigation
<h3>Timeline</h3>
@if (username != null)
{
<span>You are logged in as @username </span>
<AuthorizeView>
<p>Authorization says you are @context.User.Identity!.Name</p>
</AuthorizeView>
<button @onclick="Logout">Logout</button>
}else
{
<span>Not logged in</span>
}
@code {
private string? username;
protected override void OnInitialized()
{
if (Session.Current != null)
{
username = Session.Current.Username;
}
}
public void Logout()
{
Session.EndSession();
Navigation.NavigateTo("/login");
}
}

View file

@ -1,7 +1,9 @@
using Blazored.LocalStorage;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Iceshrimp.Frontend;
using Iceshrimp.Frontend.Core.Services;
using Microsoft.AspNetCore.Components.Authorization;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
@ -10,5 +12,10 @@ builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddSingleton(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddSingleton<ApiClient>();
builder.Services.AddSingleton<ApiService>();
builder.Services.AddSingleton<SessionService>();
builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthStateProvider>();
builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddBlazoredLocalStorageAsSingleton();
await builder.Build().RunAsync();
await builder.Build().RunAsync();