From 5fb95c83058ecf33bdbe0374538d32b2492a91e0 Mon Sep 17 00:00:00 2001 From: Gordon Pedersen Date: Fri, 31 May 2024 23:16:09 +1000 Subject: [PATCH] Added auth. Unfortunately it does not refresh at first login. Have to figure out how to make it refresh --- App.xaml.cs | 2 +- AppShell.xaml | 14 ++++++ AppShell.xaml.cs | 11 +++++ Components/Layout/NavMenu.razor | 48 ++++++++++++++++--- Components/Pages/Home.razor | 4 +- Components/Pages/Login.razor | 29 ++++++++++++ Components/RedirectToLogin.razor | 8 ++++ Components/Routes.razor | 22 +++++++-- Components/_Imports.razor | 2 + CustomAuthenticationStateProvider.cs | 70 ++++++++++++++++++++++++++++ LoginWebViewPage.xaml | 12 +++++ LoginWebViewPage.xaml.cs | 37 +++++++++++++++ MainPage.xaml | 2 +- MainPage.xaml.cs | 12 ++++- MauiProgram.cs | 10 +++- Models/AccountResponseData.cs | 14 ++++++ Models/AddressResponseData.cs | 41 ++++++++++++++++ Models/AddressResponseList.cs | 10 ++++ Models/IOmgLolResponseData.cs | 1 - Models/IOmgLolResponseList.cs | 10 ++++ Models/TokenResponseData.cs | 7 +++ Neighbourhood.omg.lol.csproj | 13 ++++++ RestService.cs | 59 +++++++++++++++++------ 23 files changed, 406 insertions(+), 32 deletions(-) create mode 100644 AppShell.xaml create mode 100644 AppShell.xaml.cs create mode 100644 Components/Pages/Login.razor create mode 100644 Components/RedirectToLogin.razor create mode 100644 CustomAuthenticationStateProvider.cs create mode 100644 LoginWebViewPage.xaml create mode 100644 LoginWebViewPage.xaml.cs create mode 100644 Models/AccountResponseData.cs create mode 100644 Models/AddressResponseData.cs create mode 100644 Models/AddressResponseList.cs create mode 100644 Models/IOmgLolResponseList.cs create mode 100644 Models/TokenResponseData.cs diff --git a/App.xaml.cs b/App.xaml.cs index 451ac1a..d01dad5 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -3,7 +3,7 @@ public App() { InitializeComponent(); - MainPage = new MainPage(); + MainPage = new AppShell(); } } } diff --git a/AppShell.xaml b/AppShell.xaml new file mode 100644 index 0000000..e50bd25 --- /dev/null +++ b/AppShell.xaml @@ -0,0 +1,14 @@ + + + + + + diff --git a/AppShell.xaml.cs b/AppShell.xaml.cs new file mode 100644 index 0000000..c07ae1e --- /dev/null +++ b/AppShell.xaml.cs @@ -0,0 +1,11 @@ +namespace Neighbourhood.omg.lol; + +public partial class AppShell : Shell +{ + public AppShell() + { + InitializeComponent(); + + Routing.RegisterRoute(nameof(LoginWebViewPage), typeof(LoginWebViewPage)); + } +} \ No newline at end of file diff --git a/Components/Layout/NavMenu.razor b/Components/Layout/NavMenu.razor index 6d51817..d64fb0c 100644 --- a/Components/Layout/NavMenu.razor +++ b/Components/Layout/NavMenu.razor @@ -1,18 +1,39 @@ - + +@code { + private string? Name = null; + private List Addresses = new List(); + private string FirstAddress { get => this.Addresses.FirstOrDefault() ?? string.Empty; } + + protected override async Task OnInitializedAsync() { + var state = await AuthStateProvider.GetAuthenticationStateAsync(); + var identity = state.User.Identity; + + Name = identity?.Name ?? string.Empty; + Addresses = state.User.FindFirst("addresses")?.Value?.Split(',')?.ToList() ?? new List(); + } + +} \ No newline at end of file diff --git a/Components/Pages/Home.razor b/Components/Pages/Home.razor index 5850dee..70a1ef0 100644 --- a/Components/Pages/Home.razor +++ b/Components/Pages/Home.razor @@ -1,5 +1,7 @@ @page "/" +@inject CustomAuthenticationStateProvider AuthStateProvider; -

