Skip to content

Commit 21ab261

Browse files
Merge pull request #27 from pierrick-fonquerne/FRO-10-Connexion-utilisateur
Fro 10 connexion utilisateur
2 parents 149e72d + b75ae12 commit 21ab261

File tree

35 files changed

+2264
-73
lines changed

35 files changed

+2264
-73
lines changed

src/backend/src/FantasyRealm.Api/Controllers/AuthController.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,30 @@ public async Task<IActionResult> Register([FromBody] RegisterRequest request, Ca
4242

4343
return CreatedAtAction(nameof(Register), new { id = result.Value!.Id }, result.Value);
4444
}
45+
46+
/// <summary>
47+
/// Authenticates a user and returns a JWT token.
48+
/// </summary>
49+
/// <param name="request">The login credentials.</param>
50+
/// <param name="cancellationToken">Cancellation token.</param>
51+
/// <returns>The JWT token and user information.</returns>
52+
/// <response code="200">Login successful.</response>
53+
/// <response code="401">Invalid credentials.</response>
54+
/// <response code="403">Account suspended.</response>
55+
[HttpPost("login")]
56+
[ProducesResponseType(typeof(LoginResponse), StatusCodes.Status200OK)]
57+
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
58+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
59+
public async Task<IActionResult> Login([FromBody] LoginRequest request, CancellationToken cancellationToken)
60+
{
61+
var result = await _authService.LoginAsync(request, cancellationToken);
62+
63+
if (result.IsFailure)
64+
{
65+
return StatusCode(result.ErrorCode ?? 401, new { message = result.Error });
66+
}
67+
68+
return Ok(result.Value);
69+
}
4570
}
4671
}

src/backend/src/FantasyRealm.Api/FantasyRealm.Api.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
</PropertyGroup>
99

1010
<ItemGroup>
11-
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.11" />
11+
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.*" />
1212
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.11" />
1313
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
1414
</ItemGroup>

src/backend/src/FantasyRealm.Api/Program.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
using System.Text;
12
using FantasyRealm.Infrastructure;
3+
using FantasyRealm.Infrastructure.Security;
4+
using Microsoft.AspNetCore.Authentication.JwtBearer;
5+
using Microsoft.IdentityModel.Tokens;
26

37
namespace FantasyRealm.Api
48
{
@@ -31,6 +35,27 @@ private static void Main(string[] args)
3135
// Infrastructure services (Database, Email, Auth)
3236
builder.Services.AddInfrastructure(builder.Configuration);
3337

38+
// JWT Authentication configuration
39+
var jwtSettings = builder.Configuration.GetSection(JwtSettings.SectionName).Get<JwtSettings>()!;
40+
builder.Services.AddAuthentication(options =>
41+
{
42+
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
43+
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
44+
})
45+
.AddJwtBearer(options =>
46+
{
47+
options.TokenValidationParameters = new TokenValidationParameters
48+
{
49+
ValidateIssuer = true,
50+
ValidateAudience = true,
51+
ValidateLifetime = true,
52+
ValidateIssuerSigningKey = true,
53+
ValidIssuer = jwtSettings.Issuer,
54+
ValidAudience = jwtSettings.Audience,
55+
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret))
56+
};
57+
});
58+
3459
var app = builder.Build();
3560

3661
// Configure the HTTP request pipeline
@@ -45,6 +70,7 @@ private static void Main(string[] args)
4570

4671
app.UseHttpsRedirection();
4772
app.UseCors("AllowFrontend");
73+
app.UseAuthentication();
4874
app.UseAuthorization();
4975
app.MapControllers();
5076

src/backend/src/FantasyRealm.Api/appsettings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,11 @@
2121
"Password": "",
2222
"FromAddress": "noreply@fantasy-realm.com",
2323
"FromName": "FantasyRealm"
24+
},
25+
"Jwt": {
26+
"Secret": "CHANGE_ME_IN_PRODUCTION_THIS_IS_A_DEV_SECRET_KEY_MIN_32_CHARS",
27+
"Issuer": "FantasyRealm",
28+
"Audience": "FantasyRealmUsers",
29+
"ExpirationHours": 24
2430
}
2531
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace FantasyRealm.Application.DTOs
4+
{
5+
/// <summary>
6+
/// Request payload for user login.
7+
/// </summary>
8+
public sealed record LoginRequest(
9+
[Required(ErrorMessage = "L'email est requis.")]
10+
[EmailAddress(ErrorMessage = "Le format de l'email est invalide.")]
11+
string Email,
12+
13+
[Required(ErrorMessage = "Le mot de passe est requis.")]
14+
string Password
15+
);
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace FantasyRealm.Application.DTOs
2+
{
3+
/// <summary>
4+
/// Response payload for successful login.
5+
/// </summary>
6+
public sealed record LoginResponse(
7+
string Token,
8+
DateTime ExpiresAt,
9+
UserInfo User,
10+
bool MustChangePassword
11+
);
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace FantasyRealm.Application.DTOs
2+
{
3+
/// <summary>
4+
/// Basic user information returned in authentication responses.
5+
/// </summary>
6+
public sealed record UserInfo(
7+
int Id,
8+
string Email,
9+
string Pseudo,
10+
string Role
11+
);
12+
}

src/backend/src/FantasyRealm.Application/Interfaces/IAuthService.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,13 @@ public interface IAuthService
1515
/// <param name="cancellationToken">Cancellation token.</param>
1616
/// <returns>A result containing the registered user info or an error.</returns>
1717
Task<Result<RegisterResponse>> RegisterAsync(RegisterRequest request, CancellationToken cancellationToken = default);
18+
19+
/// <summary>
20+
/// Authenticates a user and returns a JWT token.
21+
/// </summary>
22+
/// <param name="request">The login request containing credentials.</param>
23+
/// <param name="cancellationToken">Cancellation token.</param>
24+
/// <returns>A result containing the login response with token or an error.</returns>
25+
Task<Result<LoginResponse>> LoginAsync(LoginRequest request, CancellationToken cancellationToken = default);
1826
}
1927
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace FantasyRealm.Application.Interfaces
2+
{
3+
/// <summary>
4+
/// Service interface for JWT token operations.
5+
/// </summary>
6+
public interface IJwtService
7+
{
8+
/// <summary>
9+
/// Generates a JWT token for the specified user.
10+
/// </summary>
11+
/// <param name="userId">The user's unique identifier.</param>
12+
/// <param name="email">The user's email address.</param>
13+
/// <param name="pseudo">The user's display name.</param>
14+
/// <param name="role">The user's role.</param>
15+
/// <returns>A signed JWT token string.</returns>
16+
string GenerateToken(int userId, string email, string pseudo, string role);
17+
18+
/// <summary>
19+
/// Gets the expiration date for a newly generated token.
20+
/// </summary>
21+
/// <returns>The UTC datetime when the token will expire.</returns>
22+
DateTime GetExpirationDate();
23+
}
24+
}

src/backend/src/FantasyRealm.Application/Interfaces/IUserRepository.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,13 @@ public interface IUserRepository
2727
/// Retrieves a role by its label.
2828
/// </summary>
2929
Task<Role?> GetRoleByLabelAsync(string label, CancellationToken cancellationToken = default);
30+
31+
/// <summary>
32+
/// Retrieves a user by email address, including their role.
33+
/// </summary>
34+
/// <param name="email">The email address to search for (case-insensitive).</param>
35+
/// <param name="cancellationToken">Cancellation token.</param>
36+
/// <returns>The user with their role, or null if not found.</returns>
37+
Task<User?> GetByEmailWithRoleAsync(string email, CancellationToken cancellationToken = default);
3038
}
3139
}

0 commit comments

Comments
 (0)