Register -> Next step manage exceptions and tests
This commit is contained in:
parent
b3223df0f9
commit
4c98577eed
22
src/chat.API/AuthController/AuthController.cs
Normal file
22
src/chat.API/AuthController/AuthController.cs
Normal file
@ -0,0 +1,22 @@
|
||||
using chat.Application.Authentication.Commands.Register;
|
||||
using chat.Application.Authentication.Dtos;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http.HttpResults;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace chat.API.AuthController;
|
||||
|
||||
[ApiController]
|
||||
[Route("chat_api/auth")]
|
||||
[Authorize]
|
||||
public class AuthController(IMediator mediator) : ControllerBase
|
||||
{
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
public async Task<ActionResult<RegisterResponseDto>> Register([FromBody] RegisterCommand command)
|
||||
{
|
||||
RegisterResponseDto dto = await mediator.Send(command);
|
||||
return Ok(dto);
|
||||
}
|
||||
}
|
||||
43
src/chat.API/Extensions/WebApplicationBuilderExtensions.cs
Normal file
43
src/chat.API/Extensions/WebApplicationBuilderExtensions.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using chat.API.Middlewares;
|
||||
using Microsoft.OpenApi.Models;
|
||||
|
||||
namespace chat.API.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static void AddPresentation(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.AddAuthentication();
|
||||
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddEndpointsApiExplorer();
|
||||
|
||||
builder.Services.AddSwaggerGen(c =>
|
||||
{
|
||||
// Définit le schéma de sécurité pour l'authentification Bearer (JWT)
|
||||
c.AddSecurityDefinition("bearerAuth", new OpenApiSecurityScheme()
|
||||
{
|
||||
Type = SecuritySchemeType.Http,
|
||||
Scheme = "bearer",
|
||||
});
|
||||
|
||||
// Ajoute l'exigence de sécurité pour que Swagger utilise le schéma Bearer
|
||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = "bearerAuth"
|
||||
}
|
||||
},
|
||||
[]
|
||||
}
|
||||
});
|
||||
});
|
||||
builder.Services.AddScoped<ErrorHandlingMiddleware>();
|
||||
}
|
||||
}
|
||||
28
src/chat.API/Middlewares/ErrorHandlingMiddleware.cs
Normal file
28
src/chat.API/Middlewares/ErrorHandlingMiddleware.cs
Normal file
@ -0,0 +1,28 @@
|
||||
using chat.Domain.Exceptions;
|
||||
|
||||
namespace chat.API.Middlewares;
|
||||
|
||||
public class ErrorHandlingMiddleware(ILogger<ErrorHandlingMiddleware> logger) : IMiddleware
|
||||
{
|
||||
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
|
||||
{
|
||||
try
|
||||
{
|
||||
await next(context);
|
||||
}
|
||||
catch (HttpResponseException ex)
|
||||
{
|
||||
logger.LogWarning(ex, "HTTP {StatusCode}: {Message}", ex.StatusCode, ex.Message);
|
||||
context.Response.StatusCode = ex.StatusCode;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(new { error = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Unexpected error occurred");
|
||||
context.Response.StatusCode = 500;
|
||||
context.Response.ContentType = "application/json";
|
||||
await context.Response.WriteAsJsonAsync(new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,27 @@
|
||||
using chat.API.Extensions;
|
||||
using chat.API.Middlewares;
|
||||
using chat.Application.Extensions;
|
||||
using chat.Infrastructure.Extensions;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.AddPresentation();
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddInfrastructure();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseMiddleware<ErrorHandlingMiddleware>();
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.MapOpenApi();
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
@ -5,5 +5,15 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
"AllowedHosts": "*",
|
||||
"Logto": {
|
||||
"Endpoint": "https://en7hny.logto.app/", // URL de base de ton tenant Logto
|
||||
"Issuer": "https://en7hny.logto.app/oidc", // URL pour valider les JWT (utilisé par le middleware auth)
|
||||
"Audience": "https://localhost:7169", // Identifiant de ton API (pour valider les tokens)
|
||||
"ManagementApi": "https://en7hny.logto.app/api", // URL de l'API Management Logto (pour créer des users)
|
||||
"M2M": {
|
||||
"AppId": "", // Client ID de l'app M2M (pour obtenir un token Management) dans user-secrets
|
||||
"AppSecret": "" // Client Secret de l'app M2M dans user-secrets
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>7224f703-f9b3-4ef2-a34d-b2ba0038d160</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\chat.Application\chat.Application.csproj" />
|
||||
<ProjectReference Include="..\chat.Domain\chat.Domain.csproj" />
|
||||
<ProjectReference Include="..\chat.Infrastructure\chat.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
using chat.Application.Authentication.Dtos;
|
||||
using MediatR;
|
||||
|
||||
namespace chat.Application.Authentication.Commands.Register;
|
||||
|
||||
public class RegisterCommand : IRequest<RegisterResponseDto>
|
||||
{
|
||||
public string Username { get; set; } = default!;
|
||||
public string Email { get; set; } = default!;
|
||||
public string Password { get; set; } = default!;
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
using AutoMapper;
|
||||
using chat.Application.Authentication.Dtos;
|
||||
using chat.Domain.Entities;
|
||||
using chat.Domain.Interfaces;
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace chat.Application.Authentication.Commands.Register;
|
||||
|
||||
public class RegisterCommandHandler(
|
||||
ILogger<RegisterCommandHandler> logger,
|
||||
IMapper mapper,
|
||||
ILogtoAuthService authRepository
|
||||
) : IRequestHandler<RegisterCommand, RegisterResponseDto>
|
||||
{
|
||||
public async Task<RegisterResponseDto> Handle(RegisterCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogInformation("Creating a new user with the following credentials {Username} {Email}", request.Username, request.Email);
|
||||
var user = mapper.Map<User>(request);
|
||||
var userRegistered = await authRepository.Register(user , cancellationToken);
|
||||
if (userRegistered == null)
|
||||
{
|
||||
throw new Exception("Registration failed");
|
||||
}
|
||||
var response = mapper.Map<RegisterResponseDto>(userRegistered);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
using FluentValidation;
|
||||
|
||||
namespace chat.Application.Authentication.Commands.Register;
|
||||
|
||||
public class RegisterCommandValidator : AbstractValidator<RegisterCommand>
|
||||
{
|
||||
public RegisterCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Username)
|
||||
.NotEmpty().WithMessage("Username is required.")
|
||||
.MinimumLength(3).WithMessage("Username must be at least 3 characters long.");
|
||||
RuleFor(x => x.Password)
|
||||
.NotEmpty().WithMessage("Password is required.")
|
||||
.MinimumLength(6).WithMessage("Password must be at least 6 characters long.");
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email is required.")
|
||||
.EmailAddress().WithMessage("A valid email address is required.");
|
||||
}
|
||||
}
|
||||
24
src/chat.Application/Authentication/Dtos/AuthProfile.cs
Normal file
24
src/chat.Application/Authentication/Dtos/AuthProfile.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using AutoMapper;
|
||||
using chat.Application.Authentication.Commands.Register;
|
||||
using chat.Domain.Entities;
|
||||
|
||||
namespace chat.Application.Authentication.Dtos
|
||||
{
|
||||
public class AuthProfile : Profile
|
||||
{
|
||||
public AuthProfile()
|
||||
{
|
||||
CreateMap<RegisterCommand, User>()
|
||||
.ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.Username))
|
||||
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
|
||||
.ForMember(dest => dest.Password, opt => opt.MapFrom(src => src.Password))
|
||||
.ReverseMap();
|
||||
|
||||
CreateMap<RegisterResponseDto, User>()
|
||||
.ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.Username))
|
||||
.ForMember(dest => dest.Email, opt => opt.MapFrom(src => src.Email))
|
||||
.ForMember(dest => dest.Password, opt => opt.MapFrom(src => src.Password))
|
||||
.ReverseMap();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace chat.Application.Authentication.Dtos;
|
||||
|
||||
public class RegisterResponseDto
|
||||
{
|
||||
public string Username { get; set; } = default!;
|
||||
public string Email { get; set; } = default!;
|
||||
public string Password { get; set; } = default!;
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using FluentValidation;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions;
|
||||
|
||||
namespace chat.Application.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddApplication(this IServiceCollection services)
|
||||
{
|
||||
var applicationAssembly = typeof(ServiceCollectionExtensions).Assembly;
|
||||
|
||||
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(applicationAssembly));
|
||||
|
||||
services.AddAutoMapper(cfg => cfg.AddMaps(applicationAssembly));
|
||||
|
||||
services.AddValidatorsFromAssembly(applicationAssembly)
|
||||
.AddFluentValidationAutoValidation();
|
||||
}
|
||||
}
|
||||
@ -6,4 +6,17 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper" Version="16.1.0" />
|
||||
<PackageReference Include="FluentValidation" Version="12.1.1" />
|
||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="12.1.1" />
|
||||
<PackageReference Include="MediatR" Version="14.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
||||
<PackageReference Include="SharpGrip.FluentValidation.AutoValidation.Mvc" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\chat.Domain\chat.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
8
src/chat.Domain/Entities/User.cs
Normal file
8
src/chat.Domain/Entities/User.cs
Normal file
@ -0,0 +1,8 @@
|
||||
namespace chat.Domain.Entities;
|
||||
|
||||
public class User
|
||||
{
|
||||
public string Username { get; set; } = default!;
|
||||
public string Email { get; set; } = default!;
|
||||
public string Password { get; set; } = default!;
|
||||
}
|
||||
11
src/chat.Domain/Exceptions/HttpResponseException.cs
Normal file
11
src/chat.Domain/Exceptions/HttpResponseException.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace chat.Domain.Exceptions;
|
||||
|
||||
public class HttpResponseException : Exception
|
||||
{
|
||||
public int StatusCode { get; }
|
||||
|
||||
public HttpResponseException(int statusCode, string message) : base(message)
|
||||
{
|
||||
StatusCode = statusCode;
|
||||
}
|
||||
}
|
||||
9
src/chat.Domain/Interfaces/ILogtoAuthService.cs
Normal file
9
src/chat.Domain/Interfaces/ILogtoAuthService.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using chat.Domain.Entities;
|
||||
|
||||
namespace chat.Domain.Interfaces
|
||||
{
|
||||
public interface ILogtoAuthService
|
||||
{
|
||||
Task<User?> Register(User request, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@ -6,4 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
using chat.Domain.Interfaces;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace chat.Infrastructure.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddInfrastructure(this IServiceCollection services)
|
||||
{
|
||||
services.AddHttpClient<LogtoAuthService>();
|
||||
services.AddScoped<ILogtoAuthService, LogtoAuthService>();
|
||||
}
|
||||
}
|
||||
115
src/chat.Infrastructure/LogtoAuthService.cs
Normal file
115
src/chat.Infrastructure/LogtoAuthService.cs
Normal file
@ -0,0 +1,115 @@
|
||||
using chat.Domain.Entities;
|
||||
using chat.Domain.Interfaces;
|
||||
using chat.Domain.Exceptions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace chat.Infrastructure;
|
||||
|
||||
public class LogtoAuthService(
|
||||
ILogger<LogtoAuthService> _logger,
|
||||
HttpClient _httpClient,
|
||||
IConfiguration _configuration) : ILogtoAuthService
|
||||
{
|
||||
public async Task<User?> Register(User request, CancellationToken cancellationToken)
|
||||
{
|
||||
var token = await RetrieveManagementToken(cancellationToken);
|
||||
var managementApiUrl = _configuration["Logto:ManagementApi"]!;
|
||||
var createUserEndpoint = $"{managementApiUrl}/users";
|
||||
|
||||
var createUserRequest = new
|
||||
{
|
||||
username = request.Username,
|
||||
primaryEmail = request.Email,
|
||||
password = request.Password,
|
||||
name = request.Username
|
||||
};
|
||||
|
||||
var requestMessage = new HttpRequestMessage(HttpMethod.Post, createUserEndpoint)
|
||||
{
|
||||
Content = JsonContent.Create(createUserRequest),
|
||||
Headers =
|
||||
{
|
||||
{ "Authorization", $"Bearer {token}" }
|
||||
}
|
||||
};
|
||||
|
||||
var response = await _httpClient.SendAsync(requestMessage, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new HttpResponseException((int)response.StatusCode, error);
|
||||
}
|
||||
|
||||
var logtoUser = await response.Content.ReadFromJsonAsync<LogtoUserResponse>(cancellationToken);
|
||||
|
||||
if (logtoUser == null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to deserialize Logto user response");
|
||||
}
|
||||
|
||||
_logger.LogInformation("User created successfully in Logto. UserId: {UserId}, Username: {Username}",
|
||||
logtoUser.Id, logtoUser.Username);
|
||||
|
||||
return new User
|
||||
{
|
||||
Username = logtoUser.Username ?? request.Username,
|
||||
Email = logtoUser.PrimaryEmail ?? request.Email,
|
||||
Password = "********"
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<string> RetrieveManagementToken(CancellationToken cancellationToken)
|
||||
{
|
||||
var tokenEndpoint = $"{_configuration["Logto:Issuer"]}/token";
|
||||
var managementApiUrl = _configuration["Logto:ManagementApi"]!;
|
||||
|
||||
var tokenRequest = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["grant_type"] = "client_credentials",
|
||||
["client_id"] = _configuration["Logto:M2M:AppId"]!,
|
||||
["client_secret"] = _configuration["Logto:M2M:AppSecret"]!,
|
||||
["resource"] = managementApiUrl,
|
||||
["scope"] = "all"
|
||||
});
|
||||
|
||||
var response = await _httpClient.PostAsync(tokenEndpoint, tokenRequest, cancellationToken);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
throw new HttpResponseException((int)response.StatusCode, $"{error}");
|
||||
}
|
||||
|
||||
var tokenResponse = await response.Content.ReadFromJsonAsync<TokenResponse>(cancellationToken);
|
||||
|
||||
if (tokenResponse?.AccessToken == null)
|
||||
{
|
||||
throw new InvalidOperationException("M2M token response is invalid");
|
||||
}
|
||||
|
||||
_logger.LogInformation("M2M token obtained successfully. Type: {TokenType}, Expires in: {ExpiresIn}s, scope: {Scope}",
|
||||
tokenResponse.TokenType,
|
||||
tokenResponse.ExpiresIn,
|
||||
tokenResponse.Scope);
|
||||
|
||||
return tokenResponse.AccessToken;
|
||||
}
|
||||
|
||||
private record TokenResponse(
|
||||
[property: JsonPropertyName("access_token")] string AccessToken,
|
||||
[property: JsonPropertyName("token_type")] string TokenType,
|
||||
[property: JsonPropertyName("expires_in")] int ExpiresIn,
|
||||
[property: JsonPropertyName("scope")] string Scope
|
||||
);
|
||||
|
||||
private record LogtoUserResponse(
|
||||
[property: JsonPropertyName("id")] string Id,
|
||||
[property: JsonPropertyName("username")] string? Username,
|
||||
[property: JsonPropertyName("primaryEmail")] string? PrimaryEmail,
|
||||
[property: JsonPropertyName("name")] string? Name
|
||||
);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user