Browse Source

Some error handling

Krzysztof Krysiński 4 months ago
parent
commit
352e231e61

+ 6 - 0
src/PixiEditor.PixiAuth/Exceptions/BadRequestException.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.PixiAuth.Exceptions;
+
+public class BadRequestException(string message) : PixiAuthException(400, message)
+{
+
+}

+ 6 - 0
src/PixiEditor.PixiAuth/Exceptions/ForbiddenException.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.PixiAuth.Exceptions;
+
+public class ForbiddenException(string message) : PixiAuthException(403, message)
+{
+
+}

+ 6 - 0
src/PixiEditor.PixiAuth/Exceptions/InternalServerErrorException.cs

@@ -0,0 +1,6 @@
+namespace PixiEditor.PixiAuth.Exceptions;
+
+public class InternalServerErrorException(string message) : PixiAuthException(500, message)
+{
+
+}

+ 11 - 0
src/PixiEditor.PixiAuth/Exceptions/PixiAuthException.cs

@@ -0,0 +1,11 @@
+namespace PixiEditor.PixiAuth.Exceptions;
+
+public class PixiAuthException : Exception
+{
+    public int StatusCode { get; }
+
+    public PixiAuthException(int statusCode, string message) : base(message)
+    {
+        StatusCode = statusCode;
+    }
+}

+ 87 - 2
src/PixiEditor.PixiAuth/PixiAuthClient.cs

@@ -1,4 +1,7 @@
+using System.Net;
+using System.Net.Http.Headers;
 using System.Net.Http.Json;
+using PixiEditor.PixiAuth.Exceptions;
 
 namespace PixiEditor.PixiAuth;
 
@@ -12,9 +15,8 @@ public class PixiAuthClient
     {
         httpClient = new HttpClient();
         httpClient.BaseAddress = new Uri(baseUrl);
+        // TODO: Update expiration date locally
         // TODO: Add error code handling
-        // TODO: Add refreshing token
-        // TODO: Add logout
     }
 
     public async Task<Guid?> GenerateSession(string email)
@@ -36,6 +38,14 @@ public class PixiAuthClient
                 return sessionId;
             }
         }
+        else if (response.StatusCode == HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
+        else if (response.StatusCode == HttpStatusCode.InternalServerError)
+        {
+            throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
+        }
 
         return null;
     }
@@ -61,7 +71,82 @@ public class PixiAuthClient
                 return token;
             }
         }
+        else if (response.StatusCode == HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
+        else if (response.StatusCode == HttpStatusCode.InternalServerError)
+        {
+            throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
+        }
 
         return null;
     }
+
+    /// <summary>
+    ///     /// Refreshes the session token.
+    /// </summary>
+    /// <param name="userSessionId">Id of the session.</param>
+    /// <param name="userSessionToken">Authentication token.</param>
+    /// <returns>Token if successful, null otherwise.</returns>
+    /// <exception cref="UnauthorizedAccessException">Thrown if the session is not valid.</exception>
+    public async Task<string?> RefreshToken(Guid userSessionId, string userSessionToken)
+    {
+        if (string.IsNullOrEmpty(userSessionToken))
+        {
+            return null;
+        }
+
+        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "/session/refreshToken");
+        request.Content = JsonContent.Create(new SessionModel(userSessionId, userSessionToken));
+        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", userSessionToken);
+
+        var response = await httpClient.SendAsync(request);
+
+        if (response.IsSuccessStatusCode)
+        {
+            string result = await response.Content.ReadAsStringAsync();
+            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 token;
+            }
+        }
+        else if (response.StatusCode == HttpStatusCode.Forbidden)
+        {
+            throw new ForbiddenException("SESSION_NOT_VALID");
+        }
+        else if (response.StatusCode == HttpStatusCode.BadRequest)
+        {
+            throw new BadRequestException(await response.Content.ReadAsStringAsync());
+        }
+        else if (response.StatusCode == HttpStatusCode.InternalServerError)
+        {
+            throw new InternalServerErrorException("INTERNAL_SERVER_ERROR");
+        }
+
+        return null;
+    }
+
+    public async Task Logout(Guid userSessionId, string userSessionToken)
+    {
+        if (string.IsNullOrEmpty(userSessionToken))
+        {
+            return;
+        }
+
+        HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "/session/logout");
+        string sessionId = userSessionId.ToString(); // Name is important here, do not change!
+        request.Content = JsonContent.Create(sessionId);
+        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", userSessionToken);
+
+        await httpClient.SendAsync(request);
+    }
 }

+ 13 - 0
src/PixiEditor.PixiAuth/SessionModel.cs

@@ -0,0 +1,13 @@
+namespace PixiEditor.PixiAuth;
+
+public class SessionModel
+{
+    public Guid SessionId { get; set; }
+    public string SessionToken { get; set; } = string.Empty;
+
+    public SessionModel(Guid sessionId, string sessionToken)
+    {
+        SessionId = sessionId;
+        SessionToken = sessionToken;
+    }
+}

+ 4 - 1
src/PixiEditor/Data/Localization/Languages/en.json

@@ -1025,5 +1025,8 @@
   "ONB_SHORTCUTS": "Select Your Shortcuts",
   "GRAPH_STATE_UNABLE_TO_CREATE_MEMBER": "Current Node Graph setup disallows creation of a new layer next to the selected one.",
   "PRIMARY_TOOLSET": "Primary Toolset",
-  "OPEN_ONBOARDING_WINDOW": "Open onboarding window"
+  "OPEN_ONBOARDING_WINDOW": "Open onboarding window",
+  "USER_NOT_FOUND": "User not found",
+  "SESSION_NOT_VALID": "Session is not valid, please log in again",
+  "INTERNAL_SERVER_ERROR": "There was an internal server error. Please try again later."
 }

+ 99 - 10
src/PixiEditor/ViewModels/SubViewModels/UserViewModel.cs

@@ -1,12 +1,15 @@
 using CommunityToolkit.Mvvm.Input;
+using PixiEditor.Extensions.Common.Localization;
 using PixiEditor.Models.Commands.Attributes.Commands;
 using PixiEditor.OperatingSystem;
 using PixiEditor.PixiAuth;
+using PixiEditor.PixiAuth.Exceptions;
 
 namespace PixiEditor.ViewModels.SubViewModels;
 
 internal class UserViewModel : SubViewModel<ViewModelMain>
 {
+    private LocalizedString? lastError = null;
     public PixiAuthClient PixiAuthClient { get; }
     public User? User { get; private set; }
 
@@ -16,6 +19,13 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
 
     public AsyncRelayCommand<string> RequestLoginCommand { get; }
     public AsyncRelayCommand TryValidateSessionCommand { get; }
+    public AsyncRelayCommand LogoutCommand { get; }
+
+    public LocalizedString? LastError
+    {
+        get => lastError;
+        set => SetProperty(ref lastError, value);
+    }
 
     private bool apiValid = true;
 
@@ -23,6 +33,7 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     {
         RequestLoginCommand = new AsyncRelayCommand<string>(RequestLogin);
         TryValidateSessionCommand = new AsyncRelayCommand(TryValidateSession);
+        LogoutCommand = new AsyncRelayCommand(Logout);
 
         string baseUrl = BuildConstants.PixiEditorApiUrl;
 #if DEBUG
@@ -45,7 +56,11 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             apiValid = false;
         }
 
-        Task.Run(async () => await LoadUserData());
+        Task.Run(async () =>
+        {
+            await LoadUserData();
+            await TryRefreshToken();
+        });
     }
 
 
@@ -53,13 +68,57 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
     {
         if (!apiValid) return;
 
-        Guid? session = await PixiAuthClient.GenerateSession(email);
-        if (session != null)
+        try
         {
-            User = new User(email) { SessionId = session.Value };
+            Guid? session = await PixiAuthClient.GenerateSession(email);
+            if (session != null)
+            {
+                LastError = null;
+                User = new User(email) { SessionId = session.Value };
+                NotifyProperties();
+                SaveUserInfo();
+            }
+        }
+        catch (PixiAuthException authException)
+        {
+           LastError = new LocalizedString(authException.Message);
+        }
+    }
+
+    public async Task<bool> TryRefreshToken()
+    {
+        if (!apiValid) return false;
+
+        if (!IsLoggedIn)
+        {
+            return false;
+        }
+
+        try
+        {
+            string? token = await PixiAuthClient.RefreshToken(User.SessionId.Value, User.SessionToken);
+
+            if (token != null)
+            {
+                User.SessionToken = token;
+                NotifyProperties();
+                SaveUserInfo();
+                return true;
+            }
+        }
+        catch (ForbiddenException e)
+        {
+            User = null;
             NotifyProperties();
             SaveUserInfo();
+            LastError = new LocalizedString(e.Message);
+        }
+        catch (PixiAuthException authException)
+        {
+            LastError = new LocalizedString(authException.Message);
         }
+
+        return false;
     }
 
     public async Task<bool> TryValidateSession()
@@ -71,18 +130,48 @@ internal class UserViewModel : SubViewModel<ViewModelMain>
             return false;
         }
 
-        string? token = await PixiAuthClient.TryClaimSessionToken(User.Email, User.SessionId.Value);
-        if (token != null)
+        try
         {
-            User.SessionToken = token;
-            NotifyProperties();
-            SaveUserInfo();
-            return true;
+            string? token = await PixiAuthClient.TryClaimSessionToken(User.Email, User.SessionId.Value);
+            if (token != null)
+            {
+                LastError = null;
+                User.SessionToken = token;
+                NotifyProperties();
+                SaveUserInfo();
+                return true;
+            }
+        }
+        catch (BadRequestException ex)
+        {
+            if (ex.Message == "SESSION_NOT_VALIDATED")
+            {
+                LastError = null;
+            }
+        }
+        catch (PixiAuthException authException)
+        {
+            LastError = new LocalizedString(authException.Message);
         }
 
         return false;
     }
 
+    public async Task Logout()
+    {
+        if (!apiValid) return;
+
+        if (!IsLoggedIn)
+        {
+            return;
+        }
+
+        User = null;
+        NotifyProperties();
+        SaveUserInfo();
+        await PixiAuthClient.Logout(User.SessionId.Value, User.SessionToken);
+    }
+
     public async Task SaveUserInfo()
     {
         await IOperatingSystem.Current.SecureStorage.SetValueAsync("UserData", User);

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

@@ -6,6 +6,7 @@
                          xmlns:auth="clr-namespace:PixiEditor.Views.Auth"
                          xmlns:viewModels="clr-namespace:PixiEditor.ViewModels"
                          xmlns:subViewModels="clr-namespace:PixiEditor.ViewModels.SubViewModels"
+                         xmlns:ui="clr-namespace:PixiEditor.Extensions.UI;assembly=PixiEditor.Extensions"
                          mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
                          x:Class="PixiEditor.Views.Auth.LoginPopup"
                          Width="400" Height="300"
@@ -19,5 +20,9 @@
         <TextBlock Text="Logged in as " IsVisible="{Binding IsLoggedIn}">
             <Run Text="{Binding User.Email}"/>
         </TextBlock>
+        <Button IsVisible="{Binding IsLoggedIn}"
+                Command="{Binding Path=LogoutCommand}" Content="Logout" HorizontalAlignment="Right"/>
+        <TextBlock ui:Translator.LocalizedString="{Binding LastError}" IsVisible="{Binding !!LastError}"
+                   Foreground="{DynamicResource ErrorBrush}"/>
     </StackPanel>
 </dialogs:PixiEditorPopup>