Added auth.

Unfortunately it does not refresh at first login. Have to figure out how to make it refresh
This commit is contained in:
Gordon Pedersen 2024-05-31 23:16:09 +10:00
parent ebd109d850
commit 5fb95c8305
23 changed files with 406 additions and 32 deletions

View file

@ -3,7 +3,7 @@
public App() {
InitializeComponent();
MainPage = new MainPage();
MainPage = new AppShell();
}
}
}

14
AppShell.xaml Normal file
View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="Neighbourhood.omg.lol.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Neighbourhood.omg.lol"
Shell.FlyoutBehavior="Disabled"
Title="omg.lol Neighbourhood">
<ShellContent
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>

11
AppShell.xaml.cs Normal file
View file

@ -0,0 +1,11 @@
namespace Neighbourhood.omg.lol;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
Routing.RegisterRoute(nameof(LoginWebViewPage), typeof(LoginWebViewPage));
}
}

View file

@ -1,18 +1,39 @@
<nav class="left drawer l">
@inject CustomAuthenticationStateProvider AuthStateProvider;
<nav class="left drawer l">
<header>
<nav>
<img src="https://cdn.cache.lol/img/prami.svg" class="circle">
<h6>Hey there</h6>
<AuthorizeView>
<Authorized>
<img class="circle medium" src="https://profiles.cache.lol/@FirstAddress/picture" alt="@FirstAddress" />
<div>Hey, @Name.</div>
</Authorized>
<NotAuthorized>
<img src="https://cdn.cache.lol/img/prami.svg" class="medium">
<div>
Hey there. <br/>
<a href="/login">Login?</a>
</div>
</NotAuthorized>
</AuthorizeView>
</nav>
</header>
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<NavLink class="row nav-link" href="" Match="NavLinkMatch.All">
<i class="fa-solid fa-fw fa-home"></i>
<div>Home</div>
</NavLink>
<NavLink class="nav-link" href="/statuslog/latest">
<NavLink class="row nav-link" href="/statuslog/latest">
<i><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500"><path fill="#4dabf7" d="M250 450c-38.388 0-76.775-14.646-106.066-43.934l-100-100c-58.579-58.58-58.579-153.553 0-212.132C100.534 37.336 191.105 35.421 250 88.191c58.898-52.768 149.47-50.853 206.066 5.743 58.58 58.58 58.58 153.553 0 212.132l-100 100C326.778 435.354 288.39 450 250 450" /><path fill="#228be6" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M220.52 176.634a11.792 11.792 0 1 1-23.586 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0" /><path fill="#1864ab" stroke="#1864ab" stroke-miterlimit="10" stroke-width="11.32074" d="M303.066 176.634a11.792 11.792 0 1 1-23.585 0 11.792 11.792 0 0 1 23.585 0Z" /><path fill="#228be6" stroke="#461036" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /><path fill="#1971c2" d="M438.68 212.011c0-32.564-26.399-58.962-58.963-58.962s-58.962 26.398-58.962 58.962 26.398 58.963 58.962 58.963 58.962-26.399 58.962-58.963m-259.433 0c0-32.564-26.398-58.962-58.962-58.962S61.32 179.447 61.32 212.011s26.398 58.963 58.963 58.963 58.962-26.399 58.962-58.963" /><path fill="#4dabf7" stroke="#1864ab" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="18.8679" d="M208.75 223.817c18.875 30.802 63.626 30.802 82.501 0" /></svg></i>
<div>Statuslog</div>
</NavLink>
<div class="row max"></div>
<footer>
<AuthorizeView>
<Authorized><a class="button small" @onclick='() => AuthStateProvider.Logout()'>Logout</a></Authorized>
<NotAuthorized><a class="button small" href="/login">Login</a></NotAuthorized>
</AuthorizeView>
</footer>
</nav>
<nav class="left m">
@ -39,3 +60,18 @@
<div>Statuslog</div>
</NavLink>
</nav>
@code {
private string? Name = null;
private List<string> Addresses = new List<string>();
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<string>();
}
}

View file

@ -1,5 +1,7 @@
@page "/"
@inject CustomAuthenticationStateProvider AuthStateProvider;
<h1>Hello, lol!</h1>
<h1><i data-emoji="👋"></i> Hello, lol!</h1>
<a @onclick='() => AuthStateProvider.Logout()'>Logout</a>
Welcome to your new app.

View file

@ -0,0 +1,29 @@
@page "/login"
@using Microsoft.AspNetCore.Components.Authorization
@using System.Security.Claims
@inject NavigationManager navigationManager
@inject AuthenticationStateProvider AuthStateProvider
<h3>Login</h3>
@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));
}
}
}

View file

@ -0,0 +1,8 @@
@inject NavigationManager navigationManager
<div class="loader loader-bouncing"><span>Redirecting...</span></div>
@code {
protected override void OnInitialized() {
navigationManager.NavigateTo("/login");
}
}

View file

@ -1,6 +1,18 @@
<Router AppAssembly="@typeof(MauiProgram).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
@using Microsoft.AspNetCore.Components.Authorization
<Router AppAssembly="@typeof(MauiProgram).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)">
<Authorizing>Logging in...</Authorizing>
<NotAuthorized><RedirectToLogin /></NotAuthorized>
</AuthorizeRouteView>
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(Layout.MainLayout)">
<img data-emoji="🦒" />
<p>Sorry, there's nothing here.</p>
</LayoutView>
</CascadingAuthenticationState>
</NotFound>
</Router>

View file

@ -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

View file

@ -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<AuthenticationState> 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<string> addressStrings = new List<string>();
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));
}
}
}

12
LoginWebViewPage.xaml Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Neighbourhood.omg.lol.LoginWebViewPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<Grid>
<WebView x:Name="loginwebview"
Navigating="loginwebview_Navigating"
VerticalOptions="Fill"
HorizontalOptions="Fill" />
</Grid>
</ContentPage>

37
LoginWebViewPage.xaml.cs Normal file
View file

@ -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("..");
}
}
}
}
}

View file

@ -5,7 +5,7 @@
x:Class="Neighbourhood.omg.lol.MainPage"
BackgroundColor="{DynamicResource PageBackgroundColor}">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html" UrlLoading="BlazorUrlLoading">
<BlazorWebView.RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:Components.Routes}" />
</BlazorWebView.RootComponents>

View file

@ -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));
}
}
}
}

View file

@ -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<LoginWebViewPage>();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomAuthenticationStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthenticationStateProvider>());
#if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug();
#endif

View file

@ -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
}
}

View file

@ -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; }
}
}
}

View file

@ -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<AddressResponseData>, IOmgLolResponseList<AddressResponseData> {
}
}

View file

@ -1,5 +1,4 @@
namespace Neighbourhood.omg.lol.Models {
public interface IOmgLolResponseData {
string Message { get; set; }
}
}

View file

@ -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<T> : IList<T>, IOmgLolResponseData where T : IOmgLolResponseData {
}
}

View file

@ -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; }
}
}

View file

@ -60,10 +60,23 @@
<ItemGroup>
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.AspNetCore.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="8.0.6" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
<PackageReference Include="Microsoft.Maui.Essentials" Version="8.0.40" />
</ItemGroup>
<ItemGroup>
<MauiXaml Update="AppShell.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="LoginWebViewPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
</ItemGroup>
</Project>

View file

@ -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<T?> GetResponse<T>(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<T?> Get<T>(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<List<Status>> StatuslogLatest() {
Uri uri = new Uri("https://api.omg.lol/statuslog/latest");
return (await GetResponse<StatusResponseData>(uri))?.Statuses ?? new List<Status>();
Uri uri = new Uri($"{BaseUrl}/statuslog/latest");
return (await Get<StatusResponseData>(uri))?.Statuses ?? new List<Status>();
}
public async Task<List<Status>> Statuslog(string address) {
Uri uri = new Uri($"https://api.omg.lol/address/{address}/statuses");
return (await GetResponse<StatusResponseData>(uri))?.Statuses ?? new List<Status>();
Uri uri = new Uri($"{BaseUrl}/address/{address}/statuses");
return (await Get<StatusResponseData>(uri))?.Statuses ?? new List<Status>();
}
public async Task<MarkupString> StatuslogBio(string address) {
Uri uri = new Uri($"https://api.omg.lol/address/{address}/statuses/bio");
StatusBioResponseData? responseData = await GetResponse<StatusBioResponseData>(uri);
Debug.WriteLine(responseData?.Bio);
Uri uri = new Uri($"{BaseUrl}/address/{address}/statuses/bio");
StatusBioResponseData? responseData = await Get<StatusBioResponseData>(uri);
return (MarkupString)Markdown.ToHtml(responseData?.Bio ?? "");
}
public async Task<AccountResponseData?> AccountInfo() {
Uri uri = new Uri($"{BaseUrl}/account/application/info");
AccountResponseData? responseData = await Get<AccountResponseData>(uri);
return responseData;
}
public async Task<AddressResponseList?> Addresses() {
Uri uri = new Uri($"{BaseUrl}/account/application/addresses");
AddressResponseList? responseData = await Get<AddressResponseList>(uri);
return responseData;
}
public async Task<string?> 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<TokenResponseData>(_serializerOptions);
if (responseObj != null && !string.IsNullOrEmpty(responseObj.AccessToken)) {
token = responseObj.AccessToken;
}
}
}
catch (Exception ex) {
Debug.WriteLine(@"\tERROR {0}", ex.Message);
}
return token;
}
}
}