Browse Source

Basic login flow works

Krzysztof Krysiński 4 months ago
parent
commit
0f869aab6d

+ 20 - 5
src/PixiEditor.PixiAuth/PixiAuthClient.cs

@@ -21,7 +21,14 @@ public class PixiAuthClient
         if (response.IsSuccessStatusCode)
         {
             string result = await response.Content.ReadAsStringAsync();
-            if (Guid.TryParse(result, out Guid sessionId))
+            Dictionary<string, string>? resultDict =
+                System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(result);
+            if (resultDict == null || !resultDict.TryGetValue("sessionId", out string? sessionIdString))
+            {
+                return null;
+            }
+
+            if (Guid.TryParse(sessionIdString, out Guid sessionId))
             {
                 return sessionId;
             }
@@ -30,17 +37,25 @@ public class PixiAuthClient
         return null;
     }
 
-    public async Task<string?> TryGetSessionToken(string email, Guid session)
+    public async Task<string?> TryClaimSessionToken(string email, Guid session)
     {
         Dictionary<string, string> body = new() { { "email", email }, { "sessionId", session.ToString() } };
-        var response = await httpClient.PostAsJsonAsync("/session/getSessionToken", body);
+        var response = await httpClient.GetAsync($"/session/claimToken?userEmail={email}&sessionId={session}");
 
         if (response.IsSuccessStatusCode)
         {
             string result = await response.Content.ReadAsStringAsync();
-            if (!string.IsNullOrEmpty(result))
+            Dictionary<string, string>? resultDict =
+                System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, string>>(result);
+            string? token = null;
+            if (resultDict != null && resultDict.TryGetValue("token", out token))
+            {
+                return token;
+            }
+
+            if (!string.IsNullOrEmpty(token))
             {
-                return result;
+                return token;
             }
         }
 

+ 8 - 2
src/PixiEditor.PixiAuth/User.cs

@@ -1,7 +1,13 @@
-namespace PixiEditor.Models.Auth;
+namespace PixiEditor.PixiAuth;
 
-public record User
+public class User
 {
     public string Email { get; set; } = string.Empty;
+    public Guid? SessionId { get; set; }
     public string? SessionToken { get; set; } = string.Empty;
+
+    public User(string email)
+    {
+        Email = email;
+    }
 }

+ 1 - 0
src/PixiEditor/Helpers/ServiceCollectionHelpers.cs

@@ -68,6 +68,7 @@ internal static class ServiceCollectionHelpers
             .AddSingleton<AnimationsViewModel>()
             .AddSingleton<NodeGraphManagerViewModel>()
             .AddSingleton<AutosaveViewModel>()
+            .AddSingleton<UserViewModel>()
             .AddSingleton<IColorsHandler, ColorsViewModel>(x => x.GetRequiredService<ColorsViewModel>())
             .AddSingleton<IWindowHandler, WindowViewModel>(x => x.GetRequiredService<WindowViewModel>())
             .AddSingleton<RegistryViewModel>()

+ 66 - 1
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -1,3 +1,5 @@
+using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.PixiAuth;
 
 namespace PixiEditor.ViewModels.SubViewModels;
@@ -5,9 +7,21 @@ namespace PixiEditor.ViewModels.SubViewModels;
 internal class UserViewModel : SubViewModel<ViewModelMain>
 {
     public PixiAuthClient PixiAuthClient { get; }
+    public User? User { get; private set; }
+
+    public bool WaitingForActivation => User is { SessionId: not null } && string.IsNullOrEmpty(User.SessionToken);
+    public bool IsLoggedIn => User is { SessionId: not null } && !string.IsNullOrEmpty(User.SessionToken);
+
+    public AsyncRelayCommand<string> RequestLoginCommand { get; }
+    public AsyncRelayCommand TryValidateSessionCommand { get; }
+
+    private bool apiValid = true;
 
     public UserViewModel(ViewModelMain owner) : base(owner)
     {
+        RequestLoginCommand = new AsyncRelayCommand<string>(RequestLogin);
+        TryValidateSessionCommand = new AsyncRelayCommand(TryValidateSession);
+
         string baseUrl = BuildConstants.PixiEditorApiUrl;
 #if DEBUG
         if (baseUrl.Contains('{') && baseUrl.Contains('}'))
@@ -19,6 +33,57 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             }
         }
 #endif
-        PixiAuthClient = new PixiAuthClient(BuildConstants.PixiEditorApiUrl);
+        try
+        {
+            PixiAuthClient = new PixiAuthClient(baseUrl);
+        }
+        catch (UriFormatException e)
+        {
+            Console.WriteLine($"Invalid api URL format: {e.Message}");
+            apiValid = false;
+        }
+    }
+
+
+    public async Task RequestLogin(string email)
+    {
+        if (!apiValid) return;
+
+        Guid? session = await PixiAuthClient.GenerateSession(email);
+        if (session != null)
+        {
+            User = new User(email) { SessionId = session.Value };
+            OnPropertyChanged(nameof(WaitingForActivation));
+            OnPropertyChanged(nameof(IsLoggedIn));
+            SaveUserInfo();
+        }
+    }
+
+    public async Task<bool> TryValidateSession()
+    {
+        if (!apiValid) return false;
+
+        if (User?.SessionId == null)
+        {
+            return false;
+        }
+
+        string? token = await PixiAuthClient.TryClaimSessionToken(User.Email, User.SessionId.Value);
+        if (token != null)
+        {
+            User.SessionToken = token;
+            OnPropertyChanged(nameof(User));
+            OnPropertyChanged(nameof(WaitingForActivation));
+            OnPropertyChanged(nameof(IsLoggedIn));
+            SaveUserInfo();
+            return true;
+        }
+
+        return false;
+    }
+
+    public void SaveUserInfo()
+    {
+        // TODO:
     }
 }

+ 9 - 0
src/PixiEditor/ViewModels/SubViewModels/WindowViewModel.cs

@@ -12,6 +12,7 @@ using PixiEditor.UI.Common.Fonts;
 using PixiEditor.ViewModels.Document;
 using PixiEditor.ViewModels.UserPreferences;
 using PixiEditor.Views;
+using PixiEditor.Views.Auth;
 using PixiEditor.Views.Dialogs;
 using PixiEditor.Views.Windows;
 using Command = PixiEditor.Models.Commands.Attributes.Commands.Command;
@@ -269,4 +270,12 @@ internal class WindowViewModel : SubViewModel<ViewModelMain>, IWindowHandler
     {
         Owner.LayoutSubViewModel.LayoutManager.ShowDockable(id);
     }
+
+    [Commands_Command.Basic("PixiEditor.Window.OpenLoginWindow", "OPEN_LOGIN_WINDOW", "OPEN_LOGIN_WINDOW",
+        MenuItemOrder = 6, AnalyticsTrack = true)]
+    public void OpenLoginWindow()
+    {
+        LoginPopup popup = new LoginPopup() { DataContext = Owner.UserViewModel };
+        popup.Show();
+    }
 }

+ 3 - 0
src/PixiEditor/ViewModels/ViewModelMain.cs

@@ -62,6 +62,7 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
     public AnimationsViewModel AnimationsSubViewModel { get; set; }
     public NodeGraphManagerViewModel NodeGraphManager { get; set; }
     public AutosaveViewModel AutosaveViewModel { get; set; }
+    public UserViewModel UserViewModel { get; set; }
 
     public IPreferences Preferences { get; set; }
     public ILocalizationProvider LocalizationProvider { get; set; }
@@ -167,6 +168,8 @@ internal partial class ViewModelMain : ViewModelBase, ICommandsHandler
 
         AutosaveViewModel = services.GetService<AutosaveViewModel>();
 
+        UserViewModel = services.GetRequiredService<UserViewModel>();
+
         ExtensionsSubViewModel = services.GetService<ExtensionsViewModel>(); // Must be last
 
         DocumentManagerSubViewModel.ActiveDocumentChanged += OnActiveDocumentChanged;

+ 4 - 1
src/PixiEditor/Views/Auth/LoginForm.axaml

@@ -2,10 +2,13 @@
              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:auth1="clr-namespace:PixiEditor.Views.Auth"
              mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
              x:Class="PixiEditor.Views.Auth.LoginForm">
     <StackPanel>
         <TextBox Name="Email" />
-        <Button Name="LoginButton" Content="Login" />
+        <Button Name="LoginButton" Content="Login"
+                Command="{Binding RequestLoginCommand, RelativeSource={RelativeSource AncestorType=auth1:LoginForm, Mode=FindAncestor}}"
+                CommandParameter="{Binding ElementName=Email, Path=Text}" />
     </StackPanel>
 </UserControl>

+ 10 - 1
src/PixiEditor/Views/Auth/LoginForm.axaml.cs

@@ -1,11 +1,20 @@
+using System.Windows.Input;
 using Avalonia;
 using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
 
 namespace PixiEditor.Views.Auth;
 
 public partial class LoginForm : UserControl
 {
+    public static readonly StyledProperty<ICommand> RequestLoginCommandProperty = AvaloniaProperty.Register<LoginForm, ICommand>(
+        nameof(RequestLoginCommand));
+
+    public ICommand RequestLoginCommand
+    {
+        get => GetValue(RequestLoginCommandProperty);
+        set => SetValue(RequestLoginCommandProperty, value);
+    }
+
     public LoginForm()
     {
         InitializeComponent();

+ 23 - 0
src/PixiEditor/Views/Auth/LoginPopup.axaml

@@ -0,0 +1,23 @@
+<dialogs:PixiEditorPopup 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:dialogs="clr-namespace:PixiEditor.Views.Dialogs"
+                         xmlns:auth="clr-namespace:PixiEditor.Views.Auth"
+                         xmlns:viewModels="clr-namespace:PixiEditor.ViewModels"
+                         xmlns:subViewModels="clr-namespace:PixiEditor.ViewModels.SubViewModels"
+                         mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+                         x:Class="PixiEditor.Views.Auth.LoginPopup"
+                         Width="400" Height="300"
+                         Title="LoginPopup">
+    <Design.DataContext>
+        <subViewModels:UserViewModel />
+    </Design.DataContext>
+    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="5" Margin="10" MinWidth="300">
+        <auth:LoginForm RequestLoginCommand="{Binding Path=RequestLoginCommand}" />
+        <TextBlock Text="Email sent! Check your inbox." IsVisible="{Binding WaitingForActivation}"/>
+        <TextBlock Text="Logged in as " IsVisible="{Binding IsLoggedIn}">
+            <Run Text="{Binding User.Email}"/>
+        </TextBlock>
+    </StackPanel>
+</dialogs:PixiEditorPopup>

+ 25 - 0
src/PixiEditor/Views/Auth/LoginPopup.axaml.cs

@@ -0,0 +1,25 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using PixiEditor.ViewModels.SubViewModels;
+using PixiEditor.Views.Dialogs;
+
+namespace PixiEditor.Views.Auth;
+
+public partial class LoginPopup : PixiEditorPopup
+{
+    public LoginPopup()
+    {
+        InitializeComponent();
+    }
+
+    protected override async void OnGotFocus(GotFocusEventArgs e)
+    {
+        if (DataContext is UserViewModel { WaitingForActivation: true } vm)
+        {
+            await vm.TryValidateSession();
+        }
+    }
+}
+