refresh user
This commit is contained in:
parent
4e24796a5d
commit
0d34774059
12 changed files with 141 additions and 32 deletions
|
@ -46,7 +46,7 @@ internal class SessionAuthenticationHandler(
|
||||||
var principal = new ClaimsPrincipal(identity);
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
this.Context.SetSession(result.Session, cookieOptions.Value);
|
this.Context.SetSession(result.Session, cookieOptions.Value);
|
||||||
currentUserContext.CurrentUser = new CurrentUser(result.User.Id, result.User.Username);
|
currentUserContext.CurrentUser = new CurrentUser(result.User.Id, result.User.Username, result.Session.SessionId);
|
||||||
|
|
||||||
return AuthenticateResult.Success(
|
return AuthenticateResult.Success(
|
||||||
new AuthenticationTicket(principal, this.Scheme.Name)
|
new AuthenticationTicket(principal, this.Scheme.Name)
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
using Femto.Api.Auth;
|
using Femto.Api.Auth;
|
||||||
using Femto.Api.Sessions;
|
using Femto.Api.Sessions;
|
||||||
|
using Femto.Common;
|
||||||
using Femto.Modules.Auth.Application;
|
using Femto.Modules.Auth.Application;
|
||||||
|
using Femto.Modules.Auth.Application.Dto;
|
||||||
using Femto.Modules.Auth.Application.Interface.CreateSignupCode;
|
using Femto.Modules.Auth.Application.Interface.CreateSignupCode;
|
||||||
using Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery;
|
using Femto.Modules.Auth.Application.Interface.GetSignupCodesQuery;
|
||||||
using Femto.Modules.Auth.Application.Interface.Login;
|
using Femto.Modules.Auth.Application.Interface.Login;
|
||||||
|
using Femto.Modules.Auth.Application.Interface.RefreshUserSession;
|
||||||
using Femto.Modules.Auth.Application.Interface.Register;
|
using Femto.Modules.Auth.Application.Interface.Register;
|
||||||
using Femto.Modules.Auth.Contracts;
|
using Femto.Modules.Auth.Contracts;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
@ -14,8 +17,11 @@ namespace Femto.Api.Controllers.Auth;
|
||||||
|
|
||||||
[ApiController]
|
[ApiController]
|
||||||
[Route("auth")]
|
[Route("auth")]
|
||||||
public class AuthController(IAuthModule authModule, IOptions<CookieSettings> cookieSettings)
|
public class AuthController(
|
||||||
: ControllerBase
|
IAuthModule authModule,
|
||||||
|
IOptions<CookieSettings> cookieSettings,
|
||||||
|
ICurrentUserContext currentUserContext
|
||||||
|
) : ControllerBase
|
||||||
{
|
{
|
||||||
[HttpPost("login")]
|
[HttpPost("login")]
|
||||||
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest request)
|
public async Task<ActionResult<LoginResponse>> Login([FromBody] LoginRequest request)
|
||||||
|
@ -24,7 +30,11 @@ public class AuthController(IAuthModule authModule, IOptions<CookieSettings> coo
|
||||||
|
|
||||||
HttpContext.SetSession(result.Session, cookieSettings.Value);
|
HttpContext.SetSession(result.Session, cookieSettings.Value);
|
||||||
|
|
||||||
return new LoginResponse(result.User.Id, result.User.Username, result.User.Roles.Any(r => r == Role.SuperUser));
|
return new LoginResponse(
|
||||||
|
result.User.Id,
|
||||||
|
result.User.Username,
|
||||||
|
result.User.Roles.Any(r => r == Role.SuperUser)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("register")]
|
[HttpPost("register")]
|
||||||
|
@ -36,16 +46,49 @@ public class AuthController(IAuthModule authModule, IOptions<CookieSettings> coo
|
||||||
|
|
||||||
HttpContext.SetSession(result.Session, cookieSettings.Value);
|
HttpContext.SetSession(result.Session, cookieSettings.Value);
|
||||||
|
|
||||||
return new RegisterResponse(result.User.Id, result.User.Username, result.User.Roles.Any(r => r == Role.SuperUser));
|
return new RegisterResponse(
|
||||||
|
result.User.Id,
|
||||||
|
result.User.Username,
|
||||||
|
result.User.Roles.Any(r => r == Role.SuperUser)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpDelete("session")]
|
[HttpDelete("session")]
|
||||||
public async Task<ActionResult> DeleteSession()
|
public async Task<ActionResult> DeleteSession()
|
||||||
{
|
{
|
||||||
HttpContext.Response.Cookies.Delete("session");
|
HttpContext.DeleteSession();
|
||||||
return Ok(new { });
|
return Ok(new { });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpGet("user/{userId}")]
|
||||||
|
[Authorize]
|
||||||
|
public async Task<ActionResult<RefreshUserResult>> RefreshUser(
|
||||||
|
Guid userId,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
var currentUser = currentUserContext.CurrentUser!;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await authModule.Command(
|
||||||
|
new RefreshUserSessionCommand(userId, currentUser),
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
return new RefreshUserResult(
|
||||||
|
result.User.Id,
|
||||||
|
result.User.Username,
|
||||||
|
result.User.Roles.Any(r => r == Role.SuperUser)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
HttpContext.DeleteSession();
|
||||||
|
return this.Forbid();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("signup-codes")]
|
[HttpPost("signup-codes")]
|
||||||
[Authorize(Roles = "SuperUser")]
|
[Authorize(Roles = "SuperUser")]
|
||||||
public async Task<ActionResult> CreateSignupCode(
|
public async Task<ActionResult> CreateSignupCode(
|
||||||
|
@ -63,7 +106,9 @@ public class AuthController(IAuthModule authModule, IOptions<CookieSettings> coo
|
||||||
|
|
||||||
[HttpGet("signup-codes")]
|
[HttpGet("signup-codes")]
|
||||||
[Authorize(Roles = "SuperUser")]
|
[Authorize(Roles = "SuperUser")]
|
||||||
public async Task<ActionResult<ListSignupCodesResult>> ListSignupCodes(CancellationToken cancellationToken)
|
public async Task<ActionResult<ListSignupCodesResult>> ListSignupCodes(
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
{
|
{
|
||||||
var codes = await authModule.Query(new GetSignupCodesQuery(), cancellationToken);
|
var codes = await authModule.Query(new GetSignupCodesQuery(), cancellationToken);
|
||||||
|
|
||||||
|
|
3
Femto.Api/Controllers/Auth/RefreshUserResult.cs
Normal file
3
Femto.Api/Controllers/Auth/RefreshUserResult.cs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
namespace Femto.Api.Controllers.Auth;
|
||||||
|
|
||||||
|
public record RefreshUserResult(Guid UserId, string Username, bool IsSuperUser);
|
|
@ -34,4 +34,10 @@ internal static class HttpContextSessionExtensions
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void DeleteSession(this HttpContext httpContext)
|
||||||
|
{
|
||||||
|
httpContext.Response.Cookies.Delete("session");
|
||||||
|
httpContext.Response.Cookies.Delete("hasSession");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,4 +5,4 @@ public interface ICurrentUserContext
|
||||||
CurrentUser? CurrentUser { get; }
|
CurrentUser? CurrentUser { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public record CurrentUser(Guid Id, string Username);
|
public record CurrentUser(Guid Id, string Username, string SessionId);
|
||||||
|
|
|
@ -5,31 +5,18 @@ using Microsoft.Extensions.Hosting;
|
||||||
|
|
||||||
namespace Femto.Modules.Auth.Application;
|
namespace Femto.Modules.Auth.Application;
|
||||||
|
|
||||||
internal class AuthModule(IHost host) : IAuthModule
|
internal class AuthModule(IMediator mediator) : IAuthModule
|
||||||
{
|
{
|
||||||
public async Task Command(ICommand command, CancellationToken cancellationToken = default)
|
public async Task Command(ICommand command, CancellationToken cancellationToken = default) =>
|
||||||
{
|
|
||||||
using var scope = host.Services.CreateScope();
|
|
||||||
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
|
||||||
await mediator.Send(command, cancellationToken);
|
await mediator.Send(command, cancellationToken);
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TResponse> Command<TResponse>(
|
public async Task<TResponse> Command<TResponse>(
|
||||||
ICommand<TResponse> command,
|
ICommand<TResponse> command,
|
||||||
CancellationToken cancellationToken = default
|
CancellationToken cancellationToken = default
|
||||||
)
|
) => await mediator.Send(command, cancellationToken);
|
||||||
{
|
|
||||||
using var scope = host.Services.CreateScope();
|
|
||||||
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
|
||||||
var response = await mediator.Send(command, cancellationToken);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<TResponse> Query<TResponse>(IQuery<TResponse> query, CancellationToken cancellationToken = default)
|
public async Task<TResponse> Query<TResponse>(
|
||||||
{
|
IQuery<TResponse> query,
|
||||||
using var scope = host.Services.CreateScope();
|
CancellationToken cancellationToken = default
|
||||||
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
) => await mediator.Send(query, cancellationToken);
|
||||||
var response = await mediator.Send(query, cancellationToken);
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,12 @@ public static class AuthStartup
|
||||||
var hostBuilder = Host.CreateDefaultBuilder();
|
var hostBuilder = Host.CreateDefaultBuilder();
|
||||||
hostBuilder.ConfigureServices(services => ConfigureServices(services, connectionString, eventBus));
|
hostBuilder.ConfigureServices(services => ConfigureServices(services, connectionString, eventBus));
|
||||||
var host = hostBuilder.Build();
|
var host = hostBuilder.Build();
|
||||||
rootContainer.AddScoped<IAuthModule>(_ => new AuthModule(host));
|
|
||||||
|
rootContainer.AddScoped(_ => new ScopeBinding(host.Services.CreateScope()));
|
||||||
|
|
||||||
|
rootContainer.AddScoped<IAuthModule>(services =>
|
||||||
|
services.GetRequiredService<ScopeBinding>().GetService<IAuthModule>());
|
||||||
|
|
||||||
rootContainer.AddHostedService(services => new AuthApplication(host));
|
rootContainer.AddHostedService(services => new AuthApplication(host));
|
||||||
eventBus.Subscribe((evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken));
|
eventBus.Subscribe((evt, cancellationToken) => EventSubscriber(evt, host.Services, cancellationToken));
|
||||||
}
|
}
|
||||||
|
@ -50,11 +55,11 @@ public static class AuthStartup
|
||||||
|
|
||||||
services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(AuthStartup).Assembly));
|
services.AddMediatR(c => c.RegisterServicesFromAssembly(typeof(AuthStartup).Assembly));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
services.ConfigureDomainServices<AuthContext>();
|
services.ConfigureDomainServices<AuthContext>();
|
||||||
|
|
||||||
services.AddSingleton(publisher);
|
services.AddSingleton(publisher);
|
||||||
|
|
||||||
|
services.AddScoped<IAuthModule, AuthModule>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task EventSubscriber(
|
private static async Task EventSubscriber(
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
namespace Femto.Modules.Auth.Application.Dto;
|
||||||
|
|
||||||
|
public record RefreshUserSessionResult(Session Session, UserInfo User);
|
|
@ -2,4 +2,9 @@ using Femto.Modules.Auth.Models;
|
||||||
|
|
||||||
namespace Femto.Modules.Auth.Application.Dto;
|
namespace Femto.Modules.Auth.Application.Dto;
|
||||||
|
|
||||||
public record Session(string SessionId, DateTimeOffset Expires);
|
public record Session(string SessionId, DateTimeOffset Expires)
|
||||||
|
{
|
||||||
|
internal Session(UserSession session) : this(session.Id, session.Expires)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
using Femto.Common;
|
||||||
|
using Femto.Common.Domain;
|
||||||
|
using Femto.Modules.Auth.Application.Dto;
|
||||||
|
|
||||||
|
namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession;
|
||||||
|
|
||||||
|
public record RefreshUserSessionCommand(Guid ForUser, CurrentUser CurrentUser) : ICommand<RefreshUserSessionResult>;
|
|
@ -0,0 +1,32 @@
|
||||||
|
using Femto.Common.Domain;
|
||||||
|
using Femto.Common.Infrastructure.DbConnection;
|
||||||
|
using Femto.Modules.Auth.Application.Dto;
|
||||||
|
using Femto.Modules.Auth.Data;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Femto.Modules.Auth.Application.Interface.RefreshUserSession;
|
||||||
|
|
||||||
|
internal class RefreshUserSessionCommandHandler(AuthContext context)
|
||||||
|
: ICommandHandler<RefreshUserSessionCommand, RefreshUserSessionResult>
|
||||||
|
{
|
||||||
|
public async Task<RefreshUserSessionResult> Handle(
|
||||||
|
RefreshUserSessionCommand request,
|
||||||
|
CancellationToken cancellationToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (request.CurrentUser.Id != request.ForUser)
|
||||||
|
throw new DomainError("invalid request");
|
||||||
|
|
||||||
|
var user = await context.Users.SingleOrDefaultAsync(
|
||||||
|
u => u.Id == request.ForUser,
|
||||||
|
cancellationToken
|
||||||
|
);
|
||||||
|
|
||||||
|
if (user is null)
|
||||||
|
throw new DomainError("invalid request");
|
||||||
|
|
||||||
|
var session = user.PossiblyRefreshSession(request.CurrentUser.SessionId);
|
||||||
|
|
||||||
|
return new(new Session(session), new UserInfo(user));
|
||||||
|
}
|
||||||
|
}
|
16
Femto.Modules.Auth/Application/ScopeBinding.cs
Normal file
16
Femto.Modules.Auth/Application/ScopeBinding.cs
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Femto.Modules.Auth.Application;
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// We use this to bind a scope to the request scope in the composition root
|
||||||
|
/// Any scoped services provided by this subcontainer should be accessed via a ScopeBinding injected in the host
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scope"></param>
|
||||||
|
public class ScopeBinding(IServiceScope scope) : IDisposable
|
||||||
|
{
|
||||||
|
public T GetService<T>() where T : notnull => scope.ServiceProvider.GetRequiredService<T>();
|
||||||
|
|
||||||
|
public void Dispose() => scope.Dispose();
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue