From 661242ebf9074e50b8be88b0158e3247726c0e48 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 7 Aug 2023 20:25:00 +0200 Subject: [PATCH] Submission wizard - Added logging in and email verification check --- .../CurrentUser/CurrentUserViewModel.cs | 9 ++- .../Steps/WelcomeStepView.axaml | 47 ++++++++++- .../Steps/WelcomeStepViewModel.cs | 81 ++++++++++++++++++- .../SubmissionWizardViewModel.cs | 4 +- .../Services/AuthenticationService.cs | 28 +++++-- .../Services/IAuthenticationService.cs | 3 +- 6 files changed, 152 insertions(+), 20 deletions(-) diff --git a/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserViewModel.cs b/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserViewModel.cs index 388ebe130..7b652d289 100644 --- a/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserViewModel.cs @@ -34,8 +34,11 @@ public class CurrentUserViewModel : ActivatableViewModelBase _windowService = windowService; Login = ReactiveCommand.CreateFromTask(ExecuteLogin); - this.WhenActivated(d => ReactiveCommand.CreateFromTask(ExecuteAutoLogin).Execute().Subscribe().DisposeWith(d)); - this.WhenActivated(d => _authenticationService.IsLoggedIn.Subscribe(_ => LoadCurrentUser().DisposeWith(d))); + this.WhenActivated(d => + { + Task.Run(AutoLogin); + _authenticationService.IsLoggedIn.Subscribe(_ => Task.Run(LoadCurrentUser)).DisposeWith(d); + }); } public bool Loading @@ -86,7 +89,7 @@ public class CurrentUserViewModel : ActivatableViewModelBase await LoadCurrentUser(); } - private async Task ExecuteAutoLogin(CancellationToken cancellationToken) + private async Task AutoLogin() { try { diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml index 7341a2307..6e273c4af 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepView.axaml @@ -2,10 +2,51 @@ 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:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps" + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.WelcomeStepView"> + x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.WelcomeStepView" + x:DataType="steps:WelcomeStepViewModel"> - Welcome to the Workshop Submission Wizard 🧙 + Welcome to the Workshop Submission Wizard 🧙 Here we'll take you, step by step, through the process of uploading your submission to the workshop. + + + First things first + + In order to submit anything to the workshop you must be logged in. + + + Creating an account is free and we'll not bother you with a newsletter or crap like that. You can also log in with Google or Discord. + + + + + + + Confirm email address + + Before you can continue, please confirm your email address. () + + You'll find the confirmation mail in your inbox. + + + Don't see an email? Check your spam box! + + + + + PS: We take this step to avoid the workshop getting flooded with bogus content. + - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs index cf70f6410..47d59e6e0 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/WelcomeStepViewModel.cs @@ -1,11 +1,51 @@ +using System; using System.Reactive; +using System.Reactive.Disposables; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Screens.Workshop.CurrentUser; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop.Services; +using IdentityModel; using ReactiveUI; +using Timer = System.Timers.Timer; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; public class WelcomeStepViewModel : SubmissionViewModel { - #region Overrides of SubmissionViewModel + private readonly IAuthenticationService _authenticationService; + private readonly ObservableAsPropertyHelper _showMissingVerification; + private readonly IWindowService _windowService; + private ObservableAsPropertyHelper? _email; + private ObservableAsPropertyHelper? _emailVerified; + private ObservableAsPropertyHelper? _isLoggedIn; + + public WelcomeStepViewModel(IAuthenticationService authenticationService, IWindowService windowService) + { + _authenticationService = authenticationService; + _windowService = windowService; + _showMissingVerification = this.WhenAnyValue(vm => vm.IsLoggedIn, vm => vm.EmailVerified, (l, v) => l && (v == null || v.Value == "false")).ToProperty(this, vm => vm.ShowMissingVerification); + + ShowGoBack = false; + Continue = ReactiveCommand.Create(ExecuteContinue); + Login = ReactiveCommand.CreateFromTask(ExecuteLogin); + Refresh = ReactiveCommand.CreateFromTask(ExecuteRefresh); + + this.WhenActivated(d => + { + _isLoggedIn = authenticationService.IsLoggedIn.ToProperty(this, vm => vm.IsLoggedIn).DisposeWith(d); + _emailVerified = authenticationService.GetClaim(JwtClaimTypes.EmailVerified).ToProperty(this, vm => vm.EmailVerified).DisposeWith(d); + _email = authenticationService.GetClaim(JwtClaimTypes.Email).ToProperty(this, vm => vm.Email).DisposeWith(d); + + Timer updateTimer = new(TimeSpan.FromSeconds(15)); + updateTimer.Elapsed += (_, _) => Task.Run(Update); + updateTimer.Start(); + + updateTimer.DisposeWith(d); + }); + } /// public override ReactiveCommand Continue { get; } @@ -13,10 +53,43 @@ public class WelcomeStepViewModel : SubmissionViewModel /// public override ReactiveCommand GoBack { get; } = null!; - public WelcomeStepViewModel() + public ReactiveCommand Login { get; } + public ReactiveCommand Refresh { get; } + + public bool ShowMissingVerification => _showMissingVerification.Value; + public bool IsLoggedIn => _isLoggedIn?.Value ?? false; + public Claim? EmailVerified => _emailVerified?.Value; + public Claim? Email => _email?.Value; + + private async Task Update() { - ShowGoBack = false; + if (EmailVerified?.Value == "true") + return; + + try + { + // Use the refresh token to login again, updating claims + await _authenticationService.AutoLogin(true); + } + catch (Exception) + { + // ignored, meh + } } - #endregion + private void ExecuteContinue() + { + throw new NotImplementedException(); + } + + private async Task ExecuteLogin(CancellationToken ct) + { + await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Workshop login").ShowAsync(); + } + + private async Task ExecuteRefresh(CancellationToken ct) + { + await Update(); + await Task.Delay(1000, ct); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs index fed2c3241..857aea286 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs @@ -10,9 +10,9 @@ public class SubmissionWizardViewModel : DialogViewModelBase { private SubmissionViewModel _screen; - public SubmissionWizardViewModel(CurrentUserViewModel currentUserViewModel) + public SubmissionWizardViewModel(CurrentUserViewModel currentUserViewModel, WelcomeStepViewModel welcomeStepViewModel) { - _screen = new WelcomeStepViewModel(); + _screen = welcomeStepViewModel; CurrentUserViewModel = currentUserViewModel; } diff --git a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs index adec69370..a9a7ac379 100644 --- a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs @@ -8,6 +8,7 @@ using System.Security.Cryptography; using System.Text; using Artemis.Core; using Artemis.WebClient.Workshop.Repositories; +using DynamicData; using IdentityModel; using IdentityModel.Client; @@ -18,7 +19,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi private const string CLIENT_ID = "artemis.desktop"; private readonly IAuthenticationRepository _authenticationRepository; private readonly SemaphoreSlim _authLock = new(1); - private readonly ObservableCollection _claims = new(); + private readonly SourceList _claims; private readonly IDiscoveryCache _discoveryCache; private readonly IHttpClientFactory _httpClientFactory; @@ -32,7 +33,9 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi _discoveryCache = discoveryCache; _authenticationRepository = authenticationRepository; - Claims = new ReadOnlyObservableCollection(_claims); + _claims = new SourceList(); + _claims.Connect().Bind(out ReadOnlyObservableCollection claims).Subscribe(); + Claims = claims; } private async Task GetDiscovery() @@ -54,9 +57,11 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi if (token == null) throw new ArtemisWebClientException("Failed to read JWT token"); - _claims.Clear(); - foreach (Claim responseClaim in token.Claims) - _claims.Add(responseClaim); + _claims.Edit(c => + { + c.Clear(); + c.AddRange(token.Claims); + }); _isLoggedInSubject.OnNext(true); } @@ -96,6 +101,15 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi /// public ReadOnlyObservableCollection Claims { get; } + /// + public IObservable GetClaim(string type) + { + return _claims.Connect() + .Filter(c => c.Type == JwtClaimTypes.Email) + .ToCollection() + .Select(f => f.FirstOrDefault()); + } + public async Task GetBearer() { await _authLock.WaitAsync(); @@ -122,13 +136,13 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi } /// - public async Task AutoLogin() + public async Task AutoLogin(bool force = false) { await _authLock.WaitAsync(); try { - if (_isLoggedInSubject.Value) + if (!force && _isLoggedInSubject.Value) return true; string? refreshToken = _authenticationRepository.GetRefreshToken(); diff --git a/src/Artemis.WebClient.Workshop/Services/IAuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/IAuthenticationService.cs index 16b05ed5e..c3a3686ba 100644 --- a/src/Artemis.WebClient.Workshop/Services/IAuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/IAuthenticationService.cs @@ -9,8 +9,9 @@ public interface IAuthenticationService : IProtectedArtemisService IObservable IsLoggedIn { get; } ReadOnlyObservableCollection Claims { get; } + IObservable GetClaim(string type); Task GetBearer(); - Task AutoLogin(); + Task AutoLogin(bool force = false); Task Login(CancellationToken cancellationToken); void Logout(); } \ No newline at end of file