Hello, lol!

+

Hello, lol!

+Logout Welcome to your new app. \ No newline at end of file diff --git a/Components/Pages/Login.razor b/Components/Pages/Login.razor new file mode 100644 index 0000000..8cfe9b6 --- /dev/null +++ b/Components/Pages/Login.razor @@ -0,0 +1,29 @@ +@page "/login" +@using Microsoft.AspNetCore.Components.Authorization +@using System.Security.Claims +@inject NavigationManager navigationManager +@inject AuthenticationStateProvider AuthStateProvider +

Login

+ +@code { + + protected override async Task OnInitializedAsync() { + await checkLogin(); + } + + // protected override async Task OnAfterRenderAsync(bool firstRender) { + // await checkLogin(); + // } + + private async Task checkLogin() { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + + if (user.Identity is not null && user.Identity.IsAuthenticated) { + navigationManager.NavigateTo("/"); + } + else { + await Shell.Current.GoToAsync(nameof(LoginWebViewPage)); + } + } +} diff --git a/Components/RedirectToLogin.razor b/Components/RedirectToLogin.razor new file mode 100644 index 0000000..3c1610c --- /dev/null +++ b/Components/RedirectToLogin.razor @@ -0,0 +1,8 @@ +@inject NavigationManager navigationManager +
Redirecting...
+ +@code { + protected override void OnInitialized() { + navigationManager.NavigateTo("/login"); + } +} diff --git a/Components/Routes.razor b/Components/Routes.razor index 631cf80..9bbebfe 100644 --- a/Components/Routes.razor +++ b/Components/Routes.razor @@ -1,6 +1,18 @@ - - - - - +@using Microsoft.AspNetCore.Components.Authorization + + + + Logging in... + + + + + + + + +

Sorry, there's nothing here.

+
+
+
diff --git a/Components/_Imports.razor b/Components/_Imports.razor index 9fa8d7d..8ecaf89 100644 --- a/Components/_Imports.razor +++ b/Components/_Imports.razor @@ -4,6 +4,8 @@ @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.AspNetCore.Components.Authorization +@using Microsoft.AspNetCore.Authorization @using Microsoft.JSInterop @using Neighbourhood.omg.lol @using Neighbourhood.omg.lol.Components diff --git a/CustomAuthenticationStateProvider.cs b/CustomAuthenticationStateProvider.cs new file mode 100644 index 0000000..24461de --- /dev/null +++ b/CustomAuthenticationStateProvider.cs @@ -0,0 +1,70 @@ +using Microsoft.AspNetCore.Components.Authorization; +using Neighbourhood.omg.lol.Models; +using System.Security.Claims; + +namespace Neighbourhood.omg.lol { + public class CustomAuthenticationStateProvider : AuthenticationStateProvider { + public CustomAuthenticationStateProvider() { + } + + public async Task Login(string token) { + await SecureStorage.SetAsync("accounttoken", token); + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } + + public async Task Logout() { + SecureStorage.Remove("accounttoken"); + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } + + public override async Task GetAuthenticationStateAsync() { + var identity = new ClaimsIdentity(); + try { + var token = await SecureStorage.GetAsync("accounttoken"); + if (token != null) { + var name = await SecureStorage.GetAsync("accountname"); + var email = await SecureStorage.GetAsync("accountemail"); + var addresses = await SecureStorage.GetAsync("accountaddresses"); + + RestService api = new RestService(token); + if (string.IsNullOrEmpty(name) || string.IsNullOrEmpty(email)) { + AccountResponseData? accountInfo = await api.AccountInfo(); + if (accountInfo != null) { + name = accountInfo.Name; + email = accountInfo.Email; + } + } + if (!string.IsNullOrEmpty(name) && !string.IsNullOrEmpty(email)) { + if (string.IsNullOrEmpty(addresses)) { + AddressResponseList? addressList = await api.Addresses(); + List addressStrings = new List(); + if (addressList != null) foreach (var address in addressList) { + if (!address.Expiration.Expired && !string.IsNullOrEmpty(address.Address)) { + addressStrings.Add(address.Address); + } + } + addresses = string.Join(',', addressStrings); + } + if(!string.IsNullOrEmpty(addresses)) { + await SecureStorage.SetAsync("accountname", name); + await SecureStorage.SetAsync("accountemail", email); + await SecureStorage.SetAsync("accountaddresses", addresses); + } + + var claims = new[] { + new Claim(ClaimTypes.Name, name), + new Claim(ClaimTypes.Email, email), + new Claim("addresses", addresses) + }; + identity = new ClaimsIdentity(claims, "Server authentication"); + } + } + } + catch (HttpRequestException ex) { + Console.WriteLine("Request failed:" + ex.ToString()); + } + + return new AuthenticationState(new ClaimsPrincipal(identity)); + } + } +} diff --git a/LoginWebViewPage.xaml b/LoginWebViewPage.xaml new file mode 100644 index 0000000..8a25b62 --- /dev/null +++ b/LoginWebViewPage.xaml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/LoginWebViewPage.xaml.cs b/LoginWebViewPage.xaml.cs new file mode 100644 index 0000000..4ef1ac8 --- /dev/null +++ b/LoginWebViewPage.xaml.cs @@ -0,0 +1,37 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using System.Diagnostics; +using System.Web; + +namespace Neighbourhood.omg.lol; + +public partial class LoginWebViewPage : ContentPage +{ + private AuthenticationStateProvider AuthStateProvider { get; set; } + + public LoginWebViewPage(AuthenticationStateProvider authStateProvider) + { + this.AuthStateProvider = authStateProvider; + InitializeComponent(); + this.loginwebview.Source = "https://home.omg.lol/oauth/authorize?client_id=ea14dafd3e92cbcf93750c35cd81a031&scope=everything&redirect_uri=https://neatnik.net/adam/bucket/omgloloauth/&response_type=code"; + + } + + public async void loginwebview_Navigating(object sender, WebNavigatingEventArgs e) { + if(e.Url.StartsWith("https://neatnik.net/adam/bucket/omgloloauth/")) { + Debug.WriteLine("And here we go..."); + Uri uri = new Uri(e.Url); + var query = HttpUtility.ParseQueryString(uri.Query); + string? code = query.Get("code"); + if (!string.IsNullOrEmpty(code)) { + RestService api = new RestService(); + string? token = await api.OAuth(code); + if (!string.IsNullOrEmpty(token)) { + Debug.WriteLine($"Fuck yeah, a token! {token}"); + await ((CustomAuthenticationStateProvider)this.AuthStateProvider).Login(token); + await Shell.Current.GoToAsync(".."); + } + } + } + } +} \ No newline at end of file diff --git a/MainPage.xaml b/MainPage.xaml index 5caf054..9cfbe95 100644 --- a/MainPage.xaml +++ b/MainPage.xaml @@ -5,7 +5,7 @@ x:Class="Neighbourhood.omg.lol.MainPage" BackgroundColor="{DynamicResource PageBackgroundColor}"> - + diff --git a/MainPage.xaml.cs b/MainPage.xaml.cs index b723721..a670591 100644 --- a/MainPage.xaml.cs +++ b/MainPage.xaml.cs @@ -1,7 +1,17 @@ -namespace Neighbourhood.omg.lol { +using Microsoft.AspNetCore.Components.WebView; +using System.Diagnostics; + +namespace Neighbourhood.omg.lol { public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); } + + private void BlazorUrlLoading(object? sender, UrlLoadingEventArgs e) { + if(e.Url.Host == "home.omg.lol" && e.Url.AbsolutePath == "/oauth/authorize") { + e.UrlLoadingStrategy = UrlLoadingStrategy.CancelLoad; + Shell.Current.GoToAsync(nameof(LoginWebViewPage)); + } + } } } diff --git a/MauiProgram.cs b/MauiProgram.cs index f99c3da..6f3430f 100644 --- a/MauiProgram.cs +++ b/MauiProgram.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.Logging; namespace Neighbourhood.omg.lol { public static class MauiProgram { @@ -11,9 +12,14 @@ namespace Neighbourhood.omg.lol { }); builder.Services.AddMauiBlazorWebView(); + builder.Services.AddTransient(); + + builder.Services.AddAuthorizationCore(); + builder.Services.AddScoped(); + builder.Services.AddScoped(s => s.GetRequiredService()); #if DEBUG - builder.Services.AddBlazorWebViewDeveloperTools(); + builder.Services.AddBlazorWebViewDeveloperTools(); builder.Logging.AddDebug(); #endif diff --git a/Models/AccountResponseData.cs b/Models/AccountResponseData.cs new file mode 100644 index 0000000..17697eb --- /dev/null +++ b/Models/AccountResponseData.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neighbourhood.omg.lol.Models { + public class AccountResponseData : IOmgLolResponseData { + public string Message { get; set; } + public string Email { get; set; } + public string Name { get; set; } + // created, api_key and settings + } +} diff --git a/Models/AddressResponseData.cs b/Models/AddressResponseData.cs new file mode 100644 index 0000000..526d39b --- /dev/null +++ b/Models/AddressResponseData.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neighbourhood.omg.lol.Models { + public class AddressResponseData : IOmgLolResponseData { + public string Address { get; set; } + public string Message { get; set; } + public RegistrationData Registration { get; set; } + public ExpirationData Expiration { get; set; } + + public class TimeData { + public long? UnixEpochTime { get; set; } + public string? Iso8601Time { get; set; } + public string? Rfc2822Time { get; set; } + public string? RelativeTime { get; set; } + public PreferenceData? Preferences { get; set; } + } + + public class RegistrationData : TimeData { + public string? Message { get; set; } + } + + public class ExpirationData : TimeData { + public bool Expired { get; set; } + public bool WillExpire { get; set; } + } + + public class PreferenceData { + public string? IncludeInDirectory { get; set; } + public string? ShowOnDashboard { get; set; } + public StatuslogPreferenceData? Statuslog { get; set; } + } + + public class StatuslogPreferenceData { + public bool? MastodonPosting { get; set; } + } + } +} diff --git a/Models/AddressResponseList.cs b/Models/AddressResponseList.cs new file mode 100644 index 0000000..a0f9f8e --- /dev/null +++ b/Models/AddressResponseList.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neighbourhood.omg.lol.Models { + public class AddressResponseList : List, IOmgLolResponseList { + } +} diff --git a/Models/IOmgLolResponseData.cs b/Models/IOmgLolResponseData.cs index 2c5950b..d126f13 100644 --- a/Models/IOmgLolResponseData.cs +++ b/Models/IOmgLolResponseData.cs @@ -1,5 +1,4 @@ namespace Neighbourhood.omg.lol.Models { public interface IOmgLolResponseData { - string Message { get; set; } } } diff --git a/Models/IOmgLolResponseList.cs b/Models/IOmgLolResponseList.cs new file mode 100644 index 0000000..66c01e9 --- /dev/null +++ b/Models/IOmgLolResponseList.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Neighbourhood.omg.lol.Models { + public interface IOmgLolResponseList : IList, IOmgLolResponseData where T : IOmgLolResponseData { + } +} diff --git a/Models/TokenResponseData.cs b/Models/TokenResponseData.cs new file mode 100644 index 0000000..659d8ec --- /dev/null +++ b/Models/TokenResponseData.cs @@ -0,0 +1,7 @@ +namespace Neighbourhood.omg.lol.Models { + public class TokenResponseData { + public string AccessToken { get; set; } + public string TokenType { get; set; } + public string Scope { get; set; } + } +} diff --git a/Neighbourhood.omg.lol.csproj b/Neighbourhood.omg.lol.csproj index d780ced..49509aa 100644 --- a/Neighbourhood.omg.lol.csproj +++ b/Neighbourhood.omg.lol.csproj @@ -60,10 +60,23 @@ + + + + + + + + + MSBuild:Compile + + + MSBuild:Compile + diff --git a/RestService.cs b/RestService.cs index 5013563..033cdbb 100644 --- a/RestService.cs +++ b/RestService.cs @@ -1,29 +1,31 @@ using Markdig; using Microsoft.AspNetCore.Components; using Neighbourhood.omg.lol.Models; -using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Linq; using System.Net.Http.Json; -using System.Text; using System.Text.Json; -using System.Threading.Tasks; namespace Neighbourhood.omg.lol { public class RestService { HttpClient _client; JsonSerializerOptions _serializerOptions; + public const string BaseUrl = "https://api.omg.lol"; - public RestService() { + public RestService(string? token = null) { _client = new HttpClient(); _serializerOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, WriteIndented = true }; + addToken(token); } - private async Task GetResponse(Uri uri) where T:IOmgLolResponseData { + private void addToken(string? token = null) { + if (token == null) token = Task.Run(() => SecureStorage.GetAsync("accounttoken")).GetAwaiter().GetResult(); + if (token != null) _client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + + private async Task Get(Uri uri) where T:IOmgLolResponseData { T? responseData = default(T); try { HttpResponseMessage response = await _client.GetAsync(uri); @@ -42,20 +44,49 @@ namespace Neighbourhood.omg.lol { } public async Task> StatuslogLatest() { - Uri uri = new Uri("https://api.omg.lol/statuslog/latest"); - return (await GetResponse(uri))?.Statuses ?? new List(); + Uri uri = new Uri($"{BaseUrl}/statuslog/latest"); + return (await Get(uri))?.Statuses ?? new List(); } public async Task> Statuslog(string address) { - Uri uri = new Uri($"https://api.omg.lol/address/{address}/statuses"); - return (await GetResponse(uri))?.Statuses ?? new List(); + Uri uri = new Uri($"{BaseUrl}/address/{address}/statuses"); + return (await Get(uri))?.Statuses ?? new List(); } public async Task StatuslogBio(string address) { - Uri uri = new Uri($"https://api.omg.lol/address/{address}/statuses/bio"); - StatusBioResponseData? responseData = await GetResponse(uri); - Debug.WriteLine(responseData?.Bio); + Uri uri = new Uri($"{BaseUrl}/address/{address}/statuses/bio"); + StatusBioResponseData? responseData = await Get(uri); return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? ""); } + + public async Task AccountInfo() { + Uri uri = new Uri($"{BaseUrl}/account/application/info"); + AccountResponseData? responseData = await Get(uri); + return responseData; + } + + public async Task Addresses() { + Uri uri = new Uri($"{BaseUrl}/account/application/addresses"); + AddressResponseList? responseData = await Get(uri); + return responseData; + } + + public async Task OAuth(string code) { + string? token = null; + Uri uri = new Uri($"{BaseUrl}/oauth/?code={code}&client_id=ea14dafd3e92cbcf93750c35cd81a031&client_secret=ec28b8653f1d98b4eef3f7a20858c43b&redirect_uri=https://neatnik.net/adam/bucket/omgloloauth/&scope=everything"); + try { + HttpResponseMessage response = await _client.GetAsync(uri); + if (response.IsSuccessStatusCode) { + TokenResponseData? responseObj = await response.Content.ReadFromJsonAsync(_serializerOptions); + if (responseObj != null && !string.IsNullOrEmpty(responseObj.AccessToken)) { + token = responseObj.AccessToken; + } + } + } + catch (Exception ex) { + Debug.WriteLine(@"\tERROR {0}", ex.Message); + } + return token; + } } }