adding pages, login working and hub connecting

This commit is contained in:
Andy Pack 2023-01-22 22:15:55 +00:00
parent 562c119e18
commit d28f28aae6
Signed by: sarsoo
GPG Key ID: A55BA3536A5E0ED7
17 changed files with 525 additions and 45 deletions

View File

@ -1,5 +1,8 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Selector.MAUI.Data; using Selector.MAUI.Data;
using Selector.MAUI.Services;
using Selector.SignalR;
namespace Selector.MAUI; namespace Selector.MAUI;
@ -16,13 +19,22 @@ public static class MauiProgram
}); });
builder.Services.AddMauiBlazorWebView(); builder.Services.AddMauiBlazorWebView();
builder.Services.AddLogging(o =>
{
//o.AddConsole();
});
#if DEBUG #if DEBUG
builder.Services.AddBlazorWebViewDeveloperTools(); builder.Services.AddBlazorWebViewDeveloperTools();
builder.Logging.AddDebug(); builder.Logging.AddDebug();
#endif #endif
builder.Services.AddHttpClient();
builder.Services.AddSingleton<WeatherForecastService>(); builder.Services.AddTransient<ISelectorNetClient, SelectorNetClient>();
builder.Services.AddSingleton<SessionManager>();
builder.Services.AddSingleton<NowHubClient>();
builder.Services.AddSingleton<NowHubCache>();
return builder.Build(); return builder.Build();
} }

View File

@ -0,0 +1,9 @@
using System;
namespace Selector.MAUI.Models;
public class LoginModel
{
public string Username { get; set; }
public string Password { get; set; }
}

View File

@ -1,17 +0,0 @@
@page "/counter"
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@ -1,8 +1,26 @@
@page "/" @page "/app"
@using Selector.SignalR
@inject NowHubClient nowClient
@inject NowHubCache nowCache
@inject ILogger<Index> logger
<h1>Hello, world!</h1> <h1>Hello, world!</h1>
Welcome to your new app. Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" /> @code {
protected async override Task OnInitializedAsync()
{
if (nowClient.State == Microsoft.AspNetCore.SignalR.Client.HubConnectionState.Disconnected)
{
logger.LogInformation("Starting now hub connection");
await nowClient.StartAsync();
nowCache.BindClient();
await nowClient.OnConnected();
}
}
}

View File

@ -0,0 +1,29 @@
@page "/"
@using Selector.MAUI.Services;
@inject ILogger<Login> logger;
@inject NavigationManager NavManager;
@inject SessionManager sessionManager;
<h1>Loading...</h1>
@code {
protected async override Task OnInitializedAsync()
{
logger.LogInformation("Starting up");
//await sessionManager.LoadUserFromDisk();
if (sessionManager.IsLoggedIn)
{
logger.LogInformation("User logged in, navigating to main app");
NavManager.NavigateTo("/app");
}
else
{
logger.LogInformation("User not logged in, navigating to login");
NavManager.NavigateTo("/login");
}
}
}

View File

@ -0,0 +1,43 @@
@page "/login"
@inject SessionManager session
@inject NavigationManager navigation
<h1>Login</h1>
<p>@toast</p>
<EditForm Model="@loginModel" OnSubmit="@HandleSubmit">
<InputText id="username" @bind-Value="loginModel.Username" />
<InputText type="password" placeholder="Password" @bind-Value="loginModel.Password" />
<button type="submit">Submit</button>
</EditForm>
@code {
private LoginModel loginModel = new();
private string toast = string.Empty;
[Inject]
private ILogger<Login> logger { get; set; }
private async Task HandleSubmit()
{
switch (await session.Authenticate(loginModel.Username, loginModel.Password))
{
case SelectorNetClient.TokenResponseStatus.Malformed:
toast = "Bad Request, Username or Password missing";
break;
case SelectorNetClient.TokenResponseStatus.UserSearchFailed:
toast = "User not found";
break;
case SelectorNetClient.TokenResponseStatus.BadCreds:
toast = "Login failed, try again";
break;
case SelectorNetClient.TokenResponseStatus.OK:
logger.LogInformation("Login succeeded, redirecting");
navigation.NavigateTo("/app");
break;
}
}
}

View File

@ -0,0 +1,16 @@
@page "/now"
@using Selector.SignalR;
<h1>Now</h1>
@if (nowCache?.LastPlaying?.Track is not null)
{
<p role="status">@nowCache.LastPlaying.Track.Name</p>
}
@code {
[Inject]
private NowHubCache nowCache { get; set; }
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
</plist>

View File

@ -31,6 +31,15 @@
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net7.0-ios|AnyCPU'">
<CreatePackage>false</CreatePackage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net7.0-ios|AnyCPU'">
<CreatePackage>false</CreatePackage>
</PropertyGroup>
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst' and '$(Configuration)' == 'Debug'">
<CodeSignEntitlements>Platforms/MacCatalyst/Entitlements.Debug.plist</CodeSignEntitlements>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<!-- App Icon --> <!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" /> <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
@ -48,6 +57,39 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
<!-- <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.2" /> -->
<PackageReference Include="Microsoft.AspNetCore.Components.Forms" Version="7.0.2" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Remove="Services\" />
<None Remove="Microsoft.Extensions.Http" />
<None Remove="System.Net.Http.Json" />
<None Remove="NLog.Extensions.Logging" />
<None Remove="NLog" />
<None Remove="Microsoft.Extensions.Logging.Console" />
<None Remove="Models\" />
<None Remove="Microsoft.AspNetCore.SignalR.Client" />
<None Remove="Microsoft.AspNetCore.Components.Forms" />
<None Remove="Platforms\iOS\Entitlements.plist" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\" />
<Folder Include="Models\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Selector\Selector.csproj" />
<ProjectReference Include="..\Selector.SignalR\Selector.SignalR.csproj" />
</ItemGroup>
<ItemGroup>
<Content Remove="nlog.config" />
</ItemGroup>
<ItemGroup>
<!-- <None Include="nlog.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None> -->
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,110 @@
using System;
using System.Net;
using System.Net.Http.Json;
using Selector.SignalR;
namespace Selector.MAUI.Services;
public interface ISelectorNetClient
{
Task<SelectorNetClient.TokenResponse> GetToken(string username, string password);
Task<SelectorNetClient.TokenResponse> GetToken(string currentKey);
}
public class SelectorNetClient : ISelectorNetClient
{
private readonly HttpClient _client;
private readonly string _baseUrl;
private readonly NowHubClient _nowClient;
public SelectorNetClient(HttpClient client, NowHubClient nowClient)
{
_client = client;
_nowClient = nowClient;
//var baseOverride = Environment.GetEnvironmentVariable("SELECTOR_BASE_URL");
//if (!string.IsNullOrWhiteSpace(baseOverride))
//{
// _baseUrl = baseOverride;
//}
//else
//{
// _baseUrl = "https://selector.sarsoo.xyz";
//}
_baseUrl = "http://localhost:5000";
}
public async Task<TokenResponse> GetToken(string username, string password)
{
ArgumentNullException.ThrowIfNullOrEmpty(username);
ArgumentNullException.ThrowIfNullOrEmpty(password);
var result = await _client.PostAsync(_baseUrl + "/api/auth/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "Username", username },
{ "Password", password }
}));
return FormTokenResponse(result);
}
public async Task<TokenResponse> GetToken(string currentKey)
{
ArgumentNullException.ThrowIfNullOrEmpty(currentKey);
var result = await _client.PostAsync(_baseUrl + "/api/auth/token", new StringContent(string.Empty));
return FormTokenResponse(result);
}
private TokenResponse FormTokenResponse(HttpResponseMessage result)
{
var ret = new TokenResponse();
switch (result.StatusCode)
{
case HttpStatusCode.BadRequest:
ret.Status = TokenResponseStatus.Malformed;
break;
case HttpStatusCode.NotFound:
ret.Status = TokenResponseStatus.UserSearchFailed;
break;
case HttpStatusCode.Unauthorized:
ret.Status = TokenResponseStatus.BadCreds;
break;
case HttpStatusCode.OK:
ret.Status = TokenResponseStatus.OK;
ret.Token = result.Content.ReadFromJsonAsync<TokenNetworkResponse>().Result.Token;
_nowClient.Token = ret.Token;
break;
default:
break;
}
return ret;
}
public class TokenResponse
{
public string Token { get; set; }
public TokenResponseStatus Status { get; set; }
}
public class TokenNetworkResponse
{
public string Token { get; set; }
}
public enum TokenResponseStatus
{
Malformed, UserSearchFailed, BadCreds, OK
}
private class TokenModel
{
public string Username { get; set; }
public string Password { get; set; }
}
}

View File

@ -0,0 +1,84 @@
using System;
using Microsoft.Extensions.Logging;
namespace Selector.MAUI.Services;
public class SessionManager
{
private const string jwt_keychain_key = "last_jwt_key";
private string lastStoredKey;
private DateTime lastRefresh;
private readonly ISelectorNetClient _selectorNetClient;
private readonly ILogger<SessionManager> _logger;
public SessionManager(ISelectorNetClient selectorNetClient, ILogger<SessionManager> logger)
{
_selectorNetClient = selectorNetClient;
_logger = logger;
}
public bool IsLoggedIn => !string.IsNullOrWhiteSpace(lastStoredKey);
public async Task LoadUserFromDisk()
{
var lastToken = await SecureStorage.Default.GetAsync(jwt_keychain_key);
lastStoredKey = lastToken;
if(!string.IsNullOrWhiteSpace(lastToken))
{
await Authenticate();
}
}
public async Task<SelectorNetClient.TokenResponseStatus> Authenticate(string username, string password)
{
_logger.LogDebug("Making login token request");
var tokenResponse = await _selectorNetClient.GetToken(username, password);
return await HandleTokenResponse(tokenResponse);
}
public async Task<SelectorNetClient.TokenResponseStatus> Authenticate()
{
_logger.LogDebug("Making token request with current key");
var tokenResponse = await _selectorNetClient.GetToken(lastStoredKey);
return await HandleTokenResponse(tokenResponse);
}
private async Task<SelectorNetClient.TokenResponseStatus> HandleTokenResponse(SelectorNetClient.TokenResponse tokenResponse)
{
switch (tokenResponse.Status)
{
case SelectorNetClient.TokenResponseStatus.OK:
_logger.LogInformation("Token response ok");
lastStoredKey = tokenResponse.Token;
lastRefresh = DateTime.Now;
//await SecureStorage.Default.SetAsync(jwt_keychain_key, lastStoredKey);
break;
case SelectorNetClient.TokenResponseStatus.Malformed:
_logger.LogInformation("Token request failed, missing username or password");
break;
case SelectorNetClient.TokenResponseStatus.UserSearchFailed:
_logger.LogInformation("Token request failed, no user by that name");
break;
case SelectorNetClient.TokenResponseStatus.BadCreds:
_logger.LogInformation("Token request failed, bad password");
break;
default:
throw new NotImplementedException();
}
return tokenResponse.Status;
}
}

View File

@ -1,10 +1,15 @@
@inherits LayoutComponentBase @using Selector.MAUI.Services;
@inherits LayoutComponentBase
@inject SessionManager session
<div class="page"> <div class="page">
<div class="sidebar"> @if (session.IsLoggedIn)
<NavMenu /> {
</div> <div class="sidebar">
<NavMenu />
</div>
}
<main> <main>
<div class="top-row px-4"> <div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a> <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>

View File

@ -15,13 +15,8 @@
</NavLink> </NavLink>
</div> </div>
<div class="nav-item px-3"> <div class="nav-item px-3">
<NavLink class="nav-link" href="counter"> <NavLink class="nav-link" href="now">
<span class="oi oi-plus" aria-hidden="true"></span> Counter <span class="oi oi-plus" aria-hidden="true"></span> Now
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink> </NavLink>
</div> </div>
</nav> </nav>

View File

@ -1,4 +1,5 @@
@using System.Net.Http @using System.Net.Http
@using Microsoft.Extensions.Logging
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web
@ -6,4 +7,5 @@
@using Microsoft.JSInterop @using Microsoft.JSInterop
@using Selector.MAUI @using Selector.MAUI
@using Selector.MAUI.Shared @using Selector.MAUI.Shared
@using Selector.MAUI.Models
@using Selector.MAUI.Services;

View File

@ -7,26 +7,37 @@ public abstract class BaseSignalRClient: IAsyncDisposable
{ {
private readonly string _baseUrl; private readonly string _baseUrl;
protected HubConnection hubConnection; protected HubConnection hubConnection;
public string Token { get; set; }
public BaseSignalRClient(string path) public BaseSignalRClient(string path, string token)
{ {
var baseOverride = Environment.GetEnvironmentVariable("SELECTOR_BASE_URL"); //var baseOverride = Environment.GetEnvironmentVariable("SELECTOR_BASE_URL");
if (!string.IsNullOrWhiteSpace(baseOverride)) //if (!string.IsNullOrWhiteSpace(baseOverride))
{ //{
_baseUrl = baseOverride; // _baseUrl = baseOverride;
} //}
else //else
{ //{
_baseUrl = "https://selector.sarsoo.xyz"; // _baseUrl = "https://selector.sarsoo.xyz";
} //}
_baseUrl = "http://localhost:5000";
hubConnection = new HubConnectionBuilder() hubConnection = new HubConnectionBuilder()
.WithUrl(_baseUrl + "/" + path) .WithUrl(_baseUrl + "/" + path, options =>
{
options.AccessTokenProvider = () =>
{
return Task.FromResult(Token);
};
})
.WithAutomaticReconnect() .WithAutomaticReconnect()
.Build(); .Build();
} }
public HubConnectionState State => hubConnection.State;
public ValueTask DisposeAsync() public ValueTask DisposeAsync()
{ {
return ((IAsyncDisposable)hubConnection).DisposeAsync(); return ((IAsyncDisposable)hubConnection).DisposeAsync();

View File

@ -0,0 +1,93 @@
using System;
using Microsoft.Extensions.Logging;
using SpotifyAPI.Web;
namespace Selector.SignalR;
public class NowHubCache
{
private readonly NowHubClient _connection;
private readonly ILogger<NowHubCache> logger;
public TrackAudioFeatures LastFeature { get; private set; }
public List<ICard> LastCards { get; private set; } = new();
private readonly object updateLock = new();
public PlayCount LastPlayCount { get; private set; }
public CurrentlyPlayingDTO LastPlaying { get; private set; }
public NowHubCache(NowHubClient connection, ILogger<NowHubCache> logger)
{
_connection = connection;
this.logger = logger;
}
public void BindClient()
{
_connection.OnNewAudioFeature(af =>
{
lock (updateLock)
{
logger.LogInformation("New audio features received: {0}", af);
LastFeature = af;
}
});
_connection.OnNewCard(c =>
{
lock(updateLock)
{
logger.LogInformation("New card received: {0}", c);
LastCards.Add(c);
}
});
_connection.OnNewPlayCount(pc =>
{
lock (updateLock)
{
logger.LogInformation("New play count received: {0}", pc);
LastPlayCount = pc;
}
});
_connection.OnNewPlaying(async np =>
{
try
{
lock (updateLock)
{
logger.LogInformation("New now playing recieved: {0}", np);
LastPlaying = np;
LastCards.Clear();
}
if (LastPlaying?.Track is not null)
{
if (!string.IsNullOrWhiteSpace(LastPlaying.Track.Id))
{
await _connection.SendAudioFeatures(LastPlaying.Track.Id);
}
await _connection.SendPlayCount(
LastPlaying.Track.Name,
LastPlaying.Track.Artists.FirstOrDefault()?.Name,
LastPlaying.Track.Album?.Name,
LastPlaying.Track.Album?.Artists.FirstOrDefault()?.Name
);
await _connection.SendFacts(
LastPlaying.Track.Name,
LastPlaying.Track.Artists.FirstOrDefault()?.Name,
LastPlaying.Track.Album?.Name,
LastPlaying.Track.Album?.Artists.FirstOrDefault()?.Name
);
}
}catch(Exception e)
{
logger.LogError(e, "Error while handling new now playing");
}
});
}
}

View File

@ -13,7 +13,7 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable
private List<IDisposable> NewCardCallbacks = new(); private List<IDisposable> NewCardCallbacks = new();
private bool disposedValue; private bool disposedValue;
public NowHubClient(): base("nowhub") public NowHubClient(string token = null): base("nowhub", token)
{ {
} }
@ -37,6 +37,26 @@ public class NowHubClient: BaseSignalRClient, INowPlayingHub, IDisposable
NewCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action)); NewCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action));
} }
public void OnNewPlaying(Func<CurrentlyPlayingDTO, Task> action)
{
NewPlayingCallbacks.Add(hubConnection.On(nameof(OnNewPlaying), action));
}
public void OnNewAudioFeature(Func<TrackAudioFeatures, Task> action)
{
NewAudioFeatureCallbacks.Add(hubConnection.On(nameof(OnNewAudioFeature), action));
}
public void OnNewPlayCount(Func<PlayCount, Task> action)
{
NewPlayCountCallbacks.Add(hubConnection.On(nameof(OnNewPlayCount), action));
}
public void OnNewCard(Func<ICard, Task> action)
{
NewCardCallbacks.Add(hubConnection.On(nameof(OnNewCard), action));
}
public Task OnConnected() public Task OnConnected()
{ {
return hubConnection.InvokeAsync(nameof(OnConnected)); return hubConnection.InvokeAsync(nameof(OnConnected));