mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-02-04 10:53:31 +00:00
Workshop - Added login UI
This commit is contained in:
parent
7eccdf079a
commit
990f55b7c5
@ -5,9 +5,11 @@ using System.Reactive.Disposables;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
using Flurl.Http;
|
using Flurl.Http;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
@ -16,21 +18,24 @@ namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
|||||||
|
|
||||||
public class CurrentUserViewModel : ActivatableViewModelBase
|
public class CurrentUserViewModel : ActivatableViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
|
||||||
private readonly IAuthenticationService _authenticationService;
|
private readonly IAuthenticationService _authenticationService;
|
||||||
private bool _loading = true;
|
private readonly ILogger _logger;
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
private Bitmap? _avatar;
|
private Bitmap? _avatar;
|
||||||
private string? _email;
|
private string? _email;
|
||||||
|
private bool _loading = true;
|
||||||
private string? _name;
|
private string? _name;
|
||||||
private string? _userId;
|
private string? _userId;
|
||||||
|
|
||||||
public CurrentUserViewModel(ILogger logger, IAuthenticationService authenticationService)
|
public CurrentUserViewModel(ILogger logger, IAuthenticationService authenticationService, IWindowService windowService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_authenticationService = authenticationService;
|
_authenticationService = authenticationService;
|
||||||
|
_windowService = windowService;
|
||||||
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
|
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
|
||||||
|
|
||||||
this.WhenActivated(d => ReactiveCommand.CreateFromTask(ExecuteAutoLogin).Execute().Subscribe().DisposeWith(d));
|
this.WhenActivated(d => ReactiveCommand.CreateFromTask(ExecuteAutoLogin).Execute().Subscribe().DisposeWith(d));
|
||||||
|
this.WhenActivated(d => _authenticationService.IsLoggedIn.Subscribe(_ => LoadCurrentUser().DisposeWith(d)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Loading
|
public bool Loading
|
||||||
@ -72,8 +77,13 @@ public class CurrentUserViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
private async Task ExecuteLogin(CancellationToken cancellationToken)
|
private async Task ExecuteLogin(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _authenticationService.Login();
|
ContentDialogResult result = await _windowService.CreateContentDialog()
|
||||||
await LoadCurrentUser();
|
.WithViewModel(out WorkshopLoginViewModel _)
|
||||||
|
.WithTitle("Workshop login")
|
||||||
|
.ShowAsync();
|
||||||
|
|
||||||
|
if (result == ContentDialogResult.Primary)
|
||||||
|
await LoadCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExecuteAutoLogin(CancellationToken cancellationToken)
|
private async Task ExecuteAutoLogin(CancellationToken cancellationToken)
|
||||||
@ -95,21 +105,26 @@ public class CurrentUserViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
private async Task LoadCurrentUser()
|
private async Task LoadCurrentUser()
|
||||||
{
|
{
|
||||||
if (!_authenticationService.IsLoggedIn)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UserId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
UserId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
||||||
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
||||||
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
||||||
|
|
||||||
if (UserId != null)
|
if (UserId != null)
|
||||||
|
{
|
||||||
await LoadAvatar(UserId);
|
await LoadAvatar(UserId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Avatar?.Dispose();
|
||||||
|
Avatar = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAvatar(string userId)
|
private async Task LoadAvatar(string userId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Avatar?.Dispose();
|
||||||
Avatar = new Bitmap(await $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}".GetStreamAsync());
|
Avatar = new Bitmap(await $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}".GetStreamAsync());
|
||||||
}
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
xmlns:currentUser="clr-namespace:Artemis.UI.Screens.Workshop.CurrentUser"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.CurrentUser.WorkshopLoginView"
|
||||||
|
x:DataType="currentUser:WorkshopLoginViewModel">
|
||||||
|
<StackPanel Width="400">
|
||||||
|
<TextBlock TextWrapping="Wrap">
|
||||||
|
In order to login you must enter your Artemis credentials in a browser.
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap" Margin="0 15">
|
||||||
|
If you do not have an account yet you can create one in the browser too. Alternatively you can log in with Google or Discord.
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<ProgressBar IsIndeterminate="True" IsVisible="{CompiledBinding Code, Converter={x:Static StringConverters.IsNullOrEmpty}}"></ProgressBar>
|
||||||
|
<StackPanel IsVisible="{CompiledBinding Code, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||||
|
<TextBlock TextWrapping="Wrap">When logging in please enter the the code below</TextBlock>
|
||||||
|
<StackPanel Margin="0 10" Orientation="Horizontal" HorizontalAlignment="Center" Spacing="5">
|
||||||
|
<TextBox Width="115" IsReadOnly="True" Text="{CompiledBinding Code}" />
|
||||||
|
<Button Width="32" Height="32" ToolTip.Tip="Copy code to clipboard" Command="{CompiledBinding CopyToClipboard}">
|
||||||
|
<avalonia:MaterialIcon Kind="ContentCopy"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Button Width="152" HorizontalAlignment="Center" Command="{CompiledBinding OpenBrowser}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<avalonia:MaterialIcon Kind="ExternalLink" />
|
||||||
|
<TextBlock>Open browser</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
|
||||||
|
public partial class WorkshopLoginView : ReactiveUserControl<WorkshopLoginViewModel>
|
||||||
|
{
|
||||||
|
public WorkshopLoginView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,85 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using FluentAvalonia.UI.Controls;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
|
||||||
|
public class WorkshopLoginViewModel : ContentDialogViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private readonly INotificationService _notificationService;
|
||||||
|
private readonly CancellationTokenSource _cts;
|
||||||
|
private ObservableAsPropertyHelper<string?>? _deviceCode;
|
||||||
|
private string? _uri;
|
||||||
|
|
||||||
|
public WorkshopLoginViewModel(IAuthenticationService authenticationService, INotificationService notificationService)
|
||||||
|
{
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
_notificationService = notificationService;
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
OpenBrowser = ReactiveCommand.Create(ExecuteOpenBrowser);
|
||||||
|
CopyToClipboard = ReactiveCommand.CreateFromTask(ExecuteCopyToClipboard);
|
||||||
|
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.InvokeAsync(Login);
|
||||||
|
|
||||||
|
_deviceCode = _authenticationService.UserCode
|
||||||
|
.Select(uc => uc != null ? string.Join("-", string.Join("-", uc.Chunk(3).Select(c => new string(c)))) : null)
|
||||||
|
.ToProperty(this, vm => vm.Code)
|
||||||
|
.DisposeWith(d);
|
||||||
|
_authenticationService.VerificationUri.Subscribe(u => _uri = u).DisposeWith(d);
|
||||||
|
Disposable.Create(_cts, cts => cts.Cancel()).DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> OpenBrowser { get; }
|
||||||
|
public ReactiveCommand<Unit, Unit> CopyToClipboard { get; }
|
||||||
|
|
||||||
|
public string? Code => _deviceCode?.Value;
|
||||||
|
|
||||||
|
private async Task Login()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool result = await _authenticationService.Login(_cts.Token);
|
||||||
|
if (result)
|
||||||
|
_notificationService.CreateNotification().WithTitle("Login succeeded").WithSeverity(NotificationSeverity.Success).Show();
|
||||||
|
ContentDialog?.Hide(result ? ContentDialogResult.Primary : ContentDialogResult.Secondary);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
if (e is not TaskCanceledException)
|
||||||
|
_notificationService.CreateNotification().WithTitle("Login failed").WithMessage(e.Message).WithSeverity(NotificationSeverity.Error).Show();
|
||||||
|
ContentDialog?.Hide(ContentDialogResult.Secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteOpenBrowser()
|
||||||
|
{
|
||||||
|
if (_uri == null)
|
||||||
|
return;
|
||||||
|
Utilities.OpenUrl(_uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ExecuteCopyToClipboard()
|
||||||
|
{
|
||||||
|
if (Code == null)
|
||||||
|
return;
|
||||||
|
await Shared.UI.Clipboard.SetTextAsync(Code);
|
||||||
|
_notificationService.CreateNotification().WithMessage("Code copied to clipboard.").Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
@ -12,29 +13,31 @@ namespace Artemis.WebClient.Workshop.Services;
|
|||||||
|
|
||||||
public interface IAuthenticationService : IProtectedArtemisService
|
public interface IAuthenticationService : IProtectedArtemisService
|
||||||
{
|
{
|
||||||
bool IsLoggedIn { get; }
|
IObservable<bool> IsLoggedIn { get; }
|
||||||
string? UserCode { get; }
|
IObservable<string?> UserCode { get; }
|
||||||
|
IObservable<string?> VerificationUri { get; }
|
||||||
ReadOnlyObservableCollection<Claim> Claims { get; }
|
ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||||
|
|
||||||
Task<string?> GetBearer();
|
Task<string?> GetBearer();
|
||||||
Task<bool> AutoLogin();
|
Task<bool> AutoLogin();
|
||||||
Task<bool> Login();
|
Task<bool> Login(CancellationToken cancellationToken);
|
||||||
void Logout();
|
void Logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class AuthenticationService : CorePropertyChanged, IAuthenticationService
|
internal class AuthenticationService : CorePropertyChanged, IAuthenticationService
|
||||||
{
|
{
|
||||||
private const string CLIENT_ID = "artemis.desktop";
|
private const string CLIENT_ID = "artemis.desktop";
|
||||||
|
private readonly IAuthenticationRepository _authenticationRepository;
|
||||||
|
private readonly SemaphoreSlim _bearerLock = new(1);
|
||||||
|
private readonly ObservableCollection<Claim> _claims = new();
|
||||||
|
|
||||||
private readonly IDiscoveryCache _discoveryCache;
|
private readonly IDiscoveryCache _discoveryCache;
|
||||||
private readonly IAuthenticationRepository _authenticationRepository;
|
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly ObservableCollection<Claim> _claims = new();
|
private readonly BehaviorSubject<bool> _isLoggedInSubject = new(false);
|
||||||
private readonly SemaphoreSlim _bearerLock = new(1);
|
private readonly BehaviorSubject<string?> _userCodeSubject = new(null);
|
||||||
|
private readonly BehaviorSubject<string?> _verificationUriSubject = new(null);
|
||||||
|
|
||||||
private AuthenticationToken? _token;
|
private AuthenticationToken? _token;
|
||||||
private string? _userCode;
|
|
||||||
private bool _isLoggedIn;
|
|
||||||
|
|
||||||
public AuthenticationService(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
public AuthenticationService(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
||||||
{
|
{
|
||||||
@ -45,102 +48,6 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
Claims = new ReadOnlyObservableCollection<Claim>(_claims);
|
Claims = new ReadOnlyObservableCollection<Claim>(_claims);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool IsLoggedIn
|
|
||||||
{
|
|
||||||
get => _isLoggedIn;
|
|
||||||
private set => SetAndNotify(ref _isLoggedIn, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string? UserCode
|
|
||||||
{
|
|
||||||
get => _userCode;
|
|
||||||
private set => SetAndNotify(ref _userCode, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public ReadOnlyObservableCollection<Claim> Claims { get; }
|
|
||||||
|
|
||||||
public async Task<string?> GetBearer()
|
|
||||||
{
|
|
||||||
await _bearerLock.WaitAsync();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// If not logged in, attempt to auto login first
|
|
||||||
if (!IsLoggedIn)
|
|
||||||
await AutoLogin();
|
|
||||||
|
|
||||||
if (_token == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
// If the token is expiring, refresh it
|
|
||||||
if (_token.Expired && !await UseRefreshToken(_token.RefreshToken))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return _token.AccessToken;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_bearerLock.Release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<bool> AutoLogin()
|
|
||||||
{
|
|
||||||
if (IsLoggedIn)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
string? refreshToken = _authenticationRepository.GetRefreshToken();
|
|
||||||
if (refreshToken == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return await UseRefreshToken(refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<bool> Login()
|
|
||||||
{
|
|
||||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
|
||||||
HttpClient client = _httpClientFactory.CreateClient();
|
|
||||||
DeviceAuthorizationResponse response = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest
|
|
||||||
{
|
|
||||||
Address = disco.DeviceAuthorizationEndpoint,
|
|
||||||
ClientId = CLIENT_ID,
|
|
||||||
Scope = "openid profile email offline_access api"
|
|
||||||
});
|
|
||||||
if (response.IsError)
|
|
||||||
throw new ArtemisWebClientException("Failed to request device authorization: " + response.Error);
|
|
||||||
if (response.DeviceCode == null)
|
|
||||||
throw new ArtemisWebClientException("Failed to request device authorization: Got no device code");
|
|
||||||
|
|
||||||
DateTimeOffset timeout = DateTimeOffset.UtcNow.AddSeconds(response.ExpiresIn ?? 1800);
|
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo {FileName = response.VerificationUriComplete, UseShellExecute = true});
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(response.Interval));
|
|
||||||
while (DateTimeOffset.UtcNow < timeout)
|
|
||||||
{
|
|
||||||
bool success = await AttemptRequestDeviceToken(client, response.DeviceCode);
|
|
||||||
if (success)
|
|
||||||
return true;
|
|
||||||
await Task.Delay(TimeSpan.FromSeconds(response.Interval));
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Logout()
|
|
||||||
{
|
|
||||||
_token = null;
|
|
||||||
_claims.Clear();
|
|
||||||
_authenticationRepository.SetRefreshToken(null);
|
|
||||||
|
|
||||||
IsLoggedIn = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<DiscoveryDocumentResponse> GetDiscovery()
|
private async Task<DiscoveryDocumentResponse> GetDiscovery()
|
||||||
{
|
{
|
||||||
DiscoveryDocumentResponse disco = await _discoveryCache.GetAsync();
|
DiscoveryDocumentResponse disco = await _discoveryCache.GetAsync();
|
||||||
@ -150,7 +57,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
return disco;
|
return disco;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> AttemptRequestDeviceToken(HttpClient client, string deviceCode)
|
private async Task<bool> AttemptRequestDeviceToken(HttpClient client, string deviceCode, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||||
TokenResponse response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest
|
TokenResponse response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest
|
||||||
@ -158,7 +65,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
Address = disco.TokenEndpoint,
|
Address = disco.TokenEndpoint,
|
||||||
ClientId = CLIENT_ID,
|
ClientId = CLIENT_ID,
|
||||||
DeviceCode = deviceCode
|
DeviceCode = deviceCode
|
||||||
});
|
}, cancellationToken);
|
||||||
|
|
||||||
if (response.IsError)
|
if (response.IsError)
|
||||||
{
|
{
|
||||||
@ -186,14 +93,14 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
foreach (Claim responseClaim in token.Claims)
|
foreach (Claim responseClaim in token.Claims)
|
||||||
_claims.Add(responseClaim);
|
_claims.Add(responseClaim);
|
||||||
|
|
||||||
IsLoggedIn = true;
|
_isLoggedInSubject.OnNext(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> UseRefreshToken(string refreshToken)
|
private async Task<bool> UseRefreshToken(string refreshToken)
|
||||||
{
|
{
|
||||||
DiscoveryDocumentResponse disco = await GetDiscovery();
|
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||||
HttpClient client = _httpClientFactory.CreateClient();
|
HttpClient client = _httpClientFactory.CreateClient();
|
||||||
TokenResponse response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest()
|
TokenResponse response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
|
||||||
{
|
{
|
||||||
Address = disco.TokenEndpoint,
|
Address = disco.TokenEndpoint,
|
||||||
ClientId = CLIENT_ID,
|
ClientId = CLIENT_ID,
|
||||||
@ -211,4 +118,105 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
SetCurrentUser(response);
|
SetCurrentUser(response);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IObservable<bool> IsLoggedIn => _isLoggedInSubject.AsObservable();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IObservable<string?> UserCode => _userCodeSubject.AsObservable();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IObservable<string?> VerificationUri => _verificationUriSubject.AsObservable();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||||
|
|
||||||
|
public async Task<string?> GetBearer()
|
||||||
|
{
|
||||||
|
await _bearerLock.WaitAsync();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// If not logged in, attempt to auto login first
|
||||||
|
if (!_isLoggedInSubject.Value)
|
||||||
|
await AutoLogin();
|
||||||
|
|
||||||
|
if (_token == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// If the token is expiring, refresh it
|
||||||
|
if (_token.Expired && !await UseRefreshToken(_token.RefreshToken))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return _token.AccessToken;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_bearerLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> AutoLogin()
|
||||||
|
{
|
||||||
|
if (_isLoggedInSubject.Value)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
string? refreshToken = _authenticationRepository.GetRefreshToken();
|
||||||
|
if (refreshToken == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return await UseRefreshToken(refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<bool> Login(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient();
|
||||||
|
DeviceAuthorizationResponse response = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest
|
||||||
|
{
|
||||||
|
Address = disco.DeviceAuthorizationEndpoint,
|
||||||
|
ClientId = CLIENT_ID,
|
||||||
|
Scope = "openid profile email offline_access api"
|
||||||
|
}, cancellationToken);
|
||||||
|
if (response.IsError)
|
||||||
|
throw new ArtemisWebClientException("Failed to request device authorization: " + response.Error);
|
||||||
|
if (response.DeviceCode == null)
|
||||||
|
throw new ArtemisWebClientException("Failed to request device authorization: Got no device code");
|
||||||
|
|
||||||
|
DateTimeOffset timeout = DateTimeOffset.UtcNow.AddSeconds(response.ExpiresIn ?? 1800);
|
||||||
|
_userCodeSubject.OnNext(response.UserCode);
|
||||||
|
_verificationUriSubject.OnNext(response.VerificationUri);
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(response.Interval), cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (DateTimeOffset.UtcNow < timeout)
|
||||||
|
{
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
bool success = await AttemptRequestDeviceToken(client, response.DeviceCode, cancellationToken);
|
||||||
|
if (success)
|
||||||
|
return true;
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(response.Interval), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_userCodeSubject.OnNext(null);
|
||||||
|
_verificationUriSubject.OnNext(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Logout()
|
||||||
|
{
|
||||||
|
_token = null;
|
||||||
|
_claims.Clear();
|
||||||
|
_authenticationRepository.SetRefreshToken(null);
|
||||||
|
|
||||||
|
_isLoggedInSubject.OnNext(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user