Compare commits
No commits in common. "main" and "features/feature_001" have entirely different histories.
main
...
features/f
72
.gitignore
vendored
72
.gitignore
vendored
@ -1,8 +1,5 @@
|
||||
# ---> VisualStudio
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
@ -10,6 +7,7 @@
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
*.env
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
@ -22,17 +20,37 @@ mono_crash.*
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
|
||||
[Dd]ebug/x64/
|
||||
[Dd]ebugPublic/x64/
|
||||
[Rr]elease/x64/
|
||||
[Rr]eleases/x64/
|
||||
bin/x64/
|
||||
obj/x64/
|
||||
|
||||
[Dd]ebug/x86/
|
||||
[Dd]ebugPublic/x86/
|
||||
[Rr]elease/x86/
|
||||
[Rr]eleases/x86/
|
||||
bin/x86/
|
||||
obj/x86/
|
||||
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
[Aa][Rr][Mm]64[Ee][Cc]/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Oo]ut/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Build results on 'Bin' directories
|
||||
**/[Bb]in/*
|
||||
# Uncomment if you have tasks that rely on *.refresh files to move binaries
|
||||
# (https://github.com/github/gitignore/pull/3736)
|
||||
#!**/[Bb]in/*.refresh
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
@ -44,12 +62,16 @@ Generated\ Files/
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
*.trx
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Approval Tests result files
|
||||
*.received.*
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
@ -76,6 +98,7 @@ StyleCopReport.xml
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.idb
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
@ -156,6 +179,7 @@ coverage*.info
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
@ -297,9 +321,6 @@ node_modules/
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
@ -317,22 +338,22 @@ node_modules/
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
**/.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
**/.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
**/.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
**/__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
#tools/**
|
||||
#!tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
@ -354,15 +375,19 @@ ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
MSBuild_Logs/
|
||||
|
||||
# AWS SAM Build and Temporary Artifacts folder
|
||||
.aws-sam
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
**/.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
**/.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
@ -374,7 +399,7 @@ healthchecksdb
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
**/.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
@ -385,18 +410,17 @@ FodyWeavers.xsd
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
|
||||
*.msp
|
||||
38
README.md
38
README.md
@ -1,2 +1,38 @@
|
||||
# chat_application
|
||||
# 💬 Chat Application
|
||||
|
||||
A modern chat application enabling real-time messaging, group conversations (salons), and role-based access control.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- User registration and authentication (Logto)
|
||||
- Real-time messaging between users
|
||||
- Group conversations (salons) and private chats
|
||||
- Role and permission management (admin panel)
|
||||
- User invitations to salons
|
||||
- Responsive Angular frontend
|
||||
- Secure .NET API backend
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Tech Stacks
|
||||
|
||||
- **Frontend:** Angular
|
||||
- **Backend:** .NET API
|
||||
- **Authentication:** Logto (Identity Provider)
|
||||
- **Hosting:** Hetzner Cloud
|
||||
|
||||
---
|
||||
|
||||
## 📦 Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js & npm (for Angular)
|
||||
- .NET SDK (for backend)
|
||||
- Logto account (for authentication)
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the QAP License.
|
||||
132
doc/feature_001.md
Normal file
132
doc/feature_001.md
Normal file
@ -0,0 +1,132 @@
|
||||
# Features
|
||||
|
||||
## Personas
|
||||
|
||||
- **Alice**: New user who wants to join the platform to chat with friends.
|
||||
- **Bob**: Regular user, member of several salons, likes to organize group conversations.
|
||||
- **Claire**: Administrator, responsible for managing roles and permissions.
|
||||
- **David**: Invited user, discovers the platform via a salon invitation.
|
||||
|
||||
## User Registration
|
||||
**Persona:** Alice
|
||||
**Scenario:** User registers into the application
|
||||
- Given Alice is on the registration page
|
||||
- When she provides valid details (username, email, password)
|
||||
- And she clicks "Register"
|
||||
- Then the username, email and password are validated by the system
|
||||
- And her account is created
|
||||
- And she receives a confirmation message
|
||||
- And she is redirected to the login page
|
||||
- And she receives a welcome email
|
||||
- When she tries to register with an existing email
|
||||
- Then the system displays an error message
|
||||
|
||||
## User Login
|
||||
**Persona:** Alice
|
||||
**Scenario:** User logs into the application
|
||||
- Given Alice is on the login page
|
||||
- When she enters valid credentials (email/username and password)
|
||||
- And she clicks "Login"
|
||||
- Then the credentials are validated by the system
|
||||
- And she is granted access to the application
|
||||
- When she enters an invalid password
|
||||
- Then the system displays an error message
|
||||
- When she forgets her password
|
||||
- Then she can request a password reset link
|
||||
|
||||
## Role Management
|
||||
**Persona:** Claire
|
||||
**Scenario:** Admin creates and assigns roles
|
||||
- Given Claire is logged in as an admin
|
||||
- When she navigates to the roles management page
|
||||
- And she creates a new role or edits an existing one
|
||||
- Then the role is saved
|
||||
- And she can assign or unassign it to the users
|
||||
- When she tries to assign a role to herself
|
||||
- Then the system prevents self-assignment for certain roles (e.g., SuperAdmin)
|
||||
- When she deletes a role
|
||||
- Then the system asks for confirmation and updates all affected users
|
||||
|
||||
## Permission Management
|
||||
**Persona:** Claire
|
||||
**Scenario:** Admin manages permissions for roles
|
||||
- Given Claire is logged in as an admin
|
||||
- When she navigates to the permissions management page
|
||||
- And she assigns or removes permissions for a role
|
||||
- Then the permissions are updated
|
||||
- And users with that role have the correct accesses
|
||||
- When she tries to remove all permissions from a role
|
||||
- Then the system warns about potential loss of access
|
||||
|
||||
## Send a Message
|
||||
**Persona:** Bob
|
||||
**Scenario:** User sends a message in a conversation
|
||||
- Given Bob is logged in
|
||||
- And he is in an existing conversation with other users
|
||||
- When he types a message
|
||||
- And he clicks "Send"
|
||||
- Then the message appears in the conversation for all participants
|
||||
- When he sends an empty message
|
||||
- Then the system prevents sending and shows a warning
|
||||
- When he sends a message with an attachment
|
||||
- Then the attachment is uploaded and visible to all participants
|
||||
|
||||
## Read Messages
|
||||
**Persona:** Alice
|
||||
**Scenario:** User reads messages in a conversation
|
||||
- Given Alice is logged in
|
||||
- And she has joined a conversation
|
||||
- When another user sends a message
|
||||
- Then the message appears in the conversation in real time
|
||||
- When she scrolls up, older messages are loaded
|
||||
- When she marks a message as read
|
||||
- Then the sender is notified (if enabled)
|
||||
|
||||
## Create a Conversation
|
||||
**Persona:** Bob
|
||||
**Scenario:** User creates a conversation with one or more users
|
||||
- Given Bob is logged in
|
||||
- When he clicks "New Conversation"
|
||||
- And he selects one or more users
|
||||
- And he clicks "Create"
|
||||
- Then a new conversation is created
|
||||
- And the selected users are added to the conversation
|
||||
- When he tries to create a conversation with himself only
|
||||
- Then the system prevents it and shows a message
|
||||
|
||||
## Salon Creation
|
||||
**Persona:** Bob
|
||||
**Scenario:** User creates a new salon
|
||||
- Given Bob is logged in
|
||||
- When he navigates to the "Create Salon" page
|
||||
- And he provides a salon name
|
||||
- And he clicks "Create"
|
||||
- Then a new salon is created
|
||||
- And Bob is added as a member of the salon
|
||||
- When he tries to create a salon with a name that already exists
|
||||
- Then the system displays an error message
|
||||
|
||||
## Invite Users to a Salon
|
||||
**Persona:** Bob, David
|
||||
**Scenario:** User invites others to join a salon
|
||||
- Given Bob is logged in
|
||||
- And he is a member of an existing salon
|
||||
- When he selects other users to invite (including David)
|
||||
- And he clicks "Invite"
|
||||
- Then the selected users receive an invitation
|
||||
- And they can join the salon once they accept
|
||||
- When David accepts the invitation
|
||||
- Then he becomes a member of the salon
|
||||
- When David declines the invitation
|
||||
- Then Bob is notified of the refusal
|
||||
|
||||
|
||||
|
||||
# Technical specifications
|
||||
|
||||
- **.NET**: Main framework for backend, API, and business logic.
|
||||
- **Angular**: Framework used for developing the interactive web frontend.
|
||||
- **Docker**: Containerization of services to facilitate deployment and portability.
|
||||
- **Logto**: Authentication and identity management solution to secure access.
|
||||
- **SQLite**: Lightweight database, ideal for local storage or testing environments.
|
||||
- **Hetzner**: Hosting provider for production deployment (servers, cloud).
|
||||
7
src/appsettings.Development.json
Normal file
7
src/appsettings.Development.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"Logto": {
|
||||
"Endpoint": "https://en7hny.logto.app/",
|
||||
"AppId": "n863z6tazsbjtn8b113x7",
|
||||
"AppSecret": "1J4FpfWaEzPsGKALpBnhT5dqoYY19Bta"
|
||||
}
|
||||
}
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/chat.API/Program.cs
Normal file
29
src/chat.API/Program.cs
Normal file
@ -0,0 +1,29 @@
|
||||
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.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapControllers();
|
||||
|
||||
app.Run();
|
||||
23
src/chat.API/Properties/launchSettings.json
Normal file
23
src/chat.API/Properties/launchSettings.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "http://localhost:5297",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"applicationUrl": "https://localhost:7169;http://localhost:5297",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/chat.API/appsettings.Development.json
Normal file
9
src/chat.API/appsettings.Development.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
19
src/chat.API/appsettings.json
Normal file
19
src/chat.API/appsettings.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/chat.API/chat.API.csproj
Normal file
20
src/chat.API/chat.API.csproj
Normal file
@ -0,0 +1,20 @@
|
||||
<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="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>
|
||||
|
||||
</Project>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
22
src/chat.Application/chat.Application.csproj
Normal file
22
src/chat.Application/chat.Application.csproj
Normal file
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<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);
|
||||
}
|
||||
}
|
||||
13
src/chat.Domain/chat.Domain.csproj
Normal file
13
src/chat.Domain/chat.Domain.csproj
Normal 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="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
|
||||
);
|
||||
}
|
||||
17
src/chat.Infrastructure/chat.Infrastructure.csproj
Normal file
17
src/chat.Infrastructure/chat.Infrastructure.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\chat.Domain\chat.Domain.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
40
src/chat_application.sln
Normal file
40
src/chat_application.sln
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.3.11312.210 d18.3
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.Infrastructure", "chat.Infrastructure\chat.Infrastructure.csproj", "{C4382C35-1776-4F39-BF67-2BE197AB4283}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.Domain", "chat.Domain\chat.Domain.csproj", "{3ED98938-5F3B-49D8-824B-95AFAF328A1F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.API", "chat.API\chat.API.csproj", "{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "chat.Application", "chat.Application\chat.Application.csproj", "{619A7FFA-4796-4731-93A3-4AFB002ABD3F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C4382C35-1776-4F39-BF67-2BE197AB4283}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{3ED98938-5F3B-49D8-824B-95AFAF328A1F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC1C6BF4-65E0-412E-ADDD-A2CCF6A9E032}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{619A7FFA-4796-4731-93A3-4AFB002ABD3F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
0
tests/test.txt
Normal file
0
tests/test.txt
Normal file
Loading…
x
Reference in New Issue
Block a user