diff --git a/Marechai.App/App.xaml.cs b/Marechai.App/App.xaml.cs index 8a6f71c5..b329691d 100644 --- a/Marechai.App/App.xaml.cs +++ b/Marechai.App/App.xaml.cs @@ -2,9 +2,11 @@ using System.Net.Http; using Marechai.App.Presentation.ViewModels; using Marechai.App.Presentation.Views; using Marechai.App.Services; +using Marechai.App.Services.Authentication; using Marechai.App.Services.Caching; using Microsoft.UI.Xaml; using Uno.Extensions; +using Uno.Extensions.Authentication; using Uno.Extensions.Configuration; using Uno.Extensions.Hosting; using Uno.Extensions.Http; @@ -94,6 +96,8 @@ public partial class App : Application .UseLocalization() .UseHttp((context, services) => { + services.AddTransient(); #if DEBUG // DelegatingHandler will be automatically injected @@ -119,6 +123,11 @@ public partial class App : Application .AddSingleton(); + services + .AddSingleton(); + + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Marechai.App/Services/Authentication/AuthService.cs b/Marechai.App/Services/Authentication/AuthService.cs new file mode 100644 index 00000000..f69252ed --- /dev/null +++ b/Marechai.App/Services/Authentication/AuthService.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Refit; +using Uno.Extensions; +using Uno.Extensions.Authentication; + +namespace Marechai.App.Services.Authentication; + +public sealed class AuthService + (ApiClient client, ITokenService tokenService, IStringLocalizer stringLocalizer) : IAuthenticationService +{ + /// + public async ValueTask LoginAsync(IDispatcher? dispatcher, IDictionary? credentials = null, + string? provider = null, CancellationToken? cancellationToken = null) + { + if(credentials is null) return false; + + string? email = credentials.FirstOrDefault(x => x.Key == "Email").Value; + + string? password = credentials.FirstOrDefault(x => x.Key == "Password").Value; + + if(email is null) + { + credentials["error"] = stringLocalizer["Auth.EmailIsRequired"]; + + return false; + } + + var loginModel = new AuthRequest + { + Email = email, + Password = password + }; + + AuthResponse? authResponse; + + try + { + tokenService.RemoveToken(); + authResponse = await client.Auth.Login.PostAsync(loginModel); + } + catch(ValidationApiException ex) + { + switch(ex.StatusCode) + { + case HttpStatusCode.BadRequest: + if(ex.Content is {} problemDetails) + { + if(problemDetails.Errors.Count > 0) + { + credentials["error"] = problemDetails.Errors.FirstOrDefault().Value?.FirstOrDefault() ?? + stringLocalizer["Http.BadRequest"]; + + return false; + } + + credentials["error"] = stringLocalizer["Http.BadRequest"]; + + return false; + } + + break; + } + + credentials["error"] = stringLocalizer["Http.BadRequest"]; + + return false; + } + catch(ApiException ex) + { + switch(ex.StatusCode) + { + case HttpStatusCode.Unauthorized: + credentials["error"] = stringLocalizer["Auth.InvalidCredentials"]; + + return false; + } + + credentials["error"] = stringLocalizer["Http.BadRequest"]; + + return false; + } + catch(Exception ex) + { +#pragma warning disable EPC12 + credentials["error"] = ex.Message; +#pragma warning restore EPC12 + + return false; + } + + if(string.IsNullOrWhiteSpace(authResponse?.Token)) return false; + + tokenService.SetToken(authResponse.Token); + + return true; + } + + /// + public ValueTask RefreshAsync(CancellationToken? cancellationToken = null) => + IsAuthenticated(cancellationToken); + + /// + public async ValueTask LogoutAsync(IDispatcher? dispatcher, CancellationToken? cancellationToken = null) + { + tokenService.RemoveToken(); + LoggedOut?.Invoke(this, EventArgs.Empty); + + return true; + } + + /// + public async ValueTask IsAuthenticated(CancellationToken? cancellationToken = null) + { + string token = tokenService.GetToken(); + + // TODO: Check token validity + return !string.IsNullOrWhiteSpace(token); + } + + /// + public string[] Providers { get; } = []; + /// + public event EventHandler? LoggedOut; +} \ No newline at end of file diff --git a/Marechai.App/Services/Authentication/TokenService.cs b/Marechai.App/Services/Authentication/TokenService.cs new file mode 100644 index 00000000..465320c1 --- /dev/null +++ b/Marechai.App/Services/Authentication/TokenService.cs @@ -0,0 +1,32 @@ +using Windows.Storage; + +namespace Marechai.App.Services.Authentication; + +public interface ITokenService +{ + string GetToken(); + + void RemoveToken(); + + void SetToken(string token); +} + +public sealed class TokenService : ITokenService +{ + readonly ApplicationDataContainer _settings = ApplicationData.Current.LocalSettings; + + /// + public string GetToken() => (string)_settings.Values["token"]; + + /// + public void RemoveToken() + { + _settings.Values.Remove("token"); + } + + /// + public void SetToken(string token) + { + _settings.Values["token"] = token; + } +} \ No newline at end of file diff --git a/Marechai.App/Services/Endpoints/HttpAuthHandler.cs b/Marechai.App/Services/Endpoints/HttpAuthHandler.cs new file mode 100644 index 00000000..96faff88 --- /dev/null +++ b/Marechai.App/Services/Endpoints/HttpAuthHandler.cs @@ -0,0 +1,22 @@ +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Marechai.App.Services.Authentication; + +namespace Marechai.App.Services.Endpoints; + +public sealed class HttpAuthHandler(ITokenService tokenService) : DelegatingHandler +{ + protected override async Task SendAsync(HttpRequestMessage request, + CancellationToken cancellationToken) + { + string token = tokenService.GetToken(); + + if(!string.IsNullOrEmpty(token)) request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); + + HttpResponseMessage response = await base.SendAsync(request, cancellationToken); + + return response; + } +} \ No newline at end of file