mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-02-04 10:53:31 +00:00
Added basic login
This commit is contained in:
parent
bde4e861c2
commit
4224875fb0
BIN
src/Artemis.UI/Assets/Images/avatar-placeholder.png
Normal file
BIN
src/Artemis.UI/Assets/Images/avatar-placeholder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 212 KiB |
@ -56,8 +56,8 @@
|
|||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
Grid.RowSpan="3"
|
Grid.RowSpan="3"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Height="75"
|
Height="40"
|
||||||
Width="75"
|
Width="40"
|
||||||
Margin="0 0 15 0"
|
Margin="0 0 15 0"
|
||||||
IsVisible="{CompiledBinding RobertProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
IsVisible="{CompiledBinding RobertProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
RenderOptions.BitmapInterpolationMode="HighQuality">
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
<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:currentUser="clr-namespace:Artemis.UI.Screens.Workshop.CurrentUser"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.CurrentUser.CurrentUserView"
|
||||||
|
x:DataType="currentUser:CurrentUserViewModel">
|
||||||
|
|
||||||
|
<Panel>
|
||||||
|
<!-- Signed out -->
|
||||||
|
<Ellipse Height="28" Width="28" IsVisible="{CompiledBinding !IsLoggedIn}">
|
||||||
|
<Ellipse.ContextFlyout>
|
||||||
|
<MenuFlyout>
|
||||||
|
<MenuItem Header="Login" Command="{CompiledBinding Login}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<avalonia:MaterialIcon Kind="Login" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Ellipse.ContextFlyout>
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<ImageBrush Source="/Assets/Images/avatar-placeholder.png" />
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
|
||||||
|
<!-- Signed in -->
|
||||||
|
<Ellipse Height="28" Width="28" IsVisible="{CompiledBinding IsLoggedIn}" Name="UserMenu">
|
||||||
|
<Ellipse.ContextFlyout>
|
||||||
|
<Flyout>
|
||||||
|
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,*,*" MinWidth="300">
|
||||||
|
<Ellipse Grid.Column="0" Grid.RowSpan="3" Height="50" Width="50" Margin="0 0 8 0" VerticalAlignment="Top">
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<ImageBrush Source="{CompiledBinding Avatar}" />
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
<TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Name}" Margin="0 4 0 0"></TextBlock>
|
||||||
|
<TextBlock Grid.Column="1" Grid.Row="1" Text="{CompiledBinding Email}"></TextBlock>
|
||||||
|
<controls:HyperlinkButton
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="-8 0 0 0"
|
||||||
|
Padding="6 4"
|
||||||
|
Click="Button_OnClick">
|
||||||
|
Sign out
|
||||||
|
</controls:HyperlinkButton>
|
||||||
|
</Grid>
|
||||||
|
</Flyout>
|
||||||
|
</Ellipse.ContextFlyout>
|
||||||
|
<Ellipse.Fill>
|
||||||
|
<ImageBrush Source="{CompiledBinding Avatar}" />
|
||||||
|
</Ellipse.Fill>
|
||||||
|
</Ellipse>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
|
||||||
|
public partial class CurrentUserView : ReactiveUserControl<CurrentUserViewModel>
|
||||||
|
{
|
||||||
|
public CurrentUserView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Button_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
UserMenu.ContextFlyout?.Hide();
|
||||||
|
ViewModel?.Logout();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,104 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Flurl.Http;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
|
||||||
|
public class CurrentUserViewModel : ActivatableViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private ObservableAsPropertyHelper<bool>? _isLoggedIn;
|
||||||
|
|
||||||
|
private string? _userId;
|
||||||
|
private string? _name;
|
||||||
|
private string? _email;
|
||||||
|
private Bitmap? _avatar;
|
||||||
|
|
||||||
|
public CurrentUserViewModel(IAuthenticationService authenticationService)
|
||||||
|
{
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
|
||||||
|
|
||||||
|
this.WhenActivated(d => _isLoggedIn = _authenticationService.WhenAnyValue(s => s.IsLoggedIn).ToProperty(this, vm => vm.IsLoggedIn).DisposeWith(d));
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await _authenticationService.AutoLogin();
|
||||||
|
await LoadCurrentUser();
|
||||||
|
}).DisposeWith(d);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Logout()
|
||||||
|
{
|
||||||
|
_authenticationService.Logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? UserId
|
||||||
|
{
|
||||||
|
get => _userId;
|
||||||
|
set => RaiseAndSetIfChanged(ref _userId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Name
|
||||||
|
{
|
||||||
|
get => _name;
|
||||||
|
set => RaiseAndSetIfChanged(ref _name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Email
|
||||||
|
{
|
||||||
|
get => _email;
|
||||||
|
set => RaiseAndSetIfChanged(ref _email, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap? Avatar
|
||||||
|
{
|
||||||
|
get => _avatar;
|
||||||
|
set => RaiseAndSetIfChanged(ref _avatar, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> Login { get; }
|
||||||
|
public bool IsLoggedIn => _isLoggedIn?.Value ?? false;
|
||||||
|
|
||||||
|
private async Task ExecuteLogin(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _authenticationService.Login();
|
||||||
|
await LoadCurrentUser();
|
||||||
|
Console.WriteLine(_authenticationService.Claims);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadCurrentUser()
|
||||||
|
{
|
||||||
|
if (!IsLoggedIn)
|
||||||
|
return;
|
||||||
|
|
||||||
|
UserId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
||||||
|
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
||||||
|
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
||||||
|
|
||||||
|
if (UserId != null)
|
||||||
|
await LoadAvatar(UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAvatar(string userId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Avatar = new Bitmap(await $"{IAuthenticationService.AUTHORITY}/user/avatar/{userId}".GetStreamAsync());
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -30,6 +30,7 @@
|
|||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Border>
|
</Border>
|
||||||
|
<ContentControl Grid.Column="1" VerticalAlignment="Top" HorizontalAlignment="Right" Content="{CompiledBinding CurrentUserViewModel}" Margin="8"></ContentControl>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -4,6 +4,7 @@ using System.Reactive;
|
|||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.Builders;
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
@ -18,8 +19,9 @@ public class WorkshopViewModel : MainScreenViewModel
|
|||||||
{
|
{
|
||||||
private readonly IWorkshopClient _workshopClient;
|
private readonly IWorkshopClient _workshopClient;
|
||||||
|
|
||||||
public WorkshopViewModel(IScreen hostScreen, IWorkshopClient workshopClient) : base(hostScreen, "workshop")
|
public WorkshopViewModel(IScreen hostScreen, IWorkshopClient workshopClient, CurrentUserViewModel currentUserViewModel) : base(hostScreen, "workshop")
|
||||||
{
|
{
|
||||||
|
CurrentUserViewModel = currentUserViewModel;
|
||||||
_workshopClient = workshopClient;
|
_workshopClient = workshopClient;
|
||||||
DisplayName = "Workshop";
|
DisplayName = "Workshop";
|
||||||
|
|
||||||
@ -27,6 +29,7 @@ public class WorkshopViewModel : MainScreenViewModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<IGetEntries_Entries_Nodes> Test { get; set; } = new();
|
public ObservableCollection<IGetEntries_Entries_Nodes> Test { get; set; } = new();
|
||||||
|
public CurrentUserViewModel CurrentUserViewModel { get; set; }
|
||||||
|
|
||||||
private async Task GetEntries()
|
private async Task GetEntries()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.4.0" />
|
<PackageReference Include="DryIoc.dll" Version="5.4.0" />
|
||||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||||
|
<PackageReference Include="IdentityModel" Version="6.1.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
<PackageReference Include="StrawberryShake.Server" Version="13.0.5" />
|
<PackageReference Include="StrawberryShake.Server" Version="13.0.5" />
|
||||||
@ -17,4 +18,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove=".graphqlconfig" />
|
<None Remove=".graphqlconfig" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
using Artemis.WebClient.Workshop.Repositories;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using DryIoc;
|
using DryIoc;
|
||||||
using DryIoc.Microsoft.DependencyInjection;
|
using DryIoc.Microsoft.DependencyInjection;
|
||||||
|
using IdentityModel.Client;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.DryIoc;
|
namespace Artemis.WebClient.Workshop.DryIoc;
|
||||||
@ -21,6 +24,15 @@ public static class ContainerExtensions
|
|||||||
.AddWorkshopClient()
|
.AddWorkshopClient()
|
||||||
.ConfigureHttpClient(client => client.BaseAddress = new Uri("https://localhost:7281/graphql"));
|
.ConfigureHttpClient(client => client.BaseAddress = new Uri("https://localhost:7281/graphql"));
|
||||||
|
|
||||||
|
serviceCollection.AddSingleton<IDiscoveryCache>(r =>
|
||||||
|
{
|
||||||
|
IHttpClientFactory factory = r.GetRequiredService<IHttpClientFactory>();
|
||||||
|
return new DiscoveryCache(IAuthenticationService.AUTHORITY, () => factory.CreateClient());
|
||||||
|
});
|
||||||
|
|
||||||
container.WithDependencyInjectionAdapter(serviceCollection);
|
container.WithDependencyInjectionAdapter(serviceCollection);
|
||||||
|
|
||||||
|
container.Register<IAuthenticationRepository, AuthenticationRepository>(Reuse.Singleton);
|
||||||
|
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
namespace Artemis.WebClient.Workshop.Entities;
|
||||||
|
|
||||||
|
public class RefreshTokenEntity
|
||||||
|
{
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Artemis.Core;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An exception thrown when a web client related error occurs
|
||||||
|
/// </summary>
|
||||||
|
public class ArtemisWebClientException : Exception
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ArtemisWebClientException()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ArtemisWebClientException(string? message) : base(message)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ArtemisWebClientException(string? message, Exception? innerException) : base(message, innerException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
using Artemis.WebClient.Workshop.Entities;
|
||||||
|
using LiteDB;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Repositories;
|
||||||
|
|
||||||
|
internal class AuthenticationRepository : IAuthenticationRepository
|
||||||
|
{
|
||||||
|
private readonly LiteRepository _repository;
|
||||||
|
|
||||||
|
public AuthenticationRepository(LiteRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_repository.Database.GetCollection<RefreshTokenEntity>().EnsureIndex(s => s.RefreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void SetRefreshToken(string? refreshToken)
|
||||||
|
{
|
||||||
|
_repository.Database.GetCollection<RefreshTokenEntity>().DeleteAll();
|
||||||
|
|
||||||
|
if (refreshToken != null)
|
||||||
|
_repository.Insert(new RefreshTokenEntity {RefreshToken = refreshToken});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string? GetRefreshToken()
|
||||||
|
{
|
||||||
|
return _repository.Query<RefreshTokenEntity>().FirstOrDefault()?.RefreshToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IAuthenticationRepository
|
||||||
|
{
|
||||||
|
void SetRefreshToken(string? refreshToken);
|
||||||
|
string? GetRefreshToken();
|
||||||
|
}
|
||||||
25
src/Artemis.WebClient.Workshop/Services/AccessToken.cs
Normal file
25
src/Artemis.WebClient.Workshop/Services/AccessToken.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
using IdentityModel.Client;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
|
internal class AuthenticationToken
|
||||||
|
{
|
||||||
|
public AuthenticationToken(TokenResponse tokenResponse)
|
||||||
|
{
|
||||||
|
if (tokenResponse.AccessToken == null)
|
||||||
|
throw new ArtemisWebClientException("Token response contains no access token");
|
||||||
|
if (tokenResponse.RefreshToken == null)
|
||||||
|
throw new ArtemisWebClientException("Token response contains no refresh token");
|
||||||
|
|
||||||
|
AccessToken = tokenResponse.AccessToken;
|
||||||
|
RefreshToken = tokenResponse.RefreshToken;
|
||||||
|
ExpiresAt = DateTimeOffset.UtcNow.AddSeconds(tokenResponse.ExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DateTimeOffset ExpiresAt { get; private set; }
|
||||||
|
public bool Expired => DateTimeOffset.UtcNow.AddSeconds(5) >= ExpiresAt;
|
||||||
|
|
||||||
|
public string AccessToken { get; private set; }
|
||||||
|
public string RefreshToken { get; private set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,222 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.WebClient.Workshop.Repositories;
|
||||||
|
using IdentityModel;
|
||||||
|
using IdentityModel.Client;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
|
public interface IAuthenticationService : IProtectedArtemisService
|
||||||
|
{
|
||||||
|
public const string AUTHORITY = "https://localhost:5001";
|
||||||
|
|
||||||
|
bool IsLoggedIn { get; }
|
||||||
|
string? UserCode { get; }
|
||||||
|
ReadOnlyObservableCollection<Claim> Claims { get; }
|
||||||
|
|
||||||
|
Task<string?> GetBearer();
|
||||||
|
Task<bool> AutoLogin();
|
||||||
|
Task<bool> Login();
|
||||||
|
void Logout();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AuthenticationService : CorePropertyChanged, IAuthenticationService
|
||||||
|
{
|
||||||
|
internal const string CLIENT_ID = "artemis.desktop";
|
||||||
|
|
||||||
|
private readonly IDiscoveryCache _discoveryCache;
|
||||||
|
private readonly IAuthenticationRepository _authenticationRepository;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly ObservableCollection<Claim> _claims = new();
|
||||||
|
private readonly SemaphoreSlim _bearerLock = new(1);
|
||||||
|
|
||||||
|
private AuthenticationToken? _token;
|
||||||
|
private string? _userCode;
|
||||||
|
private bool _isLoggedIn;
|
||||||
|
|
||||||
|
public AuthenticationService(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IAuthenticationRepository authenticationRepository)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_discoveryCache = discoveryCache;
|
||||||
|
_authenticationRepository = authenticationRepository;
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
DiscoveryDocumentResponse disco = await _discoveryCache.GetAsync();
|
||||||
|
if (disco.IsError)
|
||||||
|
throw new ArtemisWebClientException("Failed to retrieve discovery document: " + disco.Error);
|
||||||
|
|
||||||
|
return disco;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> AttemptRequestDeviceToken(HttpClient client, string deviceCode)
|
||||||
|
{
|
||||||
|
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||||
|
TokenResponse response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest
|
||||||
|
{
|
||||||
|
Address = disco.TokenEndpoint,
|
||||||
|
ClientId = CLIENT_ID,
|
||||||
|
DeviceCode = deviceCode
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.IsError)
|
||||||
|
{
|
||||||
|
if (response.Error == OidcConstants.TokenErrors.AuthorizationPending || response.Error == OidcConstants.TokenErrors.SlowDown)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
throw new ArtemisWebClientException("Failed to request device token: " + response.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SetCurrentUser(client, response);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SetCurrentUser(HttpClient client, TokenResponse response)
|
||||||
|
{
|
||||||
|
_token = new AuthenticationToken(response);
|
||||||
|
_authenticationRepository.SetRefreshToken(_token.RefreshToken);
|
||||||
|
|
||||||
|
await GetUserInfo(client, _token.AccessToken);
|
||||||
|
IsLoggedIn = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetUserInfo(HttpClient client, string accessToken)
|
||||||
|
{
|
||||||
|
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||||
|
UserInfoResponse response = await client.GetUserInfoAsync(new UserInfoRequest()
|
||||||
|
{
|
||||||
|
Address = disco.UserInfoEndpoint,
|
||||||
|
Token = accessToken
|
||||||
|
});
|
||||||
|
if (response.IsError)
|
||||||
|
throw new ArtemisWebClientException("Failed to retrieve user info: " + response.Error);
|
||||||
|
|
||||||
|
_claims.Clear();
|
||||||
|
foreach (Claim responseClaim in response.Claims)
|
||||||
|
_claims.Add(responseClaim);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> UseRefreshToken(string refreshToken)
|
||||||
|
{
|
||||||
|
DiscoveryDocumentResponse disco = await GetDiscovery();
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient();
|
||||||
|
TokenResponse response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest()
|
||||||
|
{
|
||||||
|
Address = disco.TokenEndpoint,
|
||||||
|
ClientId = CLIENT_ID,
|
||||||
|
RefreshToken = refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.IsError)
|
||||||
|
{
|
||||||
|
if (response.Error == OidcConstants.TokenErrors.ExpiredToken)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
throw new ArtemisWebClientException("Failed to request refresh token: " + response.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await SetCurrentUser(client, response);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user