Compare commits

..

14 Commits

Author SHA1 Message Date
blyssco
247e256d09 Working on auth 2025-09-06 19:51:07 +02:00
blyssco
b60c57e19c Reset pour intégrer les bonnes practices 2025-09-02 18:29:22 +02:00
blyssco
8be1a8db92 Delete unnecessary folder 2025-08-17 23:07:33 +02:00
blyssco
a27a1351cf More code 2025-08-16 21:01:14 +02:00
blyssco
2d8d8ed754 Update application part 2025-08-09 18:09:17 +02:00
blyssco
39b8312807 Maj 2025-08-02 18:56:44 +02:00
blyssco
5cd762fc3d Ajout de code, j'en suis à la partie application 2025-07-29 21:00:45 +02:00
659d263ea7 Merge pull request '1 - Setup API Clean Architecture' (#13) from feature/1 into develop
Reviewed-on: #13
2025-07-08 21:15:28 +00:00
blyssco
d1fa49bc6b Nettoyage de la classe vide Class1.cs 2025-07-07 20:02:17 +02:00
blyssco
8f3db0e133 Correction du launch browser à true 2025-07-07 19:59:08 +02:00
blyssco
5f6aa216aa Fix 2025-07-05 15:56:43 +02:00
blyssco
31299e413d Edit config 2025-07-05 14:16:02 +02:00
blyssco
380bb038cb Setup API Clean Architecture 2025-07-05 13:20:54 +02:00
blyssco
b29ebd7805 Cleaning initial project 2025-07-05 12:41:45 +02:00
32 changed files with 321 additions and 96 deletions

View File

@ -0,0 +1,6 @@
@LiberIncantamentum.API_HostAddress = http://localhost:5196
GET {{LiberIncantamentum.API_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -7,7 +7,11 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.5" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
</ItemGroup>
<ItemGroup>
<Folder Include="Controllers\" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -5,7 +5,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "http://localhost:5225", "applicationUrl": "http://localhost:5196",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }
@ -14,7 +14,7 @@
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": false,
"applicationUrl": "https://localhost:7239;http://localhost:5225", "applicationUrl": "https://localhost:7236;http://localhost:5196",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
} }

View File

@ -0,0 +1,9 @@
namespace Liber_Incantamentum.Application.Authentification.DTOs.Request
{
public class LoginRequestDto
{
public string? Username { get; set; }
public string? Email { get; set; }
public required string Password { get; set; }
}
}

View File

@ -0,0 +1,8 @@
namespace Liber_Incantamentum.Application.Authentification.DTOs.Request
{
public class RefreshTokenRequestDto
{
public Guid UserId { get; set; }
public required string RefreshToken { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Liber_Incantamentum.Application.Authentification.DTOs.Request
{
public class RegisterRequestDto
{
public required string Username { get; set; }
public required string Email { get; set; }
public required string Password { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Liber_Incantamentum.Application.Authentification.DTOs.Request
{
public class UserFilterDto
{
public Guid? Id { get; set; }
public string? UserName { get; set; }
public string? Email { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Liber_Incantamentum.Application.Authentification.DTOs.Response
{
public class AuthResponseDto
{
public UserResponseDto UserResponse { get; set; } = null!;
public string AccessToken { get; set; } = string.Empty;
public string Refreshtoken { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,10 @@
namespace Liber_Incantamentum.Application.Authentification.DTOs.Response
{
public class UserResponseDto
{
public Guid Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,12 @@
using Liber_Incantamentum.Application.Authentification.DTOs.Request;
using Liber_Incantamentum.Application.Authentification.DTOs.Response;
namespace Liber_Incantamentum.Application.Authentification.Interfaces
{
public interface IAuthentificationService
{
Task<UserResponseDto> RegisterUserAsync(RegisterRequestDto user);
Task<IEnumerable<UserResponseDto>> GetAllUsersAsync(UserFilterDto filter);
Task<UserResponseDto> LoginUserAsync(LoginRequestDto user);
}
}

View File

@ -0,0 +1,12 @@
using Liber_Incantamentum.Application.Authentification.DTOs.Request;
using Liber_Incantamentum.Application.Authentification.DTOs.Response;
using Liber_Incantamentum.Domain.Authentification.Entities;
namespace Liber_Incantamentum.Application.Authentification.Interfaces
{
public interface ITokenService
{
Task<AuthResponseDto> GenerateTokensAsync(User user); // génère et stocke refresh token
Task<AuthResponseDto?> RefreshTokensAsync(RefreshTokenRequestDto request); // vérifie et renouvelle
}
}

View File

@ -0,0 +1,23 @@
using Liber_Incantamentum.Application.Authentification.DTOs.Request;
using Liber_Incantamentum.Application.Authentification.DTOs.Response;
using Liber_Incantamentum.Application.Authentification.Interfaces;
namespace Liber_Incantamentum.Application.Authentification.Services.Generals
{
public class AuthentificationService : IAuthentificationService
{
public Task<UserResponseDto> RegisterUserAsync(RegisterRequestDto user)
{
throw new NotImplementedException();
}
public Task<UserResponseDto> LoginUserAsync(LoginRequestDto user)
{
throw new NotImplementedException();
}
public Task<IEnumerable<UserResponseDto>> GetAllUsersAsync(UserFilterDto filter)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,21 @@
using Liber_Incantamentum.Application.Authentification.DTOs.Request;
using Liber_Incantamentum.Application.Authentification.DTOs.Response;
using Liber_Incantamentum.Application.Authentification.Interfaces;
using Liber_Incantamentum.Domain.Authentification.Entities;
using System.Security.Cryptography;
namespace Liber_Incantamentum.Application.Authentification.Services.Generals
{
public class TokenService : ITokenService
{
public Task<AuthResponseDto> GenerateTokensAsync(User user)
{
throw new NotImplementedException();
}
public Task<AuthResponseDto?> RefreshTokensAsync(RefreshTokenRequestDto request)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Authentification\Exceptions\" />
<Folder Include="Authentification\Services\Mappings\" />
<Folder Include="Authentification\Services\Validations\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LiberIncantamentum.Domain\Liber_Incantamentum.Domain.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
namespace Liber_Incantamentum.Domain.Authentification.Entities
{
public class RefreshToken
{
public Guid Id { get; set; }
public required string Token { get; set; }
public DateTime ExpiresAt { get; set; }
public bool IsRevoked { get; set; }
public Guid UserId { get; set; }
public User User { get; set; } = null!;
}
}

View File

@ -0,0 +1,13 @@
namespace Liber_Incantamentum.Domain.Authentification.Entities
{
public class User
{
public Guid Id { get; set; }
public string Username { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string PasswordHash { get; set; } = string.Empty;
public string Role { get; set; } = string.Empty;
public string? RefreshToken { get; set; }
public DateTime? RefreshTokenExpiryTime { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Liber_Incantamentum.Domain.Authentification.Filters
{
public class UserFilter
{
public Guid? Id { get; set; }
public string? Username { get; set; }
public string? Email { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using Liber_Incantamentum.Domain.Authentification.Entities;
using Liber_Incantamentum.Domain.Authentification.Filters;
namespace Liber_Incantamentum.Domain.Authentification.Repositories
{
public interface IAuthentificationRepository
{
Task<User?> ListUsersAsync(UserFilter datasOnUser); // Usage : Récuperer un/des users à partir d'infos
Task<bool> AddAsync(User user); // Usage : Créer un user
Task<bool> UpdateAsync(User user); // Usage : Mettre à jour un user
Task<bool> RemoveUserAsync(Guid id); // Usage : Supprimer un user
}
}

View File

@ -0,0 +1,13 @@
using Liber_Incantamentum.Domain.Authentification.Entities;
namespace Liber_Incantamentum.Domain.Authentification.Repositories
{
public interface IRefreshTokenRepository
{
Task<RefreshToken?> GetRefreshTokenByIdAsync(string token); //Usage : quand lutilisateur envoie un refresh token pour obtenir un nouveau JWT, on doit vérifier quil est valide et non révoqué.
Task<IEnumerable<RefreshToken>> GetRefreshTokenByUserIdAsync(Guid userId); // Usage : afficher les sessions actives dun utilisateur & révoquer certains tokens manuellement
Task AddRefreshTokenAsyn(RefreshToken refreshToken); // Usage : après un login réussi ou une génération de refresh token.
Task UpdateRefreshTokenAsync(RefreshToken refreshToken); // Usage : marquer un token comme révoqué (IsRevoked = true) & prolonger la date dexpiration si tu veux étendre la validité
Task RevokeAllRefreshTokenAsync(Guid userId); // Usage : lutilisateur se déconnecte de tous les appareils & le compte a été compromis
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -1,9 +1,25 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.14.36109.1 d17.14 VisualStudioVersion = 17.14.36109.1
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum", "Liber_Incantamentum\Liber_Incantamentum.csproj", "{BB761821-A9EC-4EBA-83A2-89F32C50041F}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{93BEE101-D8F6-4622-95B6-E135AA9C066E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum.API", "LiberIncantamentum.API\Liber_Incantamentum.API.csproj", "{8B578810-E61F-4C5D-89B0-DE62B85346F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum.Domain", "LiberIncantamentum.Domain\Liber_Incantamentum.Domain.csproj", "{0B68D83C-1751-427D-840D-1285A3DB98D0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum.Infrastructure", "LiberIncantamentum.Infrastructure\Liber_Incantamentum.Infrastructure.csproj", "{8D09ADAE-591C-4487-924F-2339D29EE60D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum.Application", "LiberIncantamentum.Application\Liber_Incantamentum.Application.csproj", "{20EAEB50-C4FD-4A2D-92CC-8D0241556617}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum.UnitTests", "LiberIncantamentum.UnitTests\Liber_Incantamentum.UnitTests.csproj", "{B829A2A1-9366-422B-B372-5DDA33BD2690}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum.FunctionalTests", "Liber_Incantamentum.FunctionnalTests\Liber_Incantamentum.FunctionalTests.csproj", "{8F1D203C-1A76-4E76-AFC9-7B7477EB143E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Liber_Incantamentum.IntegrationTests", "Liber_Incantamentum.IntegrationTests\Liber_Incantamentum.IntegrationTests.csproj", "{F36ABF3F-5707-463C-BC33-DF8A261E4B5A}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -11,14 +27,47 @@ Global
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BB761821-A9EC-4EBA-83A2-89F32C50041F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8B578810-E61F-4C5D-89B0-DE62B85346F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BB761821-A9EC-4EBA-83A2-89F32C50041F}.Debug|Any CPU.Build.0 = Debug|Any CPU {8B578810-E61F-4C5D-89B0-DE62B85346F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BB761821-A9EC-4EBA-83A2-89F32C50041F}.Release|Any CPU.ActiveCfg = Release|Any CPU {8B578810-E61F-4C5D-89B0-DE62B85346F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BB761821-A9EC-4EBA-83A2-89F32C50041F}.Release|Any CPU.Build.0 = Release|Any CPU {8B578810-E61F-4C5D-89B0-DE62B85346F7}.Release|Any CPU.Build.0 = Release|Any CPU
{0B68D83C-1751-427D-840D-1285A3DB98D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0B68D83C-1751-427D-840D-1285A3DB98D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0B68D83C-1751-427D-840D-1285A3DB98D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0B68D83C-1751-427D-840D-1285A3DB98D0}.Release|Any CPU.Build.0 = Release|Any CPU
{8D09ADAE-591C-4487-924F-2339D29EE60D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8D09ADAE-591C-4487-924F-2339D29EE60D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8D09ADAE-591C-4487-924F-2339D29EE60D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8D09ADAE-591C-4487-924F-2339D29EE60D}.Release|Any CPU.Build.0 = Release|Any CPU
{20EAEB50-C4FD-4A2D-92CC-8D0241556617}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{20EAEB50-C4FD-4A2D-92CC-8D0241556617}.Debug|Any CPU.Build.0 = Debug|Any CPU
{20EAEB50-C4FD-4A2D-92CC-8D0241556617}.Release|Any CPU.ActiveCfg = Release|Any CPU
{20EAEB50-C4FD-4A2D-92CC-8D0241556617}.Release|Any CPU.Build.0 = Release|Any CPU
{B829A2A1-9366-422B-B372-5DDA33BD2690}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B829A2A1-9366-422B-B372-5DDA33BD2690}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B829A2A1-9366-422B-B372-5DDA33BD2690}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B829A2A1-9366-422B-B372-5DDA33BD2690}.Release|Any CPU.Build.0 = Release|Any CPU
{8F1D203C-1A76-4E76-AFC9-7B7477EB143E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8F1D203C-1A76-4E76-AFC9-7B7477EB143E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8F1D203C-1A76-4E76-AFC9-7B7477EB143E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8F1D203C-1A76-4E76-AFC9-7B7477EB143E}.Release|Any CPU.Build.0 = Release|Any CPU
{F36ABF3F-5707-463C-BC33-DF8A261E4B5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F36ABF3F-5707-463C-BC33-DF8A261E4B5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F36ABF3F-5707-463C-BC33-DF8A261E4B5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F36ABF3F-5707-463C-BC33-DF8A261E4B5A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{8B578810-E61F-4C5D-89B0-DE62B85346F7} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{0B68D83C-1751-427D-840D-1285A3DB98D0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{8D09ADAE-591C-4487-924F-2339D29EE60D} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{20EAEB50-C4FD-4A2D-92CC-8D0241556617} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{B829A2A1-9366-422B-B372-5DDA33BD2690} = {93BEE101-D8F6-4622-95B6-E135AA9C066E}
{8F1D203C-1A76-4E76-AFC9-7B7477EB143E} = {93BEE101-D8F6-4622-95B6-E135AA9C066E}
{F36ABF3F-5707-463C-BC33-DF8A261E4B5A} = {93BEE101-D8F6-4622-95B6-E135AA9C066E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B7CB31CD-9866-48CB-8A83-9F2EB1E9E53B} SolutionGuid = {B7CB31CD-9866-48CB-8A83-9F2EB1E9E53B}
EndGlobalSection EndGlobalSection

View File

@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace Liber_Incantamentum.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
}

View File

@ -1,6 +0,0 @@
@Liber_Incantamentum_HostAddress = http://localhost:5225
GET {{Liber_Incantamentum_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@ -1,13 +0,0 @@
namespace Liber_Incantamentum
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}

View File

@ -38,14 +38,14 @@ Développer une API professionnelle respectant les bonnes pratiques modernes :
Liber_Incantamentum/ Liber_Incantamentum/
├── src/ ├── src/
│ ├── Liber_Incantamentum.Api/ → API (controllers, middlewares) │ ├── Liber_Incantamentum.Api/
│ ├── Liber_Incantamentum.Application/ → Use cases (logique métier) │ ├── Liber_Incantamentum.Application/
│ ├── Liber_Incantamentum.Domain/ → Entités, Enums, Interfaces │ ├── Liber_Incantamentum.Domain/
│ └── Liber_Incantamentum.Infrastructure/ → Persistance (ex: EF Core) │ └── Liber_Incantamentum.Infrastructure/
└── tests/ └── tests/
├── Liber_Incantamentum.Tests.Unit/ → Tests unitaires ├── Liber_Incantamentum.Tests.Unit/
└── Liber_Incantamentum.Tests.Integration/→ Tests dintégration & sécurité └── Liber_Incantamentum.Tests.Integration/
``` ```
--- ---
@ -118,35 +118,6 @@ Toutes les routes sont protégées par une clé API :
--- ---
## ▶️ Lancer le projet
```bash
# Compilation
dotnet build
# Lancer lAPI
dotnet run --project src/Liber_Incantamentum.Api
```
---
## 🧪 Lancer les tests
```bash
dotnet test
```
---
## ✅ Fichiers présents à la racine
- `Liber_Incantamentum.sln` — Solution .NET
- `.editorconfig` — Convention de code
- `.gitignore` — Fichiers à ignorer
- `README.md` — Documentation du projet
---
## ✨ Technologies utilisées ## ✨ Technologies utilisées
- C# / .NET 9 - C# / .NET 9