main project split, authpassword endpoint created

This commit is contained in:
2026-01-20 02:14:01 +01:00
Unverified
parent a01e8666a3
commit 49e6c8a643
32 changed files with 246 additions and 104 deletions

View File

@@ -0,0 +1,11 @@
using MediatR;
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthPassword;
public record AuthPasswordCommand
(
string Email,
string Password,
bool RememberMe
)
: IRequest<AuthPasswordResult>;

View File

@@ -0,0 +1,36 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
using TimetableDesigner.Backend.Services.Authentication.Database;
using TimetableDesigner.Backend.Services.Authentication.Database.Model;
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthPassword;
public class AuthPasswordHandler : IRequestHandler<AuthPasswordCommand, AuthPasswordResult>
{
private readonly DatabaseContext _databaseContext;
private readonly IPasswordHasher _passwordHasher;
public AuthPasswordHandler(DatabaseContext databaseContext, IPasswordHasher passwordHasher)
{
_databaseContext = databaseContext;
_passwordHasher = passwordHasher;
}
public async Task<AuthPasswordResult> Handle(AuthPasswordCommand request, CancellationToken cancellationToken)
{
Account? account = await _databaseContext.Accounts.FirstOrDefaultAsync(x => x.Email == request.Email, cancellationToken);
if (account is null)
{
return AuthPasswordResult.Failure();
}
PasswordHashData hash = new PasswordHashData(account.Password, account.PasswordSalt);
if (!_passwordHasher.ValidatePassword(hash, request.Password))
{
return AuthPasswordResult.Failure();
}
return null;
}
}

View File

@@ -0,0 +1,21 @@
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthPassword;
public record AuthPasswordResult
{
public bool IsSuccess { get; }
public string? AccessToken { get; }
public string? RefreshToken { get; }
private AuthPasswordResult(bool isSuccess, string? accessToken, string? refreshToken)
{
IsSuccess = isSuccess;
AccessToken = accessToken;
RefreshToken = refreshToken;
}
public static AuthPasswordResult Success(string accessToken, string refreshToken) =>
new AuthPasswordResult(true, accessToken, refreshToken);
public static AuthPasswordResult Failure() =>
new AuthPasswordResult(false, null, null);
}

View File

@@ -0,0 +1,10 @@
using MediatR;
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register;
public record RegisterCommand
(
string Email,
string Password
)
: IRequest<RegisterResult>;

View File

@@ -1,23 +1,19 @@
using MediatR; using MediatR;
using TimetableDesigner.Backend.Events; using TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
using TimetableDesigner.Backend.Services.Authentication.Application.Helpers;
using TimetableDesigner.Backend.Services.Authentication.Database; using TimetableDesigner.Backend.Services.Authentication.Database;
using TimetableDesigner.Backend.Services.Authentication.Database.Model; using TimetableDesigner.Backend.Services.Authentication.Database.Model;
using TimetableDesigner.Backend.Services.Authentication.DTO.Events;
namespace TimetableDesigner.Backend.Services.Authentication.Application.Commands.Register; namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register;
public class RegisterHandler : IRequestHandler<RegisterCommand, RegisterResult> public class RegisterHandler : IRequestHandler<RegisterCommand, RegisterResult>
{ {
private readonly DatabaseContext _databaseContext; private readonly DatabaseContext _databaseContext;
private readonly IPasswordHasher _passwordHasher; private readonly IPasswordHasher _passwordHasher;
private readonly IEventQueuePublisher _eventQueuePublisher;
public RegisterHandler(DatabaseContext databaseContext, IPasswordHasher passwordHasher, IEventQueuePublisher eventQueuePublisher) public RegisterHandler(DatabaseContext databaseContext, IPasswordHasher passwordHasher)
{ {
_databaseContext = databaseContext; _databaseContext = databaseContext;
_passwordHasher = passwordHasher; _passwordHasher = passwordHasher;
_eventQueuePublisher = eventQueuePublisher;
} }
public async Task<RegisterResult> Handle(RegisterCommand command, CancellationToken cancellationToken) public async Task<RegisterResult> Handle(RegisterCommand command, CancellationToken cancellationToken)
@@ -31,12 +27,13 @@ public class RegisterHandler : IRequestHandler<RegisterCommand, RegisterResult>
PasswordSalt = hash.Salt, PasswordSalt = hash.Salt,
}; };
await _databaseContext.Accounts.AddAsync(account, cancellationToken); await _databaseContext.Accounts.AddAsync(account, cancellationToken);
// Change to outbox pattern
//RegisterEvent eventData = account.ToEvent();
//await _eventQueuePublisher.PublishAsync(eventData);
await _databaseContext.SaveChangesAsync(cancellationToken); await _databaseContext.SaveChangesAsync(cancellationToken);
RegisterEvent eventData = account.ToEvent(); return new RegisterResult(account.Id, account.Email);
await _eventQueuePublisher.PublishAsync(eventData);
RegisterResult result = account.ToResult();
return result;
} }
} }

View File

@@ -0,0 +1,7 @@
namespace TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register;
public record RegisterResult
(
long Id,
string Email
);

View File

@@ -1,4 +1,4 @@
namespace TimetableDesigner.Backend.Services.Authentication.Application.Helpers; namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
public interface IPasswordHasher public interface IPasswordHasher
{ {

View File

@@ -0,0 +1,6 @@
namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
public record PasswordHashData(
byte[] Hash,
string Salt
);

View File

@@ -2,7 +2,7 @@
using System.Text; using System.Text;
using Konscious.Security.Cryptography; using Konscious.Security.Cryptography;
namespace TimetableDesigner.Backend.Services.Authentication.Application.Helpers; namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
public class PasswordHasher : IPasswordHasher public class PasswordHasher : IPasswordHasher
{ {

View File

@@ -0,0 +1,27 @@
using Microsoft.Extensions.Configuration;
namespace TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
public class TokenGenerator
{
/*
public TokenGenerator(IConfiguration configuration, DatabaseContext databaseContext)
{
}
public string GenerateAccessToken(Account account)
{
}
public async Task<string> GenerateRefreshTokenAsync(Account account)
{
}
public async Task<string> ExtendRefreshTokenAsync()
{
}*/
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TimetableDesigner.Backend.Services.Authentication.Database\TimetableDesigner.Backend.Services.Authentication.Database.csproj" />
<ProjectReference Include="..\TimetableDesigner.Backend.Services.Authentication.DTO.Events\TimetableDesigner.Backend.Services.Authentication.DTO.Events.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
<PackageReference Include="MediatR" Version="14.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,8 @@
namespace TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
public record AuthPasswordRequest
(
string Email,
string Password,
bool RememberMe
);

View File

@@ -0,0 +1,7 @@
namespace TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
public record AuthResponse
(
string AccessToken,
string RefreshToken
);

View File

@@ -1,6 +1,6 @@
namespace TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI; namespace TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
public class AuthenticateResponse public class AuthTokenRequest
{ {
public string AccessToken { get; set; } = null!; public string AccessToken { get; set; } = null!;
public string RefreshToken { get; set; } = null!; public string RefreshToken { get; set; } = null!;

View File

@@ -1,8 +0,0 @@
namespace TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
public class AuthenticatePasswordRequest
{
public string Email { get; set; } = null!;
public string Password { get; set; } = null!;
public bool RememberMe { get; set; }
}

View File

@@ -1,7 +0,0 @@
namespace TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
public class AuthenticateTokenRequest
{
public string AccessToken { get; set; } = null!;
public string RefreshToken { get; set; } = null!;
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,6 @@
<Solution> <Solution>
<Project Path="TimetableDesigner.Backend.Services.Authentication.Core/TimetableDesigner.Backend.Services.Authentication.Core.csproj" />
<Project Path="TimetableDesigner.Backend.Services.Authentication.Database/TimetableDesigner.Backend.Services.Authentication.Database.csproj" />
<Project Path="TimetableDesigner.Backend.Services.Authentication.DTO.Events/TimetableDesigner.Backend.Services.Authentication.DTO.Events.csproj" /> <Project Path="TimetableDesigner.Backend.Services.Authentication.DTO.Events/TimetableDesigner.Backend.Services.Authentication.DTO.Events.csproj" />
<Project Path="TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI/TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI.csproj" /> <Project Path="TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI/TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI.csproj" />
<Project Path="TimetableDesigner.Backend.Services.Authentication/TimetableDesigner.Backend.Services.Authentication.csproj" /> <Project Path="TimetableDesigner.Backend.Services.Authentication/TimetableDesigner.Backend.Services.Authentication.csproj" />

View File

@@ -1,8 +0,0 @@
using MediatR;
namespace TimetableDesigner.Backend.Services.Authentication.Application.Commands.Register;
public record RegisterCommand(
string Email,
string Password
) : IRequest<RegisterResult>;

View File

@@ -1,20 +0,0 @@
using TimetableDesigner.Backend.Services.Authentication.Database.Model;
using TimetableDesigner.Backend.Services.Authentication.DTO.Events;
using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
namespace TimetableDesigner.Backend.Services.Authentication.Application.Commands.Register;
public static class RegisterMappers
{
public static RegisterCommand ToCommand(this RegisterRequest request) =>
new RegisterCommand(request.Email, request.Password);
public static RegisterResult ToResult(this Account account) =>
new RegisterResult(account.Id, account.Email);
public static RegisterResponse ToResponse(this RegisterResult result) =>
new RegisterResponse(result.Id, result.Email);
public static RegisterEvent ToEvent(this Account account) =>
new RegisterEvent(account.Id, account.Email);
}

View File

@@ -1,6 +0,0 @@
namespace TimetableDesigner.Backend.Services.Authentication.Application.Commands.Register;
public record RegisterResult(
long Id,
string Email
);

View File

@@ -1,6 +0,0 @@
namespace TimetableDesigner.Backend.Services.Authentication.Application.Helpers;
public record PasswordHashData(
byte[] Hash,
string Salt
);

View File

@@ -6,7 +6,7 @@ using TimetableDesigner.Backend.Events.Extensions.AspNetCore.OpenApi;
using TimetableDesigner.Backend.Events.Providers.RabbitMQ; using TimetableDesigner.Backend.Events.Providers.RabbitMQ;
using TimetableDesigner.Backend.Services.Authentication.API; using TimetableDesigner.Backend.Services.Authentication.API;
using TimetableDesigner.Backend.Services.Authentication.API.Validators; using TimetableDesigner.Backend.Services.Authentication.API.Validators;
using TimetableDesigner.Backend.Services.Authentication.Application.Helpers; using TimetableDesigner.Backend.Services.Authentication.Core.Helpers;
using TimetableDesigner.Backend.Services.Authentication.Database; using TimetableDesigner.Backend.Services.Authentication.Database;
using TimetableDesigner.Backend.Services.Authentication.Events; using TimetableDesigner.Backend.Services.Authentication.Events;
using TimetableDesigner.Backend.Services.Authentication.WebAPI; using TimetableDesigner.Backend.Services.Authentication.WebAPI;

View File

@@ -23,8 +23,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\TimetableDesigner.Backend.Services.Authentication.Core\TimetableDesigner.Backend.Services.Authentication.Core.csproj" />
<ProjectReference Include="..\TimetableDesigner.Backend.Services.Authentication.DTO.Events\TimetableDesigner.Backend.Services.Authentication.DTO.Events.csproj" /> <ProjectReference Include="..\TimetableDesigner.Backend.Services.Authentication.DTO.Events\TimetableDesigner.Backend.Services.Authentication.DTO.Events.csproj" />
<ProjectReference Include="..\TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI\TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI.csproj" /> <ProjectReference Include="..\TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI\TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Workers\" />
</ItemGroup>
</Project> </Project>

View File

@@ -2,62 +2,64 @@
using FluentValidation.Results; using FluentValidation.Results;
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.HttpResults;
using TimetableDesigner.Backend.Events; using TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthPassword;
using TimetableDesigner.Backend.Services.Authentication.Application.Commands.Register; using TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register;
using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI; using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
using TimetableDesigner.Backend.Services.Authentication.WebAPI.Mappers;
namespace TimetableDesigner.Backend.Services.Authentication.WebAPI; namespace TimetableDesigner.Backend.Services.Authentication.WebAPI;
public static class Endpoints public static class Endpoints
{ {
public static IEndpointRouteBuilder MapWebAPIEndpoints(this IEndpointRouteBuilder app) public static IEndpointRouteBuilder MapWebAPIEndpoints(this IEndpointRouteBuilder builder)
{ {
app.MapPost("/register", Register) builder.MapPost("/register", Register)
.AllowAnonymous() .AllowAnonymous()
.Produces<RegisterResponse>(201) .Produces<RegisterResponse>(201)
.Produces<HttpValidationProblemDetails>(400) .Produces<HttpValidationProblemDetails>(400)
.Produces(500) .Produces(500)
.WithName("Register"); .WithName("Register");
app.MapPost("/authenticate_password", AuthenticatePassword) builder.MapPost("/auth/password", AuthPassword)
.WithName("AuthenticatePassword");
app.MapPost("/authenticate_token", AuthenticateToken)
.WithName("AuthenticateToken");
app.MapPost("/test", Test)
.AllowAnonymous() .AllowAnonymous()
.WithName("Test"); .Produces<AuthResponse>()
.Produces(401)
.Produces(500)
.WithName("AuthPassword");
builder.MapPost("/auth/token", AuthToken)
.WithName("AuthToken");
return app; return builder;
} }
private static async Task<Results<Created<RegisterResponse>, ValidationProblem>> Register(IMediator mediator, IValidator<RegisterRequest> validator, RegisterRequest request, CancellationToken cancellationToken) private static async Task<Results<Created<RegisterResponse>, ValidationProblem, InternalServerError>> Register(IMediator mediator, IValidator<RegisterRequest> validator, RegisterRequest request, CancellationToken cancellationToken)
{ {
ValidationResult validationResult = await validator.ValidateAsync(request); ValidationResult validationResult = await validator.ValidateAsync(request, cancellationToken);
if (!validationResult.IsValid) if (!validationResult.IsValid)
{ {
return TypedResults.ValidationProblem(validationResult.ToDictionary()); return TypedResults.ValidationProblem(validationResult.ToDictionary());
} }
RegisterCommand registerCommand = request.ToCommand(); RegisterResult result = await mediator.Send(request.ToCommand(), cancellationToken);
RegisterResult result = await mediator.Send(registerCommand, cancellationToken);
RegisterResponse response = result.ToResponse();
RegisterResponse response = result.ToResponse();
return TypedResults.Created($"accounts/{response.Id}", response); return TypedResults.Created($"accounts/{response.Id}", response);
} }
public static async Task<Results<Ok<AuthenticateResponse>, ProblemHttpResult>> AuthenticatePassword(AuthenticatePasswordRequest request) private static async Task<Results<Ok<AuthResponse>, UnauthorizedHttpResult, InternalServerError>> AuthPassword(IMediator mediator, AuthPasswordRequest request, CancellationToken cancellationToken)
{ {
return null; AuthPasswordResult result = await mediator.Send(request.ToCommand(), cancellationToken);
if (!result.IsSuccess)
{
return TypedResults.Unauthorized();
} }
public static async Task<Results<Ok<AuthenticateResponse>, ProblemHttpResult>> AuthenticateToken(AuthenticateTokenRequest request) AuthResponse response = result.ToResponse();
{ return TypedResults.Ok(response);
return null;
} }
public static async Task<Results<Ok, InternalServerError>> Test(IEventQueuePublisher publisher) public static async Task<Results<Ok<AuthResponse>, ProblemHttpResult>> AuthToken(AuthTokenRequest request)
{ {
await publisher.PublishAsync(new RegisterRequest("aaaa", "bbbb", "ccccc")); return null;
return TypedResults.Ok();
} }
} }

View File

@@ -0,0 +1,13 @@
using TimetableDesigner.Backend.Services.Authentication.Core.Commands.AuthPassword;
using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
namespace TimetableDesigner.Backend.Services.Authentication.WebAPI.Mappers;
public static class AuthPasswordMappers
{
public static AuthPasswordCommand ToCommand(this AuthPasswordRequest request) =>
new AuthPasswordCommand(request.Email, request.Password, request.RememberMe);
public static AuthResponse ToResponse(this AuthPasswordResult result) =>
new AuthResponse(result.AccessToken!, result.RefreshToken!);
}

View File

@@ -0,0 +1,13 @@
using TimetableDesigner.Backend.Services.Authentication.Core.Commands.Register;
using TimetableDesigner.Backend.Services.Authentication.DTO.WebAPI;
namespace TimetableDesigner.Backend.Services.Authentication.WebAPI.Mappers;
public static class RegisterMappers
{
public static RegisterCommand ToCommand(this RegisterRequest request) =>
new RegisterCommand(request.Email, request.Password);
public static RegisterResponse ToResponse(this RegisterResult result) =>
new RegisterResponse(result.Id, result.Email);
}