From adb9267cd216f948778b5c080234a1911525f7db Mon Sep 17 00:00:00 2001 From: Lilian Date: Mon, 8 Apr 2024 04:27:11 +0200 Subject: [PATCH] [frontend] Add basic login flow and session management. --- Iceshrimp.Frontend/App.razor | 11 ++- Iceshrimp.Frontend/Components/Note.razor | 5 ++ Iceshrimp.Frontend/Components/Note.razor.css | 0 .../Components/RedirectToLogin.razor | 10 +++ Iceshrimp.Frontend/Core/Schemas/StoredUser.cs | 9 +++ .../Core/Services/CustomAuthStateProvider.cs | 22 ++++++ .../Core/Services/SessionService.cs | 71 +++++++++++++++++++ Iceshrimp.Frontend/Iceshrimp.Frontend.csproj | 2 + Iceshrimp.Frontend/Pages/Home.razor | 2 +- Iceshrimp.Frontend/Pages/Login.razor | 66 ++++++++++++++++- Iceshrimp.Frontend/Pages/Login.razor.css | 0 Iceshrimp.Frontend/Pages/Timeline.razor | 39 ++++++++++ Iceshrimp.Frontend/Startup.cs | 9 ++- 13 files changed, 241 insertions(+), 5 deletions(-) create mode 100644 Iceshrimp.Frontend/Components/Note.razor create mode 100644 Iceshrimp.Frontend/Components/Note.razor.css create mode 100644 Iceshrimp.Frontend/Components/RedirectToLogin.razor create mode 100644 Iceshrimp.Frontend/Core/Schemas/StoredUser.cs create mode 100644 Iceshrimp.Frontend/Core/Services/CustomAuthStateProvider.cs create mode 100644 Iceshrimp.Frontend/Core/Services/SessionService.cs create mode 100644 Iceshrimp.Frontend/Pages/Login.razor.css create mode 100644 Iceshrimp.Frontend/Pages/Timeline.razor diff --git a/Iceshrimp.Frontend/App.razor b/Iceshrimp.Frontend/App.razor index cb237fe5..c07d085f 100644 --- a/Iceshrimp.Frontend/App.razor +++ b/Iceshrimp.Frontend/App.razor @@ -1,6 +1,13 @@ - +@using Microsoft.AspNetCore.Components.Authorization +@using Iceshrimp.Frontend.Components + - + + + + + + @* *@ diff --git a/Iceshrimp.Frontend/Components/Note.razor b/Iceshrimp.Frontend/Components/Note.razor new file mode 100644 index 00000000..7d3af006 --- /dev/null +++ b/Iceshrimp.Frontend/Components/Note.razor @@ -0,0 +1,5 @@ +

Note

+ +@code { + +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Components/Note.razor.css b/Iceshrimp.Frontend/Components/Note.razor.css new file mode 100644 index 00000000..e69de29b diff --git a/Iceshrimp.Frontend/Components/RedirectToLogin.razor b/Iceshrimp.Frontend/Components/RedirectToLogin.razor new file mode 100644 index 00000000..4f9f81f9 --- /dev/null +++ b/Iceshrimp.Frontend/Components/RedirectToLogin.razor @@ -0,0 +1,10 @@ +@* FixMe! Make this actually re-direct back to the content! *@ +@inject NavigationManager Navigation +

RedirectToLogin

+@code { + + protected override void OnInitialized() + { + Navigation.NavigateTo("/login"); + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/Schemas/StoredUser.cs b/Iceshrimp.Frontend/Core/Schemas/StoredUser.cs new file mode 100644 index 00000000..490958f9 --- /dev/null +++ b/Iceshrimp.Frontend/Core/Schemas/StoredUser.cs @@ -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; } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/Services/CustomAuthStateProvider.cs b/Iceshrimp.Frontend/Core/Services/CustomAuthStateProvider.cs new file mode 100644 index 00000000..b09745ee --- /dev/null +++ b/Iceshrimp.Frontend/Core/Services/CustomAuthStateProvider.cs @@ -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 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)); + } + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Core/Services/SessionService.cs b/Iceshrimp.Frontend/Core/Services/SessionService.cs new file mode 100644 index 00000000..d11057ca --- /dev/null +++ b/Iceshrimp.Frontend/Core/Services/SessionService.cs @@ -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 Users { get; } + public UserResponse? Current { get; private set; } + + public SessionService(ApiService apiService, ISyncLocalStorageService localStorage) + { + ApiService = apiService; + LocalStorage = localStorage; + Users = LocalStorage.GetItem>("Users") ?? []; + var lastUser = LocalStorage.GetItem("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); + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj b/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj index e9b4e012..2f881acc 100644 --- a/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj +++ b/Iceshrimp.Frontend/Iceshrimp.Frontend.csproj @@ -23,6 +23,8 @@ + + diff --git a/Iceshrimp.Frontend/Pages/Home.razor b/Iceshrimp.Frontend/Pages/Home.razor index ab8366b0..81d071c9 100644 --- a/Iceshrimp.Frontend/Pages/Home.razor +++ b/Iceshrimp.Frontend/Pages/Home.razor @@ -1,4 +1,4 @@ -@page "/" +@page "/home" Home diff --git a/Iceshrimp.Frontend/Pages/Login.razor b/Iceshrimp.Frontend/Pages/Login.razor index 9e933c3e..4ef5f3c3 100644 --- a/Iceshrimp.Frontend/Pages/Login.razor +++ b/Iceshrimp.Frontend/Pages/Login.razor @@ -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

Login

+
+ + + +
+@if (Loading) +{ + Loading! +} +@if (Failure) +{ + Authentication Failed +}

A login page is being constructed here.

@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; + } + } } \ No newline at end of file diff --git a/Iceshrimp.Frontend/Pages/Login.razor.css b/Iceshrimp.Frontend/Pages/Login.razor.css new file mode 100644 index 00000000..e69de29b diff --git a/Iceshrimp.Frontend/Pages/Timeline.razor b/Iceshrimp.Frontend/Pages/Timeline.razor new file mode 100644 index 00000000..2dee28e3 --- /dev/null +++ b/Iceshrimp.Frontend/Pages/Timeline.razor @@ -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 +

Timeline

+@if (username != null) +{ + You are logged in as @username + +

Authorization says you are @context.User.Identity!.Name

+
+ +}else +{ + Not logged in +} + + + +@code { + private string? username; + + protected override void OnInitialized() + { + if (Session.Current != null) + { + username = Session.Current.Username; + } + } + + public void Logout() + { + Session.EndSession(); + Navigation.NavigateTo("/login"); + } +} \ No newline at end of file diff --git a/Iceshrimp.Frontend/Startup.cs b/Iceshrimp.Frontend/Startup.cs index a7c9dc79..6137d5ff 100644 --- a/Iceshrimp.Frontend/Startup.cs +++ b/Iceshrimp.Frontend/Startup.cs @@ -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"); @@ -10,5 +12,10 @@ builder.RootComponents.Add("head::after"); builder.Services.AddSingleton(_ => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddAuthorizationCore(); +builder.Services.AddCascadingAuthenticationState(); +builder.Services.AddBlazoredLocalStorageAsSingleton(); -await builder.Build().RunAsync(); \ No newline at end of file +await builder.Build().RunAsync(); \ No newline at end of file