From d7e0c5955939a5057db3e2942fc251e996d885ac Mon Sep 17 00:00:00 2001 From: john Date: Sun, 18 May 2025 22:22:20 +0200 Subject: [PATCH] misc --- .../Auth/SessionAuthenticationHandler.cs | 18 ++++-- Femto.Api/Controllers/Auth/AuthController.cs | 54 +++++++++++++++--- .../Auth/CreateSignupCodeRequest.cs | 3 + .../Controllers/Auth/ListSignupCodesResult.cs | 14 +++++ Femto.Api/Controllers/Auth/LoginResponse.cs | 2 +- .../Controllers/Auth/RegisterResponse.cs | 2 +- .../Controllers/Media/MediaController.cs | 2 - Femto.Database/Seed/TestDataSeeder.cs | 10 ++++ Femto.Modules.Auth.Contracts/Role.cs | 7 +++ Femto.Modules.Auth/Application/AuthModule.cs | 22 +++++++- Femto.Modules.Auth/Application/AuthStartup.cs | 18 +++--- .../Application/Dto/LoginResult.cs | 2 +- .../Application/Dto/RegisterResult.cs | 2 +- .../Application/Dto/SignupCodeDto.cs | 10 ++++ .../Application/Dto/UserInfo.cs | 10 ++++ .../Application/Dto/ValidateSessionResult.cs | 2 +- Femto.Modules.Auth/Application/IAuthModule.cs | 4 +- .../CreateSignupCodeCommand.cs | 2 +- .../CreateSignupCodeCommandHandler.cs | 2 +- .../GetSignupCodesQuery.cs | 6 ++ .../GetSignupCodesQueryHandler.cs | 55 +++++++++++++++++++ .../Login/LoginCommand.cs | 2 +- .../Login/LoginCommandHandler.cs | 6 +- .../Register/RegisterCommand.cs | 2 +- .../Register/RegisterCommandHandler.cs | 4 +- .../ValidateSession/ValidateSessionCommand.cs | 2 +- .../ValidateSessionCommandHandler.cs | 5 +- .../UserIdentityTypeConfiguration.cs | 25 ++++++--- Femto.Modules.Auth/Femto.Modules.Auth.csproj | 1 + Femto.Modules.Auth/Models/UserIdentity.cs | 2 + Femto.Modules.Auth/Models/UserRole.cs | 10 ++++ 31 files changed, 249 insertions(+), 57 deletions(-) create mode 100644 Femto.Api/Controllers/Auth/CreateSignupCodeRequest.cs create mode 100644 Femto.Api/Controllers/Auth/ListSignupCodesResult.cs create mode 100644 Femto.Modules.Auth.Contracts/Role.cs create mode 100644 Femto.Modules.Auth/Application/Dto/SignupCodeDto.cs create mode 100644 Femto.Modules.Auth/Application/Dto/UserInfo.cs rename Femto.Modules.Auth/Application/{Commands => Interface}/CreateSignupCode/CreateSignupCodeCommand.cs (66%) rename Femto.Modules.Auth/Application/{Commands => Interface}/CreateSignupCode/CreateSignupCodeCommandHandler.cs (87%) create mode 100644 Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs create mode 100644 Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs rename Femto.Modules.Auth/Application/{Commands => Interface}/Login/LoginCommand.cs (72%) rename Femto.Modules.Auth/Application/{Commands => Interface}/Login/LoginCommandHandler.cs (79%) rename Femto.Modules.Auth/Application/{Commands => Interface}/Register/RegisterCommand.cs (74%) rename Femto.Modules.Auth/Application/{Commands => Interface}/Register/RegisterCommandHandler.cs (87%) rename Femto.Modules.Auth/Application/{Commands => Interface}/ValidateSession/ValidateSessionCommand.cs (84%) rename Femto.Modules.Auth/Application/{Commands => Interface}/ValidateSession/ValidateSessionCommandHandler.cs (89%) create mode 100644 Femto.Modules.Auth/Models/UserRole.cs diff --git a/Femto.Api/Auth/SessionAuthenticationHandler.cs b/Femto.Api/Auth/SessionAuthenticationHandler.cs index 08f1b11..ae282ff 100644 --- a/Femto.Api/Auth/SessionAuthenticationHandler.cs +++ b/Femto.Api/Auth/SessionAuthenticationHandler.cs @@ -3,10 +3,11 @@ using System.Text.Encodings.Web; using Femto.Api.Sessions; using Femto.Common; using Femto.Modules.Auth.Application; -using Femto.Modules.Auth.Application.Commands.ValidateSession; +using Femto.Modules.Auth.Application.Interface.ValidateSession; using Femto.Modules.Auth.Errors; using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Extensions; namespace Femto.Api.Auth; @@ -27,20 +28,25 @@ internal class SessionAuthenticationHandler( try { - var result = await authModule.PostCommand(new ValidateSessionCommand(sessionId)); + var result = await authModule.Command(new ValidateSessionCommand(sessionId)); var claims = new List { - new(ClaimTypes.Name, result.Username), - new("sub", result.UserId.ToString()), - new("user_id", result.UserId.ToString()), + new(ClaimTypes.Name, result.User.Username), + new("sub", result.User.Id.ToString()), + new("user_id", result.User.Id.ToString()), }; + claims.AddRange( + result.User.Roles + .Select(role => new Claim(ClaimTypes.Role, role.ToString())) + ); + var identity = new ClaimsIdentity(claims, this.Scheme.Name); var principal = new ClaimsPrincipal(identity); this.Context.SetSession(result.Session, cookieOptions.Value); - currentUserContext.CurrentUser = new CurrentUser(result.UserId, result.Username); + currentUserContext.CurrentUser = new CurrentUser(result.User.Id, result.User.Username); return AuthenticateResult.Success( new AuthenticationTicket(principal, this.Scheme.Name) diff --git a/Femto.Api/Controllers/Auth/AuthController.cs b/Femto.Api/Controllers/Auth/AuthController.cs index 2021692..faa178e 100644 --- a/Femto.Api/Controllers/Auth/AuthController.cs +++ b/Femto.Api/Controllers/Auth/AuthController.cs @@ -1,8 +1,12 @@ using Femto.Api.Auth; using Femto.Api.Sessions; using Femto.Modules.Auth.Application; -using Femto.Modules.Auth.Application.Commands.Login; -using Femto.Modules.Auth.Application.Commands.Register; +using Femto.Modules.Auth.Application.Interface.CreateSignupCode; +using Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; +using Femto.Modules.Auth.Application.Interface.Login; +using Femto.Modules.Auth.Application.Interface.Register; +using Femto.Modules.Auth.Contracts; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; @@ -10,30 +14,29 @@ namespace Femto.Api.Controllers.Auth; [ApiController] [Route("auth")] -public class AuthController(IAuthModule authModule, IOptions cookieSettings) : ControllerBase +public class AuthController(IAuthModule authModule, IOptions cookieSettings) + : ControllerBase { [HttpPost("login")] public async Task> Login([FromBody] LoginRequest request) { - var result = await authModule.PostCommand( - new LoginCommand(request.Username, request.Password) - ); + var result = await authModule.Command(new LoginCommand(request.Username, request.Password)); HttpContext.SetSession(result.Session, cookieSettings.Value); - return new LoginResponse(result.UserId, result.Username); + return new LoginResponse(result.User.Id, result.User.Username, result.User.Roles.Any(r => r == Role.SuperUser)); } [HttpPost("register")] public async Task> Register([FromBody] RegisterRequest request) { - var result = await authModule.PostCommand( + var result = await authModule.Command( new RegisterCommand(request.Username, request.Password, request.SignupCode) ); HttpContext.SetSession(result.Session, cookieSettings.Value); - return new RegisterResponse(result.UserId, result.Username); + return new RegisterResponse(result.User.Id, result.User.Username, result.User.Roles.Any(r => r == Role.SuperUser)); } [HttpDelete("session")] @@ -42,4 +45,37 @@ public class AuthController(IAuthModule authModule, IOptions coo HttpContext.Response.Cookies.Delete("session"); return Ok(new { }); } + + [HttpPost("signup-codes")] + [Authorize(Roles = "SuperUser")] + public async Task CreateSignupCode( + [FromBody] CreateSignupCodeRequest request, + CancellationToken cancellationToken + ) + { + await authModule.Command( + new CreateSignupCodeCommand(request.Code, request.Email, request.Name), + cancellationToken + ); + + return Ok(new { }); + } + + [HttpGet("signup-codes")] + [Authorize(Roles = "SuperUser")] + public async Task> ListSignupCodes(CancellationToken cancellationToken) + { + var codes = await authModule.Query(new GetSignupCodesQuery(), cancellationToken); + + return new ListSignupCodesResult( + codes.Select(c => new SignupCodeDto( + c.Code, + c.Email, + c.Name, + c.RedeemedByUserId, + c.RedeemedByUsername, + c.ExpiresOn + )) + ); + } } diff --git a/Femto.Api/Controllers/Auth/CreateSignupCodeRequest.cs b/Femto.Api/Controllers/Auth/CreateSignupCodeRequest.cs new file mode 100644 index 0000000..6d80ce1 --- /dev/null +++ b/Femto.Api/Controllers/Auth/CreateSignupCodeRequest.cs @@ -0,0 +1,3 @@ +namespace Femto.Api.Controllers.Auth; + +public record CreateSignupCodeRequest(string Code, string Email, string Name); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/ListSignupCodesResult.cs b/Femto.Api/Controllers/Auth/ListSignupCodesResult.cs new file mode 100644 index 0000000..b80f82b --- /dev/null +++ b/Femto.Api/Controllers/Auth/ListSignupCodesResult.cs @@ -0,0 +1,14 @@ +namespace Femto.Api.Controllers.Auth; + +public record ListSignupCodesResult( + IEnumerable SignupCodes +); + +public record SignupCodeDto( + string Code, + string Email, + string Name, + Guid? RedeemingUserId, + string? RedeemingUsername, + DateTimeOffset? ExpiresOn +); diff --git a/Femto.Api/Controllers/Auth/LoginResponse.cs b/Femto.Api/Controllers/Auth/LoginResponse.cs index ba83e7f..1736c4e 100644 --- a/Femto.Api/Controllers/Auth/LoginResponse.cs +++ b/Femto.Api/Controllers/Auth/LoginResponse.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Auth; -public record LoginResponse(Guid UserId, string Username); \ No newline at end of file +public record LoginResponse(Guid UserId, string Username, bool IsSuperUser); \ No newline at end of file diff --git a/Femto.Api/Controllers/Auth/RegisterResponse.cs b/Femto.Api/Controllers/Auth/RegisterResponse.cs index 17de500..7f70e83 100644 --- a/Femto.Api/Controllers/Auth/RegisterResponse.cs +++ b/Femto.Api/Controllers/Auth/RegisterResponse.cs @@ -1,3 +1,3 @@ namespace Femto.Api.Controllers.Auth; -public record RegisterResponse(Guid UserId, string Username); \ No newline at end of file +public record RegisterResponse(Guid UserId, string Username, bool IsSuperUser); \ No newline at end of file diff --git a/Femto.Api/Controllers/Media/MediaController.cs b/Femto.Api/Controllers/Media/MediaController.cs index 32675ca..36bbcc1 100644 --- a/Femto.Api/Controllers/Media/MediaController.cs +++ b/Femto.Api/Controllers/Media/MediaController.cs @@ -1,9 +1,7 @@ using Femto.Api.Controllers.Media.Dto; using Femto.Modules.Media.Application; -using Femto.Modules.Media.Contracts; using Femto.Modules.Media.Contracts.LoadFile; using Femto.Modules.Media.Contracts.SaveFile; -using MediatR; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/Femto.Database/Seed/TestDataSeeder.cs b/Femto.Database/Seed/TestDataSeeder.cs index 7b0cb15..f180b8f 100644 --- a/Femto.Database/Seed/TestDataSeeder.cs +++ b/Femto.Database/Seed/TestDataSeeder.cs @@ -58,6 +58,16 @@ public static class TestDataSeeder (id, username, password_hash, password_salt) VALUES (@id, @username, @passwordHash, @salt); + + INSERT INTO authn.user_role + (user_id, role) + VALUES + (@id, 1); + + INSERT INTO authn.signup_code + (code, recipient_email, recipient_name, expires_at, redeeming_user_id) + VALUES + ('fickli', 'me@johnbotr.is', 'john', null, null); """ ); diff --git a/Femto.Modules.Auth.Contracts/Role.cs b/Femto.Modules.Auth.Contracts/Role.cs new file mode 100644 index 0000000..3aea88d --- /dev/null +++ b/Femto.Modules.Auth.Contracts/Role.cs @@ -0,0 +1,7 @@ +namespace Femto.Modules.Auth.Contracts; + +public enum Role +{ + User = 0, + SuperUser = 1, +} \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/AuthModule.cs b/Femto.Modules.Auth/Application/AuthModule.cs index 1ac5fdb..03b34d3 100644 --- a/Femto.Modules.Auth/Application/AuthModule.cs +++ b/Femto.Modules.Auth/Application/AuthModule.cs @@ -7,11 +7,29 @@ namespace Femto.Modules.Auth.Application; internal class AuthModule(IHost host) : IAuthModule { - public async Task PostCommand(ICommand command, CancellationToken cancellationToken = default) + public Task Command(ICommand command, CancellationToken cancellationToken = default) + { + using var scope = host.Services.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + return mediator.Send(command, cancellationToken); + } + + public async Task Command( + ICommand command, + CancellationToken cancellationToken = default + ) { using var scope = host.Services.CreateScope(); var mediator = scope.ServiceProvider.GetRequiredService(); var response = await mediator.Send(command, cancellationToken); return response; } -} \ No newline at end of file + + public async Task Query(IQuery query, CancellationToken cancellationToken = default) + { + using var scope = host.Services.CreateScope(); + var mediator = scope.ServiceProvider.GetRequiredService(); + var response = await mediator.Send(query, cancellationToken); + return response; + } +} diff --git a/Femto.Modules.Auth/Application/AuthStartup.cs b/Femto.Modules.Auth/Application/AuthStartup.cs index 1328e39..4cad89b 100644 --- a/Femto.Modules.Auth/Application/AuthStartup.cs +++ b/Femto.Modules.Auth/Application/AuthStartup.cs @@ -1,4 +1,5 @@ using Femto.Common.Infrastructure; +using Femto.Common.Infrastructure.DbConnection; using Femto.Common.Infrastructure.Outbox; using Femto.Common.Integration; using Femto.Modules.Auth.Data; @@ -27,12 +28,8 @@ public static class AuthStartup private static void ConfigureServices(IServiceCollection services, string connectionString, IEventPublisher publisher) { - services.AddDbContext(builder => - { - builder.UseNpgsql(connectionString); - builder.UseSnakeCaseNamingConvention(); - }); - + services.AddTransient(_ => new DbConnectionFactory(connectionString)); + services.AddQuartzHostedService(options => { options.WaitForJobsToComplete = true; @@ -44,10 +41,13 @@ public static class AuthStartup services.AddDbContext(builder => { - builder.UseNpgsql(); + builder.UseNpgsql(connectionString); builder.UseSnakeCaseNamingConvention(); - var loggerFactory = LoggerFactory.Create(b => { }); - builder.UseLoggerFactory(loggerFactory); + // var loggerFactory = LoggerFactory.Create(b => { }); + // builder.UseLoggerFactory(loggerFactory); + // #if DEBUG + // builder.EnableSensitiveDataLogging(); + // #endif }); services.ConfigureDomainServices(); diff --git a/Femto.Modules.Auth/Application/Dto/LoginResult.cs b/Femto.Modules.Auth/Application/Dto/LoginResult.cs index e56fdc1..1405a28 100644 --- a/Femto.Modules.Auth/Application/Dto/LoginResult.cs +++ b/Femto.Modules.Auth/Application/Dto/LoginResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Auth.Application.Dto; -public record LoginResult(Session Session, Guid UserId, string Username); \ No newline at end of file +public record LoginResult(Session Session, UserInfo User); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/RegisterResult.cs b/Femto.Modules.Auth/Application/Dto/RegisterResult.cs index 1c9b467..13e1d12 100644 --- a/Femto.Modules.Auth/Application/Dto/RegisterResult.cs +++ b/Femto.Modules.Auth/Application/Dto/RegisterResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Auth.Application.Dto; -public record RegisterResult(Session Session, Guid UserId, string Username); \ No newline at end of file +public record RegisterResult(Session Session, UserInfo User); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Dto/SignupCodeDto.cs b/Femto.Modules.Auth/Application/Dto/SignupCodeDto.cs new file mode 100644 index 0000000..d1d7e1f --- /dev/null +++ b/Femto.Modules.Auth/Application/Dto/SignupCodeDto.cs @@ -0,0 +1,10 @@ +namespace Femto.Modules.Auth.Application.Dto; + +public record SignupCodeDto( + string Code, + string Email, + string Name, + Guid? RedeemedByUserId, + string? RedeemedByUsername, + DateTimeOffset? ExpiresOn +); diff --git a/Femto.Modules.Auth/Application/Dto/UserInfo.cs b/Femto.Modules.Auth/Application/Dto/UserInfo.cs new file mode 100644 index 0000000..fe1a6ec --- /dev/null +++ b/Femto.Modules.Auth/Application/Dto/UserInfo.cs @@ -0,0 +1,10 @@ +using Femto.Modules.Auth.Contracts; +using Femto.Modules.Auth.Models; + +namespace Femto.Modules.Auth.Application.Dto; + +public record UserInfo(Guid Id, string Username, ICollection Roles) +{ + internal UserInfo(UserIdentity user) + : this(user.Id, user.Username, user.Roles.Select(r => r.Role).ToList()) { } +}; diff --git a/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs b/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs index 9e22c1e..7fb022f 100644 --- a/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs +++ b/Femto.Modules.Auth/Application/Dto/ValidateSessionResult.cs @@ -1,3 +1,3 @@ namespace Femto.Modules.Auth.Application.Dto; -public record ValidateSessionResult(Session Session, Guid UserId, string Username); \ No newline at end of file +public record ValidateSessionResult(Session Session, UserInfo User); \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/IAuthModule.cs b/Femto.Modules.Auth/Application/IAuthModule.cs index e9c2f4b..4559161 100644 --- a/Femto.Modules.Auth/Application/IAuthModule.cs +++ b/Femto.Modules.Auth/Application/IAuthModule.cs @@ -4,5 +4,7 @@ namespace Femto.Modules.Auth.Application; public interface IAuthModule { - Task PostCommand(ICommand command, CancellationToken cancellationToken = default); + Task Command(ICommand command, CancellationToken cancellationToken = default); + Task Command(ICommand command, CancellationToken cancellationToken = default); + Task Query(IQuery query, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/CreateSignupCode/CreateSignupCodeCommand.cs b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs similarity index 66% rename from Femto.Modules.Auth/Application/Commands/CreateSignupCode/CreateSignupCodeCommand.cs rename to Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs index 5b40c6a..be24aa9 100644 --- a/Femto.Modules.Auth/Application/Commands/CreateSignupCode/CreateSignupCodeCommand.cs +++ b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommand.cs @@ -1,5 +1,5 @@ using Femto.Common.Domain; -namespace Femto.Modules.Auth.Application.Commands.CreateSignupCode; +namespace Femto.Modules.Auth.Application.Interface.CreateSignupCode; public record CreateSignupCodeCommand(string Code, string RecipientEmail, string RecipientName): ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/CreateSignupCode/CreateSignupCodeCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs similarity index 87% rename from Femto.Modules.Auth/Application/Commands/CreateSignupCode/CreateSignupCodeCommandHandler.cs rename to Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs index ca08bdc..cfbb44a 100644 --- a/Femto.Modules.Auth/Application/Commands/CreateSignupCode/CreateSignupCodeCommandHandler.cs +++ b/Femto.Modules.Auth/Application/Interface/CreateSignupCode/CreateSignupCodeCommandHandler.cs @@ -2,7 +2,7 @@ using Femto.Common.Domain; using Femto.Modules.Auth.Data; using Femto.Modules.Auth.Models; -namespace Femto.Modules.Auth.Application.Commands.CreateSignupCode; +namespace Femto.Modules.Auth.Application.Interface.CreateSignupCode; internal class CreateSignupCodeCommandHandler(AuthContext context) : ICommandHandler { diff --git a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs new file mode 100644 index 0000000..422a09d --- /dev/null +++ b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQuery.cs @@ -0,0 +1,6 @@ +using Femto.Common.Domain; +using Femto.Modules.Auth.Application.Dto; + +namespace Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; + +public record GetSignupCodesQuery: IQuery>; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs new file mode 100644 index 0000000..4fd557c --- /dev/null +++ b/Femto.Modules.Auth/Application/Interface/GetSignupCodesQuery/GetSignupCodesQueryHandler.cs @@ -0,0 +1,55 @@ +using Dapper; +using Femto.Common.Domain; +using Femto.Common.Infrastructure.DbConnection; +using Femto.Modules.Auth.Application.Dto; + +namespace Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery; + +public class GetSignupCodesQueryHandler(IDbConnectionFactory connectionFactory) + : IQueryHandler> +{ + public async Task> Handle( + GetSignupCodesQuery request, + CancellationToken cancellationToken + ) + { + using var conn = connectionFactory.GetConnection(); + + // lang=sql + const string sql = """ + SELECT + sc.code as Code, + sc.recipient_email as Email, + sc.recipient_name as Name, + sc.redeeming_user_id as RedeemedByUserId, + u.username as RedeemedByUsername, + sc.expires_at as ExpiresOn + FROM authn.signup_code sc + LEFT JOIN authn.user_identity u ON u.id = sc.redeeming_user_id + ORDER BY sc.created_at DESC + """; + + var result = await conn.QueryAsync(sql); + + return result + .Select(row => new SignupCodeDto( + row.Code, + row.Email, + row.Name, + row.RedeemedByUserId, + row.RedeemedByUsername, + row.ExpiresOn + )) + .ToList(); + } + + private class QueryResultRow + { + public string Code { get; set; } + public string Email { get; set; } + public string Name { get; set; } + public Guid? RedeemedByUserId { get; set; } + public string? RedeemedByUsername { get; set; } + public DateTimeOffset ExpiresOn { get; set; } + } +} diff --git a/Femto.Modules.Auth/Application/Commands/Login/LoginCommand.cs b/Femto.Modules.Auth/Application/Interface/Login/LoginCommand.cs similarity index 72% rename from Femto.Modules.Auth/Application/Commands/Login/LoginCommand.cs rename to Femto.Modules.Auth/Application/Interface/Login/LoginCommand.cs index 58c3e5c..8252e2e 100644 --- a/Femto.Modules.Auth/Application/Commands/Login/LoginCommand.cs +++ b/Femto.Modules.Auth/Application/Interface/Login/LoginCommand.cs @@ -1,6 +1,6 @@ using Femto.Common.Domain; using Femto.Modules.Auth.Application.Dto; -namespace Femto.Modules.Auth.Application.Commands.Login; +namespace Femto.Modules.Auth.Application.Interface.Login; public record LoginCommand(string Username, string Password) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/Login/LoginCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/Login/LoginCommandHandler.cs similarity index 79% rename from Femto.Modules.Auth/Application/Commands/Login/LoginCommandHandler.cs rename to Femto.Modules.Auth/Application/Interface/Login/LoginCommandHandler.cs index 633867f..45b1ae4 100644 --- a/Femto.Modules.Auth/Application/Commands/Login/LoginCommandHandler.cs +++ b/Femto.Modules.Auth/Application/Interface/Login/LoginCommandHandler.cs @@ -1,11 +1,9 @@ using Femto.Common.Domain; using Femto.Modules.Auth.Application.Dto; using Femto.Modules.Auth.Data; -using Femto.Modules.Auth.Models; -using MediatR; using Microsoft.EntityFrameworkCore; -namespace Femto.Modules.Auth.Application.Commands.Login; +namespace Femto.Modules.Auth.Application.Interface.Login; internal class LoginCommandHandler(AuthContext context) : ICommandHandler @@ -25,6 +23,6 @@ internal class LoginCommandHandler(AuthContext context) var session = user.StartNewSession(); - return new(new Session(session.Id, session.Expires), user.Id, user.Username); + return new(new Session(session.Id, session.Expires), new UserInfo(user)); } } diff --git a/Femto.Modules.Auth/Application/Commands/Register/RegisterCommand.cs b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs similarity index 74% rename from Femto.Modules.Auth/Application/Commands/Register/RegisterCommand.cs rename to Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs index ac76f00..dd3c186 100644 --- a/Femto.Modules.Auth/Application/Commands/Register/RegisterCommand.cs +++ b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommand.cs @@ -1,6 +1,6 @@ using Femto.Common.Domain; using Femto.Modules.Auth.Application.Dto; -namespace Femto.Modules.Auth.Application.Commands.Register; +namespace Femto.Modules.Auth.Application.Interface.Register; public record RegisterCommand(string Username, string Password, string SignupCode) : ICommand; \ No newline at end of file diff --git a/Femto.Modules.Auth/Application/Commands/Register/RegisterCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs similarity index 87% rename from Femto.Modules.Auth/Application/Commands/Register/RegisterCommandHandler.cs rename to Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs index 2a7d88d..9e29be6 100644 --- a/Femto.Modules.Auth/Application/Commands/Register/RegisterCommandHandler.cs +++ b/Femto.Modules.Auth/Application/Interface/Register/RegisterCommandHandler.cs @@ -4,7 +4,7 @@ using Femto.Modules.Auth.Data; using Femto.Modules.Auth.Models; using Microsoft.EntityFrameworkCore; -namespace Femto.Modules.Auth.Application.Commands.Register; +namespace Femto.Modules.Auth.Application.Interface.Register; internal class RegisterCommandHandler(AuthContext context) : ICommandHandler { @@ -30,6 +30,6 @@ internal class RegisterCommandHandler(AuthContext context) : ICommandHandler /// Validate an existing session, and then return either the current session, or a new one in case the expiry is further in the future diff --git a/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommandHandler.cs b/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommandHandler.cs similarity index 89% rename from Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommandHandler.cs rename to Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommandHandler.cs index 129cf23..f79552c 100644 --- a/Femto.Modules.Auth/Application/Commands/ValidateSession/ValidateSessionCommandHandler.cs +++ b/Femto.Modules.Auth/Application/Interface/ValidateSession/ValidateSessionCommandHandler.cs @@ -4,7 +4,7 @@ using Femto.Modules.Auth.Data; using Femto.Modules.Auth.Errors; using Microsoft.EntityFrameworkCore; -namespace Femto.Modules.Auth.Application.Commands.ValidateSession; +namespace Femto.Modules.Auth.Application.Interface.ValidateSession; internal class ValidateSessionCommandHandler(AuthContext context) : ICommandHandler @@ -28,8 +28,7 @@ internal class ValidateSessionCommandHandler(AuthContext context) return new ValidateSessionResult( new Session(session.Id, session.Expires), - user.Id, - user.Username + new UserInfo(user) ); } } diff --git a/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs b/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs index 2be1295..2e5086b 100644 --- a/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs +++ b/Femto.Modules.Auth/Data/Configurations/UserIdentityTypeConfiguration.cs @@ -9,16 +9,23 @@ internal class UserIdentityTypeConfiguration : IEntityTypeConfiguration builder) { builder.ToTable("user_identity"); - builder.OwnsOne(u => u.Password, pw => - { - pw.Property(p => p.Hash) - .HasColumnName("password_hash") - .IsRequired(false); + builder.OwnsOne( + u => u.Password, + pw => + { + pw.Property(p => p.Hash).HasColumnName("password_hash").IsRequired(false); + + pw.Property(p => p.Salt).HasColumnName("password_salt").IsRequired(false); + } + ); - pw.Property(p => p.Salt) - .HasColumnName("password_salt") - .IsRequired(false); - }); builder.OwnsMany(u => u.Sessions).WithOwner().HasForeignKey("user_id"); + + builder + .OwnsMany(u => u.Roles, entity => + { + entity.WithOwner().HasForeignKey(x => x.UserId); + entity.HasKey(x => new { x.UserId, x.Role}); + }); } } diff --git a/Femto.Modules.Auth/Femto.Modules.Auth.csproj b/Femto.Modules.Auth/Femto.Modules.Auth.csproj index 3bb2c77..74d6429 100644 --- a/Femto.Modules.Auth/Femto.Modules.Auth.csproj +++ b/Femto.Modules.Auth/Femto.Modules.Auth.csproj @@ -8,6 +8,7 @@ + diff --git a/Femto.Modules.Auth/Models/UserIdentity.cs b/Femto.Modules.Auth/Models/UserIdentity.cs index 871c7f3..587859d 100644 --- a/Femto.Modules.Auth/Models/UserIdentity.cs +++ b/Femto.Modules.Auth/Models/UserIdentity.cs @@ -15,6 +15,8 @@ internal class UserIdentity : Entity public Password? Password { get; private set; } public ICollection Sessions { get; private set; } = []; + + public ICollection Roles { get; private set; } = []; private UserIdentity() { } diff --git a/Femto.Modules.Auth/Models/UserRole.cs b/Femto.Modules.Auth/Models/UserRole.cs new file mode 100644 index 0000000..a00b52d --- /dev/null +++ b/Femto.Modules.Auth/Models/UserRole.cs @@ -0,0 +1,10 @@ +using Femto.Modules.Auth.Contracts; + +namespace Femto.Modules.Auth.Models; + +internal class UserRole +{ + public Guid UserId { get; set; } + + public Role Role { get; set; } +} \ No newline at end of file