Skip to content

Commit f2339b1

Browse files
Merge pull request #109 from pierrick-fonquerne/62-dashboard-utilisateur
62 dashboard utilisateur
2 parents 9f2c176 + 6525675 commit f2339b1

27 files changed

+2399
-112
lines changed

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,65 @@ public async Task<IActionResult> CheckNameAvailability(
203203
return Ok(new { available = result.Value });
204204
}
205205

206+
/// <summary>
207+
/// Duplicates an approved character with a new name.
208+
/// </summary>
209+
/// <param name="id">The character identifier to duplicate.</param>
210+
/// <param name="request">The duplicate request containing the new name.</param>
211+
/// <param name="cancellationToken">Cancellation token.</param>
212+
/// <returns>The newly created character in Draft status.</returns>
213+
/// <response code="201">Character duplicated successfully.</response>
214+
/// <response code="400">Character status does not allow duplication (must be Approved) or invalid name.</response>
215+
/// <response code="403">The authenticated user is not the owner.</response>
216+
/// <response code="404">Character not found.</response>
217+
/// <response code="409">A character with the same name already exists for this user.</response>
218+
[HttpPost("{id:int}/duplicate")]
219+
[ProducesResponseType(typeof(CharacterResponse), StatusCodes.Status201Created)]
220+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
221+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
222+
[ProducesResponseType(StatusCodes.Status404NotFound)]
223+
[ProducesResponseType(StatusCodes.Status409Conflict)]
224+
public async Task<IActionResult> Duplicate(int id, [FromBody] DuplicateCharacterRequest request, CancellationToken cancellationToken)
225+
{
226+
if (!TryGetUserId(out var userId))
227+
return Unauthorized(new { message = "Token invalide." });
228+
229+
var result = await characterService.DuplicateAsync(id, userId, request.Name, cancellationToken);
230+
231+
if (result.IsFailure)
232+
return StatusCode(result.ErrorCode ?? 400, new { message = result.Error });
233+
234+
return CreatedAtAction(nameof(GetById), new { id = result.Value!.Id }, result.Value);
235+
}
236+
237+
/// <summary>
238+
/// Toggles the sharing status of an approved character.
239+
/// </summary>
240+
/// <param name="id">The character identifier.</param>
241+
/// <param name="cancellationToken">Cancellation token.</param>
242+
/// <returns>The updated character with toggled IsShared value.</returns>
243+
/// <response code="200">Sharing status toggled successfully.</response>
244+
/// <response code="400">Character status does not allow sharing (must be Approved).</response>
245+
/// <response code="403">The authenticated user is not the owner.</response>
246+
/// <response code="404">Character not found.</response>
247+
[HttpPatch("{id:int}/share")]
248+
[ProducesResponseType(typeof(CharacterResponse), StatusCodes.Status200OK)]
249+
[ProducesResponseType(StatusCodes.Status400BadRequest)]
250+
[ProducesResponseType(StatusCodes.Status403Forbidden)]
251+
[ProducesResponseType(StatusCodes.Status404NotFound)]
252+
public async Task<IActionResult> ToggleShare(int id, CancellationToken cancellationToken)
253+
{
254+
if (!TryGetUserId(out var userId))
255+
return Unauthorized(new { message = "Token invalide." });
256+
257+
var result = await characterService.ToggleShareAsync(id, userId, cancellationToken);
258+
259+
if (result.IsFailure)
260+
return StatusCode(result.ErrorCode ?? 400, new { message = result.Error });
261+
262+
return Ok(result.Value);
263+
}
264+
206265
private bool TryGetUserId(out int userId)
207266
{
208267
userId = 0;
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
namespace FantasyRealm.Application.DTOs
22
{
33
/// <summary>
4-
/// Lightweight character summary for list views.
4+
/// Character summary for list views including appearance data for preview.
55
/// </summary>
66
public sealed record CharacterSummaryResponse(
77
int Id,
88
string Name,
99
string ClassName,
1010
string Status,
11-
string Gender);
11+
string Gender,
12+
bool IsShared,
13+
string SkinColor,
14+
string HairColor,
15+
string EyeColor,
16+
string FaceShape,
17+
string HairStyle,
18+
string EyeShape,
19+
string NoseShape,
20+
string MouthShape);
1221
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace FantasyRealm.Application.DTOs
2+
{
3+
/// <summary>
4+
/// Request payload for duplicating a character.
5+
/// </summary>
6+
public sealed record DuplicateCharacterRequest(string Name);
7+
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,5 +47,24 @@ public interface ICharacterService
4747
/// <param name="cancellationToken">Cancellation token.</param>
4848
/// <returns>True if the name is available, false otherwise.</returns>
4949
Task<Result<bool>> IsNameAvailableAsync(string name, int userId, int? excludeCharacterId, CancellationToken cancellationToken);
50+
51+
/// <summary>
52+
/// Duplicates an approved character with a new name.
53+
/// </summary>
54+
/// <param name="characterId">The character identifier to duplicate.</param>
55+
/// <param name="userId">The user identifier (must be owner).</param>
56+
/// <param name="newName">The name for the duplicated character.</param>
57+
/// <param name="cancellationToken">Cancellation token.</param>
58+
/// <returns>The newly created character in Draft status.</returns>
59+
Task<Result<CharacterResponse>> DuplicateAsync(int characterId, int userId, string newName, CancellationToken cancellationToken);
60+
61+
/// <summary>
62+
/// Toggles the sharing status of an approved character.
63+
/// </summary>
64+
/// <param name="characterId">The character identifier.</param>
65+
/// <param name="userId">The user identifier (must be owner).</param>
66+
/// <param name="cancellationToken">Cancellation token.</param>
67+
/// <returns>The updated character with toggled IsShared value.</returns>
68+
Task<Result<CharacterResponse>> ToggleShareAsync(int characterId, int userId, CancellationToken cancellationToken);
5069
}
5170
}

src/backend/src/FantasyRealm.Application/Services/CharacterService.cs

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,7 @@ public async Task<Result<CharacterResponse>> GetByIdAsync(int characterId, int u
6868
public async Task<Result<IReadOnlyList<CharacterSummaryResponse>>> GetMyCharactersAsync(int userId, CancellationToken cancellationToken)
6969
{
7070
var characters = await characterRepository.GetByUserIdAsync(userId, cancellationToken);
71-
72-
var summaries = characters
73-
.Select(c => new CharacterSummaryResponse(
74-
c.Id,
75-
c.Name,
76-
c.Class.Name,
77-
c.Status.ToString(),
78-
c.Gender.ToString()))
79-
.ToList() as IReadOnlyList<CharacterSummaryResponse>;
80-
71+
var summaries = characters.Select(MapToSummaryResponse).ToList().AsReadOnly();
8172
return Result<IReadOnlyList<CharacterSummaryResponse>>.Success(summaries);
8273
}
8374

@@ -167,6 +158,70 @@ public async Task<Result<bool>> IsNameAvailableAsync(string name, int userId, in
167158
return Result<bool>.Success(!nameExists);
168159
}
169160

161+
/// <inheritdoc />
162+
public async Task<Result<CharacterResponse>> DuplicateAsync(int characterId, int userId, string newName, CancellationToken cancellationToken)
163+
{
164+
if (string.IsNullOrWhiteSpace(newName))
165+
return Result<CharacterResponse>.Failure("Le nom est requis.", 400);
166+
167+
var character = await characterRepository.GetByIdAsync(characterId, cancellationToken);
168+
if (character is null)
169+
return Result<CharacterResponse>.Failure("Personnage introuvable.", 404);
170+
171+
if (character.UserId != userId)
172+
return Result<CharacterResponse>.Failure("Accès non autorisé.", 403);
173+
174+
if (character.Status != CharacterStatus.Approved)
175+
return Result<CharacterResponse>.Failure("Seuls les personnages approuvés peuvent être dupliqués.", 400);
176+
177+
var nameExists = await characterRepository.ExistsByNameAndUserAsync(newName, userId, null, cancellationToken);
178+
if (nameExists)
179+
return Result<CharacterResponse>.Failure("Vous avez déjà un personnage avec ce nom.", 409);
180+
181+
var duplicate = new Character
182+
{
183+
Name = newName,
184+
ClassId = character.ClassId,
185+
Gender = character.Gender,
186+
Status = CharacterStatus.Draft,
187+
SkinColor = character.SkinColor,
188+
EyeColor = character.EyeColor,
189+
HairColor = character.HairColor,
190+
HairStyle = character.HairStyle,
191+
EyeShape = character.EyeShape,
192+
NoseShape = character.NoseShape,
193+
MouthShape = character.MouthShape,
194+
FaceShape = character.FaceShape,
195+
IsShared = false,
196+
CreatedAt = DateTime.UtcNow,
197+
UpdatedAt = DateTime.UtcNow,
198+
UserId = userId
199+
};
200+
201+
var created = await characterRepository.CreateAsync(duplicate, cancellationToken);
202+
return Result<CharacterResponse>.Success(MapToResponse(created, character.Class.Name));
203+
}
204+
205+
/// <inheritdoc />
206+
public async Task<Result<CharacterResponse>> ToggleShareAsync(int characterId, int userId, CancellationToken cancellationToken)
207+
{
208+
var character = await characterRepository.GetByIdAsync(characterId, cancellationToken);
209+
if (character is null)
210+
return Result<CharacterResponse>.Failure("Personnage introuvable.", 404);
211+
212+
if (character.UserId != userId)
213+
return Result<CharacterResponse>.Failure("Accès non autorisé.", 403);
214+
215+
if (character.Status != CharacterStatus.Approved)
216+
return Result<CharacterResponse>.Failure("Seuls les personnages approuvés peuvent être partagés.", 400);
217+
218+
character.IsShared = !character.IsShared;
219+
character.UpdatedAt = DateTime.UtcNow;
220+
221+
await characterRepository.UpdateAsync(character, cancellationToken);
222+
return Result<CharacterResponse>.Success(MapToResponse(character, character.Class.Name));
223+
}
224+
170225
private static CharacterResponse MapToResponse(Character character, string className)
171226
{
172227
return new CharacterResponse(
@@ -188,5 +243,24 @@ private static CharacterResponse MapToResponse(Character character, string class
188243
character.CreatedAt,
189244
character.UpdatedAt);
190245
}
246+
247+
private static CharacterSummaryResponse MapToSummaryResponse(Character character)
248+
{
249+
return new CharacterSummaryResponse(
250+
character.Id,
251+
character.Name,
252+
character.Class.Name,
253+
character.Status.ToString(),
254+
character.Gender.ToString(),
255+
character.IsShared,
256+
character.SkinColor,
257+
character.HairColor,
258+
character.EyeColor,
259+
character.FaceShape,
260+
character.HairStyle,
261+
character.EyeShape,
262+
character.NoseShape,
263+
character.MouthShape);
264+
}
191265
}
192266
}

src/backend/tests/FantasyRealm.Tests.Integration/Controllers/AuthControllerIntegrationTests.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,10 @@ namespace FantasyRealm.Tests.Integration.Controllers
1616
/// </summary>
1717
[Trait("Category", "Integration")]
1818
[Trait("Category", "Auth")]
19-
public class AuthControllerIntegrationTests : IClassFixture<FantasyRealmWebApplicationFactory>
19+
public class AuthControllerIntegrationTests(FantasyRealmWebApplicationFactory factory) : IClassFixture<FantasyRealmWebApplicationFactory>
2020
{
21-
private readonly HttpClient _client;
22-
private readonly FantasyRealmWebApplicationFactory _factory;
23-
24-
public AuthControllerIntegrationTests(FantasyRealmWebApplicationFactory factory)
25-
{
26-
_factory = factory;
27-
_client = factory.CreateClient();
28-
}
21+
private readonly HttpClient _client = factory.CreateClient();
22+
private readonly FantasyRealmWebApplicationFactory _factory = factory;
2923

3024
[Fact]
3125
public async Task Register_WithValidData_ReturnsCreatedAndUserResponse()

0 commit comments

Comments
 (0)