From cac55b0772ea6b2aca4be4a6d9706fc06a8a19f9 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Thu, 23 Nov 2023 17:58:50 -0500 Subject: [PATCH 01/43] Building foundations --- .../src/components/Profile/Profile.js | 37 +++++- .../src/components/Profile/ProfileClan.js | 36 ++++++ .../src/components/Profile/ProfileDeck.js | 36 ++++++ .../src/components/Profile/ProfileIdentity.js | 31 ++--- .../src/components/Profile/ProfilePlayer.js | 106 +++++++----------- 5 files changed, 166 insertions(+), 80 deletions(-) create mode 100644 src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js create mode 100644 src/WebAPI/ClientApp/src/components/Profile/ProfileDeck.js diff --git a/src/WebAPI/ClientApp/src/components/Profile/Profile.js b/src/WebAPI/ClientApp/src/components/Profile/Profile.js index ac7677d..4a31ff5 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/Profile.js +++ b/src/WebAPI/ClientApp/src/components/Profile/Profile.js @@ -17,23 +17,54 @@ import { Avatar } from '../Avatar' import { Col, Container, Row } from 'reactstrap' import { Nav, NavItem, NavLink } from 'reactstrap' +import { PlayerClient } from '../../webApiClient.ts' +import { ProfileClan } from './ProfileClan' +import { ProfileDeck } from './ProfileDeck' import { ProfileIdentity } from './ProfileIdentity' import { ProfilePlayer } from './ProfilePlayer' import { useAuthorize } from '../../services/AuthorizeProvider' +import { useErrorReporter } from '../ErrorReporter' import { WaitSpinner } from '../WaitSpinner' -import React, { useState } from 'react' +import React, { useEffect, useState } from 'react' export function Profile () { const { isAuthorized, userProfile } = useAuthorize () const [ activeIndex, setActiveIndex ] = useState (0) + const [ playerProfile, setPlayerProfile ] = useState (-1) + const [ playerClient ] = useState (new PlayerClient ()) + const errorReporter = useErrorReporter () + + const downProps = { playerProfile, userProfile } const pages = [ - { title: 'Identity', component: }, - { title: 'Player', component: }, + { title: 'Identity', component: }, + { title: 'Player', component: }, + { separator : true }, + { title: 'Clan', component: }, + { title: 'Deck', component: }, ] + useEffect (() => + { + const refreshPlayer = async () => + { + if (isAuthorized) try + { + return await playerClient.get (-1) + } + catch (error) + { + errorReporter (error) + } + } + + setPlayerProfile (undefined) + refreshPlayer ().then ((player) => setPlayerProfile (player)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isAuthorized]) + return ( !isAuthorized ? diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js new file mode 100644 index 0000000..81540e7 --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js @@ -0,0 +1,36 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +import { Alert } from 'reactstrap' +import { WaitSpinner } from '../WaitSpinner' +import React, { useState } from 'react' +import { ProfilePage } from './ProfilePage' + +export function ProfileClan (props) +{ + const { playerProfile } = props + const [ isLoading, setIsLoading ] = useState (false) + + if (!playerProfile) + return (User has not player status) + else + return ( + isLoading + ? + : +

Player clan placeholder

+
) +} diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileDeck.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileDeck.js new file mode 100644 index 0000000..24b13f1 --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileDeck.js @@ -0,0 +1,36 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +import { Alert } from 'reactstrap' +import { WaitSpinner } from '../WaitSpinner' +import React, { useState } from 'react' +import { ProfilePage } from './ProfilePage' + +export function ProfileDeck (props) +{ + const { playerProfile } = props + const [ isLoading, setIsLoading ] = useState (false) + + if (!playerProfile) + return (User has not player status) + else + return ( + isLoading + ? + : +

Player cards placeholder

+
) +} diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileIdentity.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileIdentity.js index 341faee..e7d2794 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/ProfileIdentity.js +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileIdentity.js @@ -32,18 +32,21 @@ export function ProfileIdentity (props) errorReporter (new Error ('unimplemented')) } - return ( - -
{ onSubmit (e) }}> - - setUserEmail (e.target.value)} value={userEmail} disabled/> - - - - setUserName (e.target.value)} value={userName} /> - - - -
-
) + if (!userProfile) + throw new Error ('This was unexpected') + else + return ( + +
{ onSubmit (e) }}> + + setUserEmail (e.target.value)} value={userEmail} disabled/> + + + + setUserName (e.target.value)} value={userName} /> + + + +
+
) } diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfilePlayer.js b/src/WebAPI/ClientApp/src/components/Profile/ProfilePlayer.js index cb5cf99..da4fc92 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/ProfilePlayer.js +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfilePlayer.js @@ -14,32 +14,34 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ -import { Button, Form, FormGroup, Input, Label } from 'reactstrap' +import { Alert, Button } from 'reactstrap' +import { Form, FormGroup, Input, Label } from 'reactstrap' import { PlayerClient, UpdatePlayerCommand } from '../../webApiClient.ts' import { ProfilePage } from './ProfilePage' import { useErrorReporter } from '../ErrorReporter' import { WaitSpinner } from '../WaitSpinner' -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' export function ProfilePlayer (props) { + const { playerProfile } = props const [ isLoading, setIsLoading ] = useState (false) const [ playerClient ] = useState (new PlayerClient ()) - const [ playerId, setPlayerId ] = useState (-1) - const [ playerLevel, setPlayerLevel ] = useState (0) - const [ playerNickname, setPlayerNickname ] = useState ('') - const [ playerTotalCardsFound, setPlayerTotalCardsFound ] = useState (0) - const [ playerTotalThrophies, setPlayerTotalThrophies ] = useState (0) - const [ playerTotalWins, setPlayerTotalWins ] = useState (0) + const [ playerLevel, setPlayerLevel ] = useState (playerProfile.level) + const [ playerNickname, setPlayerNickname ] = useState (playerProfile.nickname) + const [ playerTotalCardsFound, setPlayerTotalCardsFound ] = useState (playerProfile.totalCardsFound) + const [ playerTotalThrophies, setPlayerTotalThrophies ] = useState (playerProfile.totalThrophies) + const [ playerTotalWins, setPlayerTotalWins ] = useState (playerProfile.totalWins) const errorReporter = useErrorReporter () const onSubmit = async (e) => { e.preventDefault () + const id = playerProfile.id const command = new UpdatePlayerCommand () - command.id = playerId + command.id = id command.level = playerLevel command.nickname = playerNickname === '' ? undefined : playerNickname command.totalCardsFound = playerTotalCardsFound @@ -47,64 +49,42 @@ export function ProfilePlayer (props) command.totalWins = playerTotalWins try { - await playerClient.update (playerId, command) + await playerClient.update (id, command) } catch (error) { errorReporter (error) } } - useEffect (() => - { - const refreshForm = async () => - { - try { - const data = await playerClient.get (-1) - - setPlayerId (data.id) - setPlayerLevel (data.level) - setPlayerNickname (data.nickname ?? '') - setPlayerTotalCardsFound (data.totalCardsFound) - setPlayerTotalThrophies (data.totalThrophies) - setPlayerTotalWins (data.totalWins) - setIsLoading (false) - } catch (error) - { - errorReporter (error) - } - } - - setIsLoading (true) - refreshForm ().then (() => setIsLoading (false)) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - return ( - isLoading - ? - : -
{ setIsLoading (true); onSubmit (e).then (() => setIsLoading (false)) }}> - - setPlayerLevel (e.target.value)} value={playerLevel} /> - - - - setPlayerNickname(e.target.value)} value={playerNickname} /> - - - - setPlayerTotalCardsFound (e.target.value)} value={playerTotalCardsFound} /> - - - - setPlayerTotalThrophies (e.target.value)} value={playerTotalThrophies} /> - - - - setPlayerTotalWins (e.target.value)} value={playerTotalWins} /> - - - -
-
) + if (!playerProfile) + return (User has not player status) + else + return ( + isLoading + ? + : +
{ setIsLoading (true); onSubmit (e).then (() => setIsLoading (false)) }}> + + setPlayerLevel (e.target.value)} value={playerLevel} /> + + + + setPlayerNickname(e.target.value)} value={playerNickname} /> + + + + setPlayerTotalCardsFound (e.target.value)} value={playerTotalCardsFound} /> + + + + setPlayerTotalThrophies (e.target.value)} value={playerTotalThrophies} /> + + + + setPlayerTotalWins (e.target.value)} value={playerTotalWins} /> + + + +
+
) } From deabe1842fff6ff2038098434e3d83f4b67dccff Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Thu, 23 Nov 2023 18:52:18 -0500 Subject: [PATCH 02/43] Clan controller --- .../Command/CreateClan/CreateClanCommand.cs | 66 +++++++++++++++++++ .../CreateClan/CreateClanCommandValidator.cs | 33 ++++++++++ .../Command/DeleteClan/DeleteClanCommand.cs | 53 +++++++++++++++ .../Command/UpdateClan/UpdateClanCommand.cs | 64 ++++++++++++++++++ .../UpdateClan/UpdateClanCommandValidator.cs | 34 ++++++++++ .../EventHandlers/ClanCreateEventHandler.cs | 38 +++++++++++ .../EventHandlers/ClanDeletedEventHandler.cs | 38 +++++++++++ .../EventHandlers/ClanUpdatedEventHandler.cs | 38 +++++++++++ .../GetClansWithPagination/ClanBriefDto.cs | 33 ++++++++++ .../GetClansWithPaginationQuery.cs | 52 +++++++++++++++ .../GetClansWithPaginationQueryValidator.cs | 29 ++++++++ src/Domain/Events/ClanCreatedEvent.cs | 31 +++++++++ src/Domain/Events/ClanDeletedEvent.cs | 31 +++++++++ src/Domain/Events/ClanUpdatedEvent.cs | 31 +++++++++ src/WebAPI/Controllers/ClanController.cs | 64 ++++++++++++++++++ 15 files changed, 635 insertions(+) create mode 100644 src/Application/Clan/Command/CreateClan/CreateClanCommand.cs create mode 100644 src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs create mode 100644 src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs create mode 100644 src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs create mode 100644 src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs create mode 100644 src/Application/Clan/EventHandlers/ClanCreateEventHandler.cs create mode 100644 src/Application/Clan/EventHandlers/ClanDeletedEventHandler.cs create mode 100644 src/Application/Clan/EventHandlers/ClanUpdatedEventHandler.cs create mode 100644 src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs create mode 100644 src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs create mode 100644 src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQueryValidator.cs create mode 100644 src/Domain/Events/ClanCreatedEvent.cs create mode 100644 src/Domain/Events/ClanDeletedEvent.cs create mode 100644 src/Domain/Events/ClanUpdatedEvent.cs create mode 100644 src/WebAPI/Controllers/ClanController.cs diff --git a/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs b/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs new file mode 100644 index 0000000..bfe3bc1 --- /dev/null +++ b/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs @@ -0,0 +1,66 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.Events; +using DataClash.Domain.ValueObjects; +using MediatR; + +namespace DataClash.Application.Clans.Commands.CreateClan +{ + [Authorize] + public record CreateClanCommand : IRequest + { + public string? Description { get; init; } + public string? Name { get; init; } + public Region? Region { get; init; } + public long TotalTrophiesToEnter { get; init; } + public long TotalTrophiesWonOnWar { get; init; } + public ClanType Type { get; init; } + } + + public class CreateClanCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public CreateClanCommandHandler (IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle (CreateClanCommand request, CancellationToken cancellationToken) + { + var entity = new Clan + { + Description = request.Description, + Name = request.Name, + Region = request.Region, + TotalTrophiesToEnter = request.TotalTrophiesToEnter, + TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar, + Type = request.Type, + }; + + entity.AddDomainEvent (new ClanCreatedEvent (entity)); + _context.Clans.Add (entity); + + await _context.SaveChangesAsync (cancellationToken); + return entity.Id; + } + } +} diff --git a/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs b/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs new file mode 100644 index 0000000..a50f3e2 --- /dev/null +++ b/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs @@ -0,0 +1,33 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Clans.Commands.CreateClan +{ + public class CreateClanCommandValidator : AbstractValidator + { + public CreateClanCommandValidator () + { + RuleFor (v => v.Description).NotEmpty ().MaximumLength (256); + RuleFor (v => v.Name).NotEmpty ().MaximumLength (128); + RuleFor (v => v.Region).NotEmpty (); + RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); + RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); + RuleFor (v => v.Type).NotEmpty (); + } + } +} diff --git a/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs new file mode 100644 index 0000000..3838079 --- /dev/null +++ b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs @@ -0,0 +1,53 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace DataClash.Application.Clans.Commands.DeleteClan +{ + [Authorize] + public record DeleteClanCommand (long Id) : IRequest; + + public class DeleteClanCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public DeleteClanCommandHandler (IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle (DeleteClanCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Clans + .Where (l => l.Id == request.Id) + .SingleOrDefaultAsync (cancellationToken) + ?? throw new NotFoundException (nameof (Clan), request.Id); + + _context.Clans.Remove (entity); + entity.AddDomainEvent (new ClanDeletedEvent (entity)); + + await _context.SaveChangesAsync (cancellationToken); + } + } +} + diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs new file mode 100644 index 0000000..1ae48f1 --- /dev/null +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs @@ -0,0 +1,64 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.Events; +using DataClash.Domain.ValueObjects; +using MediatR; + +namespace DataClash.Application.Clans.Commands.UpdateClan +{ + [Authorize] + public record UpdateClanCommand : IRequest + { + public long Id { get; init; } + public string? Description { get; init; } + public string? Name { get; init; } + public Region? Region { get; init; } + public long TotalTrophiesToEnter { get; init; } + public long TotalTrophiesWonOnWar { get; init; } + public ClanType Type { get; init; } + } + + public class UpdateClanCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public UpdateClanCommandHandler (IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle (UpdateClanCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Clans.FindAsync (new object [] { request.Id }, cancellationToken) ?? throw new NotFoundException (nameof (Clan), request.Id); + + entity.Description = request.Description; + entity.Name = request.Name; + entity.Region = request.Region; + entity.TotalTrophiesToEnter = request.TotalTrophiesToEnter; + entity.TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar; + entity.Type = request.Type; + + entity.AddDomainEvent (new ClanUpdatedEvent (entity)); + await _context.SaveChangesAsync (cancellationToken); + } + } +} diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs new file mode 100644 index 0000000..0af8ead --- /dev/null +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs @@ -0,0 +1,34 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Clans.Commands.UpdateClan +{ + public class UpdateClanCommandValidator : AbstractValidator + { + public UpdateClanCommandValidator () + { + RuleFor (v => v.Id).NotEmpty (); + RuleFor (v => v.Description).NotEmpty ().MaximumLength (256); + RuleFor (v => v.Name).NotEmpty ().MaximumLength (128); + RuleFor (v => v.Region).NotEmpty (); + RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); + RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); + RuleFor (v => v.Type).NotEmpty (); + } + } +} diff --git a/src/Application/Clan/EventHandlers/ClanCreateEventHandler.cs b/src/Application/Clan/EventHandlers/ClanCreateEventHandler.cs new file mode 100644 index 0000000..0f30974 --- /dev/null +++ b/src/Application/Clan/EventHandlers/ClanCreateEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Clans.EventHandlers +{ + public class ClanCreatedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public ClanCreatedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (ClanCreatedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Clan/EventHandlers/ClanDeletedEventHandler.cs b/src/Application/Clan/EventHandlers/ClanDeletedEventHandler.cs new file mode 100644 index 0000000..2249189 --- /dev/null +++ b/src/Application/Clan/EventHandlers/ClanDeletedEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Clans.EventHandlers +{ + public class ClanDeletedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public ClanDeletedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (ClanDeletedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Clan/EventHandlers/ClanUpdatedEventHandler.cs b/src/Application/Clan/EventHandlers/ClanUpdatedEventHandler.cs new file mode 100644 index 0000000..be5524e --- /dev/null +++ b/src/Application/Clan/EventHandlers/ClanUpdatedEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Clans.EventHandlers +{ + public class ClanUpdatedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public ClanUpdatedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (ClanUpdatedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs b/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs new file mode 100644 index 0000000..0098919 --- /dev/null +++ b/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs @@ -0,0 +1,33 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Mappings; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.ValueObjects; + +namespace DataClash.Application.Clans.Queries.GetClansWithPagination +{ + public class ClanBriefDto : IMapFrom + { + public string? Description { get; init; } + public string? Name { get; init; } + public Region? Region { get; init; } + public long TotalTrophiesToEnter { get; init; } + public long TotalTrophiesWonOnWar { get; init; } + public ClanType Type { get; init; } + } +} diff --git a/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs b/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs new file mode 100644 index 0000000..96be960 --- /dev/null +++ b/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs @@ -0,0 +1,52 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Mappings; +using DataClash.Application.Common.Models; +using DataClash.Application.Common.Security; +using MediatR; + +namespace DataClash.Application.Clans.Queries.GetClansWithPagination +{ + [Authorize] + public record GetClansWithPaginationQuery : IRequest> + { + public int PageNumber { get; init; } = 1; + public int PageSize { get; init; } = 10; + } + + public class GetClansWithPaginationQueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public GetClansWithPaginationQueryHandler (IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle (GetClansWithPaginationQuery query, CancellationToken cancellationToken) + { + return await _context.Wars + .ProjectTo (_mapper.ConfigurationProvider) + .PaginatedListAsync (query.PageNumber, query.PageSize); + } + } +} diff --git a/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQueryValidator.cs b/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQueryValidator.cs new file mode 100644 index 0000000..3a8e9ac --- /dev/null +++ b/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQueryValidator.cs @@ -0,0 +1,29 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Clans.Queries.GetClansWithPagination +{ + public class GetClansWithPaginationQueryValidator : AbstractValidator + { + public GetClansWithPaginationQueryValidator () + { + RuleFor (x => x.PageNumber).GreaterThanOrEqualTo (1).WithMessage ("PageNumber at least greater than or equal to 1."); + RuleFor (x => x.PageSize).GreaterThanOrEqualTo (1).WithMessage ("PageSize at least greater than or equal to 1."); + } + } +} diff --git a/src/Domain/Events/ClanCreatedEvent.cs b/src/Domain/Events/ClanCreatedEvent.cs new file mode 100644 index 0000000..a676c6a --- /dev/null +++ b/src/Domain/Events/ClanCreatedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class ClanCreatedEvent : BaseEvent + { + public Clan Item { get; } + + public ClanCreatedEvent (Clan item) + { + Item = item; + } + } +} diff --git a/src/Domain/Events/ClanDeletedEvent.cs b/src/Domain/Events/ClanDeletedEvent.cs new file mode 100644 index 0000000..6950049 --- /dev/null +++ b/src/Domain/Events/ClanDeletedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class ClanDeletedEvent : BaseEvent + { + public Clan Item { get; } + + public ClanDeletedEvent (Clan item) + { + Item = item; + } + } +} diff --git a/src/Domain/Events/ClanUpdatedEvent.cs b/src/Domain/Events/ClanUpdatedEvent.cs new file mode 100644 index 0000000..8d9c14c --- /dev/null +++ b/src/Domain/Events/ClanUpdatedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class ClanUpdatedEvent : BaseEvent + { + public Clan Item { get; } + + public ClanUpdatedEvent (Clan item) + { + Item = item; + } + } +} diff --git a/src/WebAPI/Controllers/ClanController.cs b/src/WebAPI/Controllers/ClanController.cs new file mode 100644 index 0000000..1defc3f --- /dev/null +++ b/src/WebAPI/Controllers/ClanController.cs @@ -0,0 +1,64 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Clans.Commands.DeleteClan; +using DataClash.Application.Clans.Commands.UpdateClan; +using DataClash.Application.Clans.Queries.GetClansWithPagination; +using DataClash.Application.Common.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DataClash.WebUI.Controllers +{ + [Authorize] + public class ClanController : ApiControllerBase + { + [HttpGet] + public async Task>> GetWithPagination ([FromQuery] GetClansWithPaginationQuery query) + { + return await Mediator.Send (query); + } + + [HttpPost] + public async Task> Create (CreateClanCommand command) + { + return await Mediator.Send (command); + } + + [HttpDelete ("{id}")] + [ProducesResponseType (StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public async Task Delete (long id) + { + await Mediator.Send (new DeleteClanCommand (id)); + return NoContent (); + } + + [HttpPut ("{id}")] + [ProducesResponseType (StatusCodes.Status204NoContent)] + [ProducesResponseType (StatusCodes.Status400BadRequest)] + [ProducesDefaultResponseType] + public async Task Update (long id, UpdateClanCommand command) + { + if (id != command.Id) + return BadRequest (); + + await Mediator.Send (command); + return NoContent (); + } + } +} From 57841c4122d0a9302f0aab1588a1aa9efbe507c3 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Sat, 25 Nov 2023 09:55:50 -0500 Subject: [PATCH 03/43] Clan controller ACL --- .../Command/DeleteClan/DeleteClanCommand.cs | 31 +- .../Command/UpdateClan/UpdateClanCommand.cs | 34 +- .../Interfaces/ICurrentPlayerService.cs | 24 + src/Domain/Entities/PlayerClan.cs | 3 +- src/Domain/Enums/ClanRole.cs | 25 + src/Domain/Enums/Roles.cs | 24 + src/Framework/ConfigureServices.cs | 1 + .../20231125144539_ClanRole.Designer.cs | 901 ++++++++++++++++++ .../Migrations/20231125144539_ClanRole.cs | 22 + .../ApplicationDbContextModelSnapshot.cs | 2 +- .../Services/CurrentPlayerService.cs | 49 + 11 files changed, 1094 insertions(+), 22 deletions(-) create mode 100644 src/Application/Common/Interfaces/ICurrentPlayerService.cs create mode 100644 src/Domain/Enums/ClanRole.cs create mode 100644 src/Domain/Enums/Roles.cs create mode 100644 src/Framework/Persistance/Migrations/20231125144539_ClanRole.Designer.cs create mode 100644 src/Framework/Persistance/Migrations/20231125144539_ClanRole.cs create mode 100644 src/Framework/Services/CurrentPlayerService.cs diff --git a/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs index 3838079..9abdd65 100644 --- a/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs +++ b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs @@ -18,9 +18,9 @@ using DataClash.Application.Common.Interfaces; using DataClash.Application.Common.Security; using DataClash.Domain.Entities; +using DataClash.Domain.Enums; using DataClash.Domain.Events; using MediatR; -using Microsoft.EntityFrameworkCore; namespace DataClash.Application.Clans.Commands.DeleteClan { @@ -30,23 +30,34 @@ public record DeleteClanCommand (long Id) : IRequest; public class DeleteClanCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + private readonly ICurrentUserService _currentUser; + private readonly IIdentityService _identityService; - public DeleteClanCommandHandler (IApplicationDbContext context) + public DeleteClanCommandHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer, ICurrentUserService currentUser, IIdentityService identityService) { - _context = context; + _context = context; + _currentPlayer = currentPlayer; + _currentUser = currentUser; + _identityService = identityService; } public async Task Handle (DeleteClanCommand request, CancellationToken cancellationToken) { - var entity = await _context.Clans - .Where (l => l.Id == request.Id) - .SingleOrDefaultAsync (cancellationToken) - ?? throw new NotFoundException (nameof (Clan), request.Id); + var userId = _currentUser.UserId!; + var entity = await _context.Clans.FindAsync (new object[] { request.Id }, cancellationToken) ?? throw new NotFoundException (nameof (Clan), request.Id); + var playerId = _currentPlayer.PlayerId!; + var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.Id, playerId }, cancellationToken); - _context.Clans.Remove (entity); - entity.AddDomainEvent (new ClanDeletedEvent (entity)); + if (playerClan?.Role != Domain.Enums.ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + throw new ForbiddenAccessException (); + else + { + _context.Clans.Remove (entity); + entity.AddDomainEvent (new ClanDeletedEvent (entity)); - await _context.SaveChangesAsync (cancellationToken); + await _context.SaveChangesAsync (cancellationToken); + } } } } diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs index 1ae48f1..1e56dc9 100644 --- a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs @@ -40,25 +40,39 @@ public record UpdateClanCommand : IRequest public class UpdateClanCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + private readonly ICurrentUserService _currentUser; + private readonly IIdentityService _identityService; - public UpdateClanCommandHandler (IApplicationDbContext context) + public UpdateClanCommandHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer, ICurrentUserService currentUser, IIdentityService identityService) { _context = context; + _currentPlayer = currentPlayer; + _currentUser = currentUser; + _identityService = identityService; } public async Task Handle (UpdateClanCommand request, CancellationToken cancellationToken) { - var entity = await _context.Clans.FindAsync (new object [] { request.Id }, cancellationToken) ?? throw new NotFoundException (nameof (Clan), request.Id); + var userId = _currentUser.UserId!; + var entity = await _context.Clans.FindAsync (new object[] { request.Id }, cancellationToken) ?? throw new NotFoundException (nameof (Clan), request.Id); + var playerId = _currentPlayer.PlayerId!; + var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.Id, playerId }, cancellationToken); - entity.Description = request.Description; - entity.Name = request.Name; - entity.Region = request.Region; - entity.TotalTrophiesToEnter = request.TotalTrophiesToEnter; - entity.TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar; - entity.Type = request.Type; + if (playerClan?.Role != Domain.Enums.ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + throw new ForbiddenAccessException (); + else + { + entity.Description = request.Description; + entity.Name = request.Name; + entity.Region = request.Region; + entity.TotalTrophiesToEnter = request.TotalTrophiesToEnter; + entity.TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar; + entity.Type = request.Type; - entity.AddDomainEvent (new ClanUpdatedEvent (entity)); - await _context.SaveChangesAsync (cancellationToken); + entity.AddDomainEvent (new ClanUpdatedEvent (entity)); + await _context.SaveChangesAsync (cancellationToken); + } } } } diff --git a/src/Application/Common/Interfaces/ICurrentPlayerService.cs b/src/Application/Common/Interfaces/ICurrentPlayerService.cs new file mode 100644 index 0000000..0465142 --- /dev/null +++ b/src/Application/Common/Interfaces/ICurrentPlayerService.cs @@ -0,0 +1,24 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +namespace DataClash.Application.Common.Interfaces +{ + public interface ICurrentPlayerService + { + public long? PlayerId { get; } + } +} diff --git a/src/Domain/Entities/PlayerClan.cs b/src/Domain/Entities/PlayerClan.cs index f6ccffd..28038d2 100644 --- a/src/Domain/Entities/PlayerClan.cs +++ b/src/Domain/Entities/PlayerClan.cs @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Domain.Enums; namespace DataClash.Domain.Entities { @@ -21,7 +22,7 @@ public class PlayerClan { public long ClanId { get; set; } public long PlayerId { get; set; } - public long Role { get; set; } + public ClanRole Role { get; set; } public virtual Clan? Clan { get; set; } public virtual Player? Player { get; set; } diff --git a/src/Domain/Enums/ClanRole.cs b/src/Domain/Enums/ClanRole.cs new file mode 100644 index 0000000..cf5d826 --- /dev/null +++ b/src/Domain/Enums/ClanRole.cs @@ -0,0 +1,25 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +namespace DataClash.Domain.Enums +{ + public enum ClanRole + { + Chief, + Commoner, + } +} diff --git a/src/Domain/Enums/Roles.cs b/src/Domain/Enums/Roles.cs new file mode 100644 index 0000000..80d107b --- /dev/null +++ b/src/Domain/Enums/Roles.cs @@ -0,0 +1,24 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +namespace DataClash.Domain.Enums +{ + public static class Roles + { + public const string Administrator = "Administrator"; + } +} diff --git a/src/Framework/ConfigureServices.cs b/src/Framework/ConfigureServices.cs index b8326d0..376e8c2 100644 --- a/src/Framework/ConfigureServices.cs +++ b/src/Framework/ConfigureServices.cs @@ -44,6 +44,7 @@ public static IServiceCollection AddFrameworkServices (this IServiceCollection s services.AddScoped (provider => provider.GetRequiredService ()); services.AddScoped (); + services.AddScoped (); services .AddDefaultIdentity (options => diff --git a/src/Framework/Persistance/Migrations/20231125144539_ClanRole.Designer.cs b/src/Framework/Persistance/Migrations/20231125144539_ClanRole.Designer.cs new file mode 100644 index 0000000..1a692a8 --- /dev/null +++ b/src/Framework/Persistance/Migrations/20231125144539_ClanRole.Designer.cs @@ -0,0 +1,901 @@ +// +using System; +using DataClash.Framework.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Framework.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20231125144539_ClanRole")] + partial class ClanRole + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + modelBuilder.Entity("DataClash.Domain.Entities.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElixirCost") + .HasColumnType("REAL"); + + b.Property("InitialLevel") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Picture") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Cards"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.CardGift", b => + { + b.Property("ClanId") + .HasColumnType("INTEGER"); + + b.Property("CardId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("ClanId", "CardId", "PlayerId"); + + b.HasIndex("CardId", "PlayerId"); + + b.ToTable("CardGifts"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Challenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BeginDay") + .HasColumnType("TEXT"); + + b.Property("Bounty") + .HasColumnType("INTEGER"); + + b.Property("Cost") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("MaxLooses") + .HasColumnType("INTEGER"); + + b.Property("MinLevel") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Challenges"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Clan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("TotalTrophiesToEnter") + .HasColumnType("INTEGER"); + + b.Property("TotalTrophiesWonOnWar") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Clans"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Match", b => + { + b.Property("LooserPlayerId") + .HasColumnType("INTEGER"); + + b.Property("WinnerPlayerId") + .HasColumnType("INTEGER"); + + b.Property("BeginDate") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.HasKey("LooserPlayerId", "WinnerPlayerId", "BeginDate"); + + b.HasIndex("WinnerPlayerId"); + + b.ToTable("Matches"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FavoriteCardId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("TotalCardsFound") + .HasColumnType("INTEGER"); + + b.Property("TotalThrophies") + .HasColumnType("INTEGER"); + + b.Property("TotalWins") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FavoriteCardId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerCard", b => + { + b.Property("CardId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.HasKey("CardId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PlayerCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerChallenge", b => + { + b.Property("ChallengeId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("WonThrophies") + .HasColumnType("INTEGER"); + + b.HasKey("ChallengeId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PlayerChallenges"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerClan", b => + { + b.Property("ClanId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("ClanId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PlayerClans"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.War", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BeginDay") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Wars"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.WarClan", b => + { + b.Property("ClanId") + .HasColumnType("INTEGER"); + + b.Property("WarId") + .HasColumnType("INTEGER"); + + b.Property("WonThrophies") + .HasColumnType("INTEGER"); + + b.HasKey("ClanId", "WarId"); + + b.HasIndex("WarId"); + + b.ToTable("WarClans"); + }); + + modelBuilder.Entity("DataClash.Framework.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PlayerId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Expiration") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("UserCode"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("Expiration"); + + b.ToTable("DeviceCodes", (string)null); + }); + + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Algorithm") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataProtected") + .HasColumnType("INTEGER"); + + b.Property("IsX509Certificate") + .HasColumnType("INTEGER"); + + b.Property("Use") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Use"); + + b.ToTable("Keys", (string)null); + }); + + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedTime") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Expiration") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.HasIndex("ConsumedTime"); + + b.HasIndex("Expiration"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.HasIndex("SubjectId", "SessionId", "Type"); + + b.ToTable("PersistedGrants", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.MagicCard", b => + { + b.HasBaseType("DataClash.Domain.Entities.Card"); + + b.Property("AreaDamage") + .HasColumnType("REAL"); + + b.Property("DamageRadius") + .HasColumnType("REAL"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("TowerDamage") + .HasColumnType("REAL"); + + b.ToTable("MagicCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.StructCard", b => + { + b.HasBaseType("DataClash.Domain.Entities.Card"); + + b.Property("AttackPaseRate") + .HasColumnType("REAL"); + + b.Property("HitPoints") + .HasColumnType("REAL"); + + b.Property("RangeDamage") + .HasColumnType("REAL"); + + b.ToTable("StructCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.TroopCard", b => + { + b.HasBaseType("DataClash.Domain.Entities.Card"); + + b.Property("AreaDamage") + .HasColumnType("REAL"); + + b.Property("HitPoints") + .HasColumnType("REAL"); + + b.Property("UnitCount") + .HasColumnType("INTEGER"); + + b.ToTable("TroopCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.CardGift", b => + { + b.HasOne("DataClash.Domain.Entities.Clan", "Clan") + .WithMany() + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.PlayerCard", "PlayerCard") + .WithMany() + .HasForeignKey("CardId", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("PlayerCard"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Clan", b => + { + b.OwnsOne("DataClash.Domain.ValueObjects.Region", "Region", b1 => + { + b1.Property("ClanId") + .HasColumnType("INTEGER"); + + b1.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b1.HasKey("ClanId"); + + b1.ToTable("Clans"); + + b1.WithOwner() + .HasForeignKey("ClanId"); + }); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Match", b => + { + b.HasOne("DataClash.Domain.Entities.Player", "LooserPlayer") + .WithMany() + .HasForeignKey("LooserPlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "WinnerPlayer") + .WithMany() + .HasForeignKey("WinnerPlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LooserPlayer"); + + b.Navigation("WinnerPlayer"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Player", b => + { + b.HasOne("DataClash.Domain.Entities.Card", "FavoriteCard") + .WithMany() + .HasForeignKey("FavoriteCardId"); + + b.Navigation("FavoriteCard"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Card"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerChallenge", b => + { + b.HasOne("DataClash.Domain.Entities.Challenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Challenge"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerClan", b => + { + b.HasOne("DataClash.Domain.Entities.Clan", "Clan") + .WithMany() + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.WarClan", b => + { + b.HasOne("DataClash.Domain.Entities.Clan", "Clan") + .WithMany() + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.War", "War") + .WithMany() + .HasForeignKey("WarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("War"); + }); + + modelBuilder.Entity("DataClash.Framework.Identity.ApplicationUser", b => + { + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.MagicCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", null) + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.MagicCard", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.StructCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", null) + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.StructCard", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.TroopCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", null) + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.TroopCard", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Framework/Persistance/Migrations/20231125144539_ClanRole.cs b/src/Framework/Persistance/Migrations/20231125144539_ClanRole.cs new file mode 100644 index 0000000..f97cf8b --- /dev/null +++ b/src/Framework/Persistance/Migrations/20231125144539_ClanRole.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Framework.Migrations +{ + /// + public partial class ClanRole : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs index 89add39..3103f18 100644 --- a/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs @@ -223,7 +223,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PlayerId") .HasColumnType("INTEGER"); - b.Property("Role") + b.Property("Role") .HasColumnType("INTEGER"); b.HasKey("ClanId", "PlayerId"); diff --git a/src/Framework/Services/CurrentPlayerService.cs b/src/Framework/Services/CurrentPlayerService.cs new file mode 100644 index 0000000..ed37bcc --- /dev/null +++ b/src/Framework/Services/CurrentPlayerService.cs @@ -0,0 +1,49 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +using DataClash.Application.Common.Interfaces; +using DataClash.Framework.Persistence; + +namespace DataClash.Framework.Services +{ + public class CurrentPlayerService : ICurrentPlayerService + { + private readonly ApplicationDbContext _context; + private readonly ICurrentUserService _currentUser; + + public long? PlayerId + { + get + { + var userId = _currentUser.UserId; + if (userId == null) + return null; + else + { + var user = _context.Users.Find (new object[] { userId }); + return user?.PlayerId; + } + } + } + + public CurrentPlayerService (ApplicationDbContext context, ICurrentUserService currentUser) + { + _context = context; + _currentUser = currentUser; + } + } +} From 307a02229b80d651bddbee3e69bbaa408a35c8f7 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Sat, 25 Nov 2023 10:17:40 -0500 Subject: [PATCH 04/43] Add to and remove player from clan --- .../Command/AddPlayer/AddPlayerCommand.cs | 70 +++++++++++++++++++ .../AddPlayer/AddPlayerCommandValidator.cs | 30 ++++++++ .../Command/DeleteClan/DeleteClanCommand.cs | 2 +- .../RemovePlayerCommandHandler.cs | 70 +++++++++++++++++++ .../RemovePlayerCommandValidator.cs | 29 ++++++++ .../Command/UpdateClan/UpdateClanCommand.cs | 2 +- .../EventHandlers/PlayerAddedEventHandler.cs | 38 ++++++++++ .../PlayerRemovedEventHandler.cs | 38 ++++++++++ src/Domain/Events/PlayerAddedEvent.cs | 31 ++++++++ src/Domain/Events/PlayerRemovedEvent.cs | 31 ++++++++ 10 files changed, 339 insertions(+), 2 deletions(-) create mode 100644 src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs create mode 100644 src/Application/Clan/Command/AddPlayer/AddPlayerCommandValidator.cs create mode 100644 src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs create mode 100644 src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandValidator.cs create mode 100644 src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs create mode 100644 src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs create mode 100644 src/Domain/Events/PlayerAddedEvent.cs create mode 100644 src/Domain/Events/PlayerRemovedEvent.cs diff --git a/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs b/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs new file mode 100644 index 0000000..74813a5 --- /dev/null +++ b/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs @@ -0,0 +1,70 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.Events; +using MediatR; + +namespace DataClash.Application.Clans.Commands.AddPlayer +{ + [Authorize] + public record AddPlayerCommand : IRequest + { + public long ClanId { get; init; } + public long PlayerId { get; init; } + public ClanRole Role { get; init; } + } + + public class AddPlayerCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + private readonly ICurrentUserService _currentUser; + private readonly IIdentityService _identityService; + + public AddPlayerCommandHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer, ICurrentUserService currentUser, IIdentityService identityService) + { + _context = context; + _currentPlayer = currentPlayer; + _currentUser = currentUser; + _identityService = identityService; + } + + public async Task Handle (AddPlayerCommand request, CancellationToken cancellationToken) + { + var userId = _currentUser.UserId!; + var clan = await _context.Clans.FindAsync (new object[] { request.ClanId }, cancellationToken) ?? throw new NotFoundException (nameof (Clan), request.ClanId); + var playerId = _currentPlayer.PlayerId!; + var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.ClanId, playerId }, cancellationToken); + + if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + throw new ForbiddenAccessException (); + else + { + var entity = new PlayerClan { ClanId = request.ClanId, PlayerId = request.PlayerId, Role = request.Role, }; + + clan.AddDomainEvent (new PlayerAddedEvent (entity)); + _context.PlayerClans.Add (entity); + + await _context.SaveChangesAsync (cancellationToken); + } + } + } +} diff --git a/src/Application/Clan/Command/AddPlayer/AddPlayerCommandValidator.cs b/src/Application/Clan/Command/AddPlayer/AddPlayerCommandValidator.cs new file mode 100644 index 0000000..224e4aa --- /dev/null +++ b/src/Application/Clan/Command/AddPlayer/AddPlayerCommandValidator.cs @@ -0,0 +1,30 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Clans.Commands.AddPlayer +{ + public class AddPlayerCommandValidator : AbstractValidator + { + public AddPlayerCommandValidator () + { + RuleFor (v => v.ClanId).NotEmpty (); + RuleFor (v => v.PlayerId).NotEmpty (); + RuleFor (v => v.Role).IsInEnum (); + } + } +} diff --git a/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs index 9abdd65..7af93a2 100644 --- a/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs +++ b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs @@ -49,7 +49,7 @@ public async Task Handle (DeleteClanCommand request, CancellationToken cancellat var playerId = _currentPlayer.PlayerId!; var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.Id, playerId }, cancellationToken); - if (playerClan?.Role != Domain.Enums.ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) throw new ForbiddenAccessException (); else { diff --git a/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs new file mode 100644 index 0000000..79b0a3a --- /dev/null +++ b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs @@ -0,0 +1,70 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.Events; +using MediatR; + +namespace DataClash.Application.Clans.Commands.RemovePlayer +{ + [Authorize] + public record RemovePlayerCommand : IRequest + { + public long ClanId { get; init; } + public long PlayerId { get; init; } + } + + public class RemovePlayerCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + private readonly ICurrentUserService _currentUser; + private readonly IIdentityService _identityService; + + public RemovePlayerCommandHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer, ICurrentUserService currentUser, IIdentityService identityService) + { + _context = context; + _currentPlayer = currentPlayer; + _currentUser = currentUser; + _identityService = identityService; + } + + public async Task Handle (RemovePlayerCommand request, CancellationToken cancellationToken) + { + var userId = _currentUser.UserId!; + var clan = await _context.Clans.FindAsync (new object[] { request.ClanId }, cancellationToken) ?? throw new NotFoundException (nameof (Clan), request.ClanId); + var playerId = _currentPlayer.PlayerId!; + var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.ClanId, playerId }, cancellationToken); + + if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + throw new ForbiddenAccessException (); + else + { + var entity = await _context.PlayerClans.FindAsync (new object[] { request.ClanId, request.PlayerId }, cancellationToken) + ?? throw new NotFoundException (nameof (PlayerClan), new object[] { request.ClanId, request.PlayerId }); + + _context.PlayerClans.Remove (entity); + clan.AddDomainEvent (new PlayerRemovedEvent (entity)); + + await _context.SaveChangesAsync (cancellationToken); + } + } + } +} diff --git a/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandValidator.cs b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandValidator.cs new file mode 100644 index 0000000..91026ec --- /dev/null +++ b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandValidator.cs @@ -0,0 +1,29 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Clans.Commands.RemovePlayer +{ + public class RemovePlayerCommandValidator : AbstractValidator + { + public RemovePlayerCommandValidator () + { + RuleFor (v => v.ClanId).NotEmpty (); + RuleFor (v => v.PlayerId).NotEmpty (); + } + } +} diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs index 1e56dc9..5c6bf94 100644 --- a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs @@ -59,7 +59,7 @@ public async Task Handle (UpdateClanCommand request, CancellationToken cancellat var playerId = _currentPlayer.PlayerId!; var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.Id, playerId }, cancellationToken); - if (playerClan?.Role != Domain.Enums.ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) throw new ForbiddenAccessException (); else { diff --git a/src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs b/src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs new file mode 100644 index 0000000..bbff507 --- /dev/null +++ b/src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Clans.EventHandlers +{ + public class PlayerAddedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public PlayerAddedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (PlayerAddedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs b/src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs new file mode 100644 index 0000000..f033722 --- /dev/null +++ b/src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Clans.EventHandlers +{ + public class PlayerRemovedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public PlayerRemovedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (PlayerRemovedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Domain/Events/PlayerAddedEvent.cs b/src/Domain/Events/PlayerAddedEvent.cs new file mode 100644 index 0000000..67a250e --- /dev/null +++ b/src/Domain/Events/PlayerAddedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class PlayerAddedEvent : BaseEvent + { + public PlayerClan Item { get; } + + public PlayerAddedEvent (PlayerClan item) + { + Item = item; + } + } +} diff --git a/src/Domain/Events/PlayerRemovedEvent.cs b/src/Domain/Events/PlayerRemovedEvent.cs new file mode 100644 index 0000000..c7046bb --- /dev/null +++ b/src/Domain/Events/PlayerRemovedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class PlayerRemovedEvent : BaseEvent + { + public PlayerClan Item { get; } + + public PlayerRemovedEvent (PlayerClan item) + { + Item = item; + } + } +} From 61be181bc7c7ff4b50b6f16ddb9464062d6f5832 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Sat, 25 Nov 2023 10:48:36 -0500 Subject: [PATCH 05/43] Tests and bug fixes --- .../Command/AddPlayer/AddPlayerCommand.cs | 4 +- .../CreateClan/CreateClanCommandValidator.cs | 2 +- .../CreateClan/CreateClanWithChiefCommand.cs | 81 ++ .../CreateClanWithChiefCommandValidator.cs | 33 + .../Command/DeleteClan/DeleteClanCommand.cs | 2 +- .../RemovePlayerCommandHandler.cs | 2 +- .../Command/UpdateClan/UpdateClanCommand.cs | 2 +- .../UpdateClan/UpdateClanCommandValidator.cs | 2 +- .../GetClanForCurrentPlayerQuery.cs | 60 ++ .../GetClansWithPagination/ClanBriefDto.cs | 1 + .../ApplicationConstraintException.cs | 26 + .../Configurations/PlayerClanConfiguration.cs | 2 +- .../20231125174439_BugFix.Designer.cs | 902 ++++++++++++++++++ .../Migrations/20231125174439_BugFix.cs | 37 + .../ApplicationDbContextModelSnapshot.cs | 7 +- src/WebAPI/Controllers/ClanController.cs | 8 + .../Clan/Commands/AddPlayer.cs | 173 ++++ .../Clan/Commands/CreateClan.cs | 70 ++ .../Clan/Commands/CreateClanWithChief.cs | 147 +++ .../Clan/Commands/DeleteClan.cs | 112 +++ .../Clan/Commands/UpdateClan.cs | 175 ++++ .../Clan/Queries/GetClanForCurrentPlayer.cs | 73 ++ .../Clan/Queries/GetClansWithPagination.cs | 36 + 23 files changed, 1947 insertions(+), 10 deletions(-) create mode 100644 src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs create mode 100644 src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs create mode 100644 src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs create mode 100644 src/Application/Common/Exceptions/ApplicationConstraintException.cs create mode 100644 src/Framework/Persistance/Migrations/20231125174439_BugFix.Designer.cs create mode 100644 src/Framework/Persistance/Migrations/20231125174439_BugFix.cs create mode 100644 tests/Application.IntegrationTests/Clan/Commands/AddPlayer.cs create mode 100644 tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs create mode 100644 tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs create mode 100644 tests/Application.IntegrationTests/Clan/Commands/DeleteClan.cs create mode 100644 tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs create mode 100644 tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs create mode 100644 tests/Application.IntegrationTests/Clan/Queries/GetClansWithPagination.cs diff --git a/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs b/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs index 74813a5..d2de869 100644 --- a/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs +++ b/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs @@ -54,8 +54,10 @@ public async Task Handle (AddPlayerCommand request, CancellationToken cancellati var playerId = _currentPlayer.PlayerId!; var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.ClanId, playerId }, cancellationToken); - if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + if (playerClan?.Role != ClanRole.Chief && await _identityService.IsInRoleAsync (userId, Roles.Administrator) == false) throw new ForbiddenAccessException (); + else if (_context.PlayerClans.Where (e => e.ClanId == clan.Id && e.Role == ClanRole.Chief).Any ()) + throw new ApplicationConstraintException ("Application constraint violation"); else { var entity = new PlayerClan { ClanId = request.ClanId, PlayerId = request.PlayerId, Role = request.Role, }; diff --git a/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs b/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs index a50f3e2..24d3ca6 100644 --- a/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs +++ b/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs @@ -27,7 +27,7 @@ public CreateClanCommandValidator () RuleFor (v => v.Region).NotEmpty (); RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); - RuleFor (v => v.Type).NotEmpty (); + RuleFor (v => v.Type).IsInEnum (); } } } diff --git a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs new file mode 100644 index 0000000..5ef48cb --- /dev/null +++ b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs @@ -0,0 +1,81 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.Events; +using MediatR; + +namespace DataClash.Application.Clans.Commands.CreateClanWithChief +{ + [Authorize] + public record CreateClanWithChiefCommand () : CreateClanCommand; + + public class CreateClanWithChiefCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + + public CreateClanWithChiefCommandHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer) + { + _context = context; + _currentPlayer = currentPlayer; + } + + public async Task Handle (CreateClanWithChiefCommand request, CancellationToken cancellationToken) + { + var playerIdProxy = _currentPlayer.PlayerId; + long playerId; + + if (!_currentPlayer.PlayerId.HasValue) + throw new ApplicationConstraintException ("User is not a player"); + else + playerId = playerIdProxy!.Value; + + var clanEntity = new Clan + { + Description = request.Description, + Name = request.Name, + Region = request.Region, + TotalTrophiesToEnter = request.TotalTrophiesToEnter, + TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar, + Type = request.Type, + }; + + clanEntity.AddDomainEvent (new ClanCreatedEvent (clanEntity)); + _context.Clans.Add (clanEntity); + + await _context.SaveChangesAsync (cancellationToken); + + var playerClanEntity = new PlayerClan + { + ClanId = clanEntity.Id, + PlayerId = playerId, + Role = ClanRole.Chief, + }; + + clanEntity.AddDomainEvent (new PlayerAddedEvent (playerClanEntity)); + _context.PlayerClans.Add (playerClanEntity); + + await _context.SaveChangesAsync (cancellationToken); + return clanEntity.Id; + } + } +} diff --git a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs new file mode 100644 index 0000000..557958a --- /dev/null +++ b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs @@ -0,0 +1,33 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Clans.Commands.CreateClanWithChief +{ + public class CreateClanWithChiefCommandValidator : AbstractValidator + { + public CreateClanWithChiefCommandValidator () + { + RuleFor (v => v.Description).NotEmpty ().MaximumLength (256); + RuleFor (v => v.Name).NotEmpty ().MaximumLength (128); + RuleFor (v => v.Region).NotEmpty (); + RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); + RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); + RuleFor (v => v.Type).IsInEnum (); + } + } +} diff --git a/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs index 7af93a2..6d416c8 100644 --- a/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs +++ b/src/Application/Clan/Command/DeleteClan/DeleteClanCommand.cs @@ -49,7 +49,7 @@ public async Task Handle (DeleteClanCommand request, CancellationToken cancellat var playerId = _currentPlayer.PlayerId!; var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.Id, playerId }, cancellationToken); - if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + if (playerClan?.Role != ClanRole.Chief && await _identityService.IsInRoleAsync (userId, Roles.Administrator) == false) throw new ForbiddenAccessException (); else { diff --git a/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs index 79b0a3a..994ef9f 100644 --- a/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs +++ b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs @@ -53,7 +53,7 @@ public async Task Handle (RemovePlayerCommand request, CancellationToken cancell var playerId = _currentPlayer.PlayerId!; var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.ClanId, playerId }, cancellationToken); - if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + if (playerClan?.Role != ClanRole.Chief && await _identityService.IsInRoleAsync (userId, Roles.Administrator) == false) throw new ForbiddenAccessException (); else { diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs index 5c6bf94..818e402 100644 --- a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs @@ -59,7 +59,7 @@ public async Task Handle (UpdateClanCommand request, CancellationToken cancellat var playerId = _currentPlayer.PlayerId!; var playerClan = await _context.PlayerClans.FindAsync (new object[] { request.Id, playerId }, cancellationToken); - if (playerClan?.Role != ClanRole.Chief || await _identityService.IsInRoleAsync (userId, Roles.Administrator)) + if (playerClan?.Role != ClanRole.Chief && await _identityService.IsInRoleAsync (userId, Roles.Administrator) == false) throw new ForbiddenAccessException (); else { diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs index 0af8ead..81ba49f 100644 --- a/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs @@ -28,7 +28,7 @@ public UpdateClanCommandValidator () RuleFor (v => v.Region).NotEmpty (); RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); - RuleFor (v => v.Type).NotEmpty (); + RuleFor (v => v.Type).IsInEnum (); } } } diff --git a/src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs b/src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs new file mode 100644 index 0000000..169f226 --- /dev/null +++ b/src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs @@ -0,0 +1,60 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Queries.GetClansWithPagination; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using MediatR; +using AutoMapper; +using FluentValidation; +using System.Data; +using DataClash.Application.Common.Exceptions; +using Microsoft.EntityFrameworkCore; + +namespace DataClash.Application.Clans.Queries.GetClanForCurrentPlayer +{ + [Authorize] + public record GetClanForCurrentPlayerQuery () : IRequest; + + public class GetClanForCurrentPlayerQueryHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + private readonly IMapper _mapper; + + public GetClanForCurrentPlayerQueryHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer, IMapper mapper) + { + _context = context; + _currentPlayer = currentPlayer; + _mapper = mapper; + } + + public async Task Handle (GetClanForCurrentPlayerQuery query, CancellationToken cancellationToken) + { + var playerIdProxy = _currentPlayer.PlayerId; + long playerId; + + if (!playerIdProxy.HasValue) + throw new ApplicationConstraintException ("User is not a player"); + else + playerId = playerIdProxy.Value; + + var playerClan = await _context.PlayerClans.Where (e => e.PlayerId == playerId).FirstOrDefaultAsync (cancellationToken); + var clan = playerClan == null ? null : await _context.Clans.FindAsync (new object[] { playerClan.ClanId }, cancellationToken); + return clan == null ? null : _mapper.Map (clan); + } + } +} diff --git a/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs b/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs index 0098919..97ed18b 100644 --- a/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs +++ b/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs @@ -23,6 +23,7 @@ namespace DataClash.Application.Clans.Queries.GetClansWithPagination { public class ClanBriefDto : IMapFrom { + public long Id { get; init; } public string? Description { get; init; } public string? Name { get; init; } public Region? Region { get; init; } diff --git a/src/Application/Common/Exceptions/ApplicationConstraintException.cs b/src/Application/Common/Exceptions/ApplicationConstraintException.cs new file mode 100644 index 0000000..a7ededd --- /dev/null +++ b/src/Application/Common/Exceptions/ApplicationConstraintException.cs @@ -0,0 +1,26 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +namespace DataClash.Application.Common.Exceptions +{ + public class ApplicationConstraintException : Exception + { + public ApplicationConstraintException () : base () { } + public ApplicationConstraintException (string message) : base (message) { } + public ApplicationConstraintException (string message, Exception innerException) : base (message, innerException) { } + } +} diff --git a/src/Framework/Persistance/Configurations/PlayerClanConfiguration.cs b/src/Framework/Persistance/Configurations/PlayerClanConfiguration.cs index d55cc19..8973a8c 100644 --- a/src/Framework/Persistance/Configurations/PlayerClanConfiguration.cs +++ b/src/Framework/Persistance/Configurations/PlayerClanConfiguration.cs @@ -26,7 +26,7 @@ public void Configure (EntityTypeBuilder builder) { builder.HasKey (e => new { e.ClanId, e.PlayerId }); builder.HasOne (e => e.Clan).WithMany ().HasForeignKey (e => e.ClanId); - builder.HasOne (e => e.Player).WithMany ().HasForeignKey (e => e.PlayerId); + builder.HasOne (e => e.Player).WithOne ().HasForeignKey (e => e.PlayerId); } } } diff --git a/src/Framework/Persistance/Migrations/20231125174439_BugFix.Designer.cs b/src/Framework/Persistance/Migrations/20231125174439_BugFix.Designer.cs new file mode 100644 index 0000000..951a2c5 --- /dev/null +++ b/src/Framework/Persistance/Migrations/20231125174439_BugFix.Designer.cs @@ -0,0 +1,902 @@ +// +using System; +using DataClash.Framework.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Framework.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20231125174439_BugFix")] + partial class BugFix + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + modelBuilder.Entity("DataClash.Domain.Entities.Card", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ElixirCost") + .HasColumnType("REAL"); + + b.Property("InitialLevel") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Picture") + .HasColumnType("TEXT"); + + b.Property("Quality") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Cards"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.CardGift", b => + { + b.Property("ClanId") + .HasColumnType("INTEGER"); + + b.Property("CardId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.HasKey("ClanId", "CardId", "PlayerId"); + + b.HasIndex("CardId", "PlayerId"); + + b.ToTable("CardGifts"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Challenge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BeginDay") + .HasColumnType("TEXT"); + + b.Property("Bounty") + .HasColumnType("INTEGER"); + + b.Property("Cost") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("MaxLooses") + .HasColumnType("INTEGER"); + + b.Property("MinLevel") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Challenges"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Clan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("TotalTrophiesToEnter") + .HasColumnType("INTEGER"); + + b.Property("TotalTrophiesWonOnWar") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Clans"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Match", b => + { + b.Property("LooserPlayerId") + .HasColumnType("INTEGER"); + + b.Property("WinnerPlayerId") + .HasColumnType("INTEGER"); + + b.Property("BeginDate") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.HasKey("LooserPlayerId", "WinnerPlayerId", "BeginDate"); + + b.HasIndex("WinnerPlayerId"); + + b.ToTable("Matches"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FavoriteCardId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("TotalCardsFound") + .HasColumnType("INTEGER"); + + b.Property("TotalThrophies") + .HasColumnType("INTEGER"); + + b.Property("TotalWins") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("FavoriteCardId"); + + b.ToTable("Players"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerCard", b => + { + b.Property("CardId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.HasKey("CardId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PlayerCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerChallenge", b => + { + b.Property("ChallengeId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("WonThrophies") + .HasColumnType("INTEGER"); + + b.HasKey("ChallengeId", "PlayerId"); + + b.HasIndex("PlayerId"); + + b.ToTable("PlayerChallenges"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerClan", b => + { + b.Property("ClanId") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("Role") + .HasColumnType("INTEGER"); + + b.HasKey("ClanId", "PlayerId"); + + b.HasIndex("PlayerId") + .IsUnique(); + + b.ToTable("PlayerClans"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.War", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BeginDay") + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Wars"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.WarClan", b => + { + b.Property("ClanId") + .HasColumnType("INTEGER"); + + b.Property("WarId") + .HasColumnType("INTEGER"); + + b.Property("WonThrophies") + .HasColumnType("INTEGER"); + + b.HasKey("ClanId", "WarId"); + + b.HasIndex("WarId"); + + b.ToTable("WarClans"); + }); + + modelBuilder.Entity("DataClash.Framework.Identity.ApplicationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("PlayerId") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.HasIndex("PlayerId"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.DeviceFlowCodes", b => + { + b.Property("UserCode") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("DeviceCode") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Expiration") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("UserCode"); + + b.HasIndex("DeviceCode") + .IsUnique(); + + b.HasIndex("Expiration"); + + b.ToTable("DeviceCodes", (string)null); + }); + + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.Key", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Algorithm") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DataProtected") + .HasColumnType("INTEGER"); + + b.Property("IsX509Certificate") + .HasColumnType("INTEGER"); + + b.Property("Use") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Use"); + + b.ToTable("Keys", (string)null); + }); + + modelBuilder.Entity("Duende.IdentityServer.EntityFramework.Entities.PersistedGrant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedTime") + .HasColumnType("TEXT"); + + b.Property("CreationTime") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasMaxLength(50000) + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Expiration") + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Key"); + + b.HasIndex("ConsumedTime"); + + b.HasIndex("Expiration"); + + b.HasIndex("SubjectId", "ClientId", "Type"); + + b.HasIndex("SubjectId", "SessionId", "Type"); + + b.ToTable("PersistedGrants", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.MagicCard", b => + { + b.HasBaseType("DataClash.Domain.Entities.Card"); + + b.Property("AreaDamage") + .HasColumnType("REAL"); + + b.Property("DamageRadius") + .HasColumnType("REAL"); + + b.Property("Duration") + .HasColumnType("TEXT"); + + b.Property("TowerDamage") + .HasColumnType("REAL"); + + b.ToTable("MagicCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.StructCard", b => + { + b.HasBaseType("DataClash.Domain.Entities.Card"); + + b.Property("AttackPaseRate") + .HasColumnType("REAL"); + + b.Property("HitPoints") + .HasColumnType("REAL"); + + b.Property("RangeDamage") + .HasColumnType("REAL"); + + b.ToTable("StructCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.TroopCard", b => + { + b.HasBaseType("DataClash.Domain.Entities.Card"); + + b.Property("AreaDamage") + .HasColumnType("REAL"); + + b.Property("HitPoints") + .HasColumnType("REAL"); + + b.Property("UnitCount") + .HasColumnType("INTEGER"); + + b.ToTable("TroopCards"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.CardGift", b => + { + b.HasOne("DataClash.Domain.Entities.Clan", "Clan") + .WithMany() + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.PlayerCard", "PlayerCard") + .WithMany() + .HasForeignKey("CardId", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("PlayerCard"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Clan", b => + { + b.OwnsOne("DataClash.Domain.ValueObjects.Region", "Region", b1 => + { + b1.Property("ClanId") + .HasColumnType("INTEGER"); + + b1.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b1.HasKey("ClanId"); + + b1.ToTable("Clans"); + + b1.WithOwner() + .HasForeignKey("ClanId"); + }); + + b.Navigation("Region"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Match", b => + { + b.HasOne("DataClash.Domain.Entities.Player", "LooserPlayer") + .WithMany() + .HasForeignKey("LooserPlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "WinnerPlayer") + .WithMany() + .HasForeignKey("WinnerPlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LooserPlayer"); + + b.Navigation("WinnerPlayer"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.Player", b => + { + b.HasOne("DataClash.Domain.Entities.Card", "FavoriteCard") + .WithMany() + .HasForeignKey("FavoriteCardId"); + + b.Navigation("FavoriteCard"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", "Card") + .WithMany() + .HasForeignKey("CardId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Card"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerChallenge", b => + { + b.HasOne("DataClash.Domain.Entities.Challenge", "Challenge") + .WithMany() + .HasForeignKey("ChallengeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Challenge"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.PlayerClan", b => + { + b.HasOne("DataClash.Domain.Entities.Clan", "Clan") + .WithMany() + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.PlayerClan", "PlayerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.WarClan", b => + { + b.HasOne("DataClash.Domain.Entities.Clan", "Clan") + .WithMany() + .HasForeignKey("ClanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Domain.Entities.War", "War") + .WithMany() + .HasForeignKey("WarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Clan"); + + b.Navigation("War"); + }); + + modelBuilder.Entity("DataClash.Framework.Identity.ApplicationUser", b => + { + b.HasOne("DataClash.Domain.Entities.Player", "Player") + .WithMany() + .HasForeignKey("PlayerId"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("DataClash.Framework.Identity.ApplicationUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.MagicCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", null) + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.MagicCard", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.StructCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", null) + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.StructCard", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("DataClash.Domain.Entities.TroopCard", b => + { + b.HasOne("DataClash.Domain.Entities.Card", null) + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.TroopCard", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Framework/Persistance/Migrations/20231125174439_BugFix.cs b/src/Framework/Persistance/Migrations/20231125174439_BugFix.cs new file mode 100644 index 0000000..81e4c64 --- /dev/null +++ b/src/Framework/Persistance/Migrations/20231125174439_BugFix.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Framework.Migrations +{ + /// + public partial class BugFix : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_PlayerClans_PlayerId", + table: "PlayerClans"); + + migrationBuilder.CreateIndex( + name: "IX_PlayerClans_PlayerId", + table: "PlayerClans", + column: "PlayerId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_PlayerClans_PlayerId", + table: "PlayerClans"); + + migrationBuilder.CreateIndex( + name: "IX_PlayerClans_PlayerId", + table: "PlayerClans", + column: "PlayerId"); + } + } +} diff --git a/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs index 3103f18..96a2af8 100644 --- a/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Framework/Persistance/Migrations/ApplicationDbContextModelSnapshot.cs @@ -228,7 +228,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("ClanId", "PlayerId"); - b.HasIndex("PlayerId"); + b.HasIndex("PlayerId") + .IsUnique(); b.ToTable("PlayerClans"); }); @@ -777,8 +778,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) .IsRequired(); b.HasOne("DataClash.Domain.Entities.Player", "Player") - .WithMany() - .HasForeignKey("PlayerId") + .WithOne() + .HasForeignKey("DataClash.Domain.Entities.PlayerClan", "PlayerId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); diff --git a/src/WebAPI/Controllers/ClanController.cs b/src/WebAPI/Controllers/ClanController.cs index 1defc3f..73578fe 100644 --- a/src/WebAPI/Controllers/ClanController.cs +++ b/src/WebAPI/Controllers/ClanController.cs @@ -17,6 +17,7 @@ using DataClash.Application.Clans.Commands.CreateClan; using DataClash.Application.Clans.Commands.DeleteClan; using DataClash.Application.Clans.Commands.UpdateClan; +using DataClash.Application.Clans.Queries.GetClanForCurrentPlayer; using DataClash.Application.Clans.Queries.GetClansWithPagination; using DataClash.Application.Common.Models; using Microsoft.AspNetCore.Authorization; @@ -33,6 +34,13 @@ public async Task>> GetWithPagination ( return await Mediator.Send (query); } + [HttpGet] + [Route ("current")] + public async Task> GetForCurrentUser ([FromQuery] GetClanForCurrentPlayerQuery query) + { + return await Mediator.Send (query); + } + [HttpPost] public async Task> Create (CreateClanCommand command) { diff --git a/tests/Application.IntegrationTests/Clan/Commands/AddPlayer.cs b/tests/Application.IntegrationTests/Clan/Commands/AddPlayer.cs new file mode 100644 index 0000000..6ea8383 --- /dev/null +++ b/tests/Application.IntegrationTests/Clan/Commands/AddPlayer.cs @@ -0,0 +1,173 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.AddPlayer; +using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Common.Exceptions; +using DataClash.Domain.Entities; +using DataClash.Domain.ValueObjects; +using DataClash.Framework.Identity; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Namotion.Reflection; +using NUnit.Framework; + +namespace DataClash.Application.IntegrationTests.Clans.Commands +{ + using static Testing; + + public class AddPlayerTests : BaseTestFixture + { + [Test] + public async Task ShouldRequireAdministrator () + { + var userId = await RunAsDefaultUserAsync (); + + var createClanCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createClanCommand); + var user = await FindAsync (userId); + var playerId = user!.PlayerId!.Value; + + var command = new AddPlayerCommand + { + ClanId = clanId, + PlayerId = playerId, + Role = Domain.Enums.ClanRole.Commoner, + }; + + await FluentActions.Invoking (() => SendAsync (command)).Should ().ThrowAsync (); + } + + [Test] + public async Task ShouldAddPlayer () + { + var userId = await RunAsDefaultUserAsync (); + + var createClanCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createClanCommand); + var user = await FindAsync (userId); + var playerId = user!.PlayerId!.Value; + + var command = new AddPlayerCommand + { + ClanId = clanId, + PlayerId = playerId, + Role = Domain.Enums.ClanRole.Commoner, + }; + + await RunAsAdministratorAsync (); + await SendAsync (command); + + var item = await FindAsync (clanId, playerId); + + item.Should ().NotBeNull (); + item.Should ().HasProperty ("ClanId"); + item.Should ().HasProperty ("PlayerId"); + + item!.ClanId.Should ().Be (command.ClanId); + item!.PlayerId.Should ().Be (command.PlayerId); + item!.Role.Should ().Be (command.Role); + } + + [Test] + public async Task ShouldNotAddPlayerTwoTimes () + { + var userId = await RunAsDefaultUserAsync (); + + var createClanCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createClanCommand); + var user = await FindAsync (userId); + var playerId = user!.PlayerId!.Value; + + var command = new AddPlayerCommand + { + ClanId = clanId, + PlayerId = playerId, + Role = Domain.Enums.ClanRole.Commoner, + }; + + await RunAsAdministratorAsync (); + await FluentActions.Invoking (() => SendAsync (command)).Should ().NotThrowAsync (); + await FluentActions.Invoking (() => SendAsync (command)).Should ().ThrowAsync (); + } + + [Test] + public async Task ShouldNotAddPlayerInTwoClans () + { + var userId = await RunAsDefaultUserAsync (); + + var createClanCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var firstClanId = await SendAsync (createClanCommand); + var secondClanId = await SendAsync (createClanCommand); + var user = await FindAsync (userId); + var playerId = user!.PlayerId!.Value; + + var firstAddPlayerCommand = new AddPlayerCommand + { + ClanId = firstClanId, + PlayerId = playerId, + Role = Domain.Enums.ClanRole.Commoner, + }; + + var secondAddPlayerCommand = new AddPlayerCommand + { + ClanId = secondClanId, + PlayerId = playerId, + Role = Domain.Enums.ClanRole.Commoner, + }; + + await RunAsAdministratorAsync (); + await FluentActions.Invoking (() => SendAsync (firstAddPlayerCommand)).Should ().NotThrowAsync (); + await FluentActions.Invoking (() => SendAsync (secondAddPlayerCommand)).Should ().ThrowAsync (); + } + } +} diff --git a/tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs b/tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs new file mode 100644 index 0000000..b5caec7 --- /dev/null +++ b/tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs @@ -0,0 +1,70 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Common.Exceptions; +using DataClash.Domain.Entities; +using DataClash.Domain.ValueObjects; +using FluentAssertions; +using Namotion.Reflection; +using NUnit.Framework; + +namespace DataClash.Application.IntegrationTests.Clans.Commands +{ + using static Testing; + + public class CreateClanTests : BaseTestFixture + { + [Test] + public async Task ShouldRequireMinimunFields () + { + await RunAsDefaultUserAsync (); + var command = new CreateClanCommand (); + + await FluentActions.Invoking (() => SendAsync (command)).Should ().ThrowAsync (); + } + + [Test] + public async Task ShouldCreateClan () + { + await RunAsDefaultUserAsync (); + var command = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var newId = await SendAsync (command); + var item = await FindAsync (newId); + + item.Should ().NotBeNull (); + item.Should ().HasProperty ("Description"); + item.Should ().HasProperty ("Name"); + item.Should ().HasProperty ("Region"); + + item!.Description.Should ().Be (command.Description); + item!.Name.Should ().Be (command.Name); + item!.Region.Should ().Be (command.Region); + item!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); + item!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); + item!.Type.Should ().Be (command.Type); + } + } +} diff --git a/tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs b/tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs new file mode 100644 index 0000000..6e698ef --- /dev/null +++ b/tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs @@ -0,0 +1,147 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.AddPlayer; +using DataClash.Application.Clans.Commands.CreateClanWithChief; +using DataClash.Application.Common.Exceptions; +using DataClash.Domain.Entities; +using DataClash.Domain.ValueObjects; +using DataClash.Framework.Identity; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Namotion.Reflection; +using NUnit.Framework; + +namespace DataClash.Application.IntegrationTests.Clans.Commands +{ + using static Testing; + + public class CreateWithChiefTest : BaseTestFixture + { + [Test] + public async Task ShouldRequireMinimunFields () + { + await RunAsDefaultUserAsync (); + var command = new CreateClanWithChiefCommand (); + + await FluentActions.Invoking (() => SendAsync (command)).Should ().ThrowAsync (); + } + + [Test] + public async Task ShouldCreateClanWithChief () + { + var userId = await RunAsDefaultUserAsync (); + var user = await FindAsync (userId); + var playerId = user!.PlayerId!.Value; + + var command = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (command); + + var clanItem = await FindAsync (clanId); + var playerClanItem = await FindAsync (clanId, playerId); + + clanItem.Should ().NotBeNull (); + clanItem.Should ().HasProperty ("Description"); + clanItem.Should ().HasProperty ("Name"); + clanItem.Should ().HasProperty ("Region"); + + clanItem!.Description.Should ().Be (command.Description); + clanItem!.Name.Should ().Be (command.Name); + clanItem!.Region.Should ().Be (command.Region); + clanItem!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); + clanItem!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); + clanItem!.Type.Should ().Be (command.Type); + + playerClanItem.Should ().NotBeNull (); + playerClanItem.Should ().HasProperty ("ClanId"); + playerClanItem.Should ().HasProperty ("PlayerId"); + + playerClanItem!.ClanId.Should ().Be (clanId); + playerClanItem!.PlayerId.Should ().Be (playerId); + } + + [Test] + public async Task ShouldAllowAccessToChief () + { + var user2Id = await RunAsUserAsync ("seconduser@local", "SecondUs3r!", Array.Empty ()); + var user2 = await FindAsync (user2Id); + var player2Id = user2!.PlayerId!.Value; + + var userId = await RunAsDefaultUserAsync (); + var user = await FindAsync (userId); + var playerId = user!.PlayerId!.Value; + + var createClanWithChiefCommand = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var addPlayerCommand = new AddPlayerCommand + { + ClanId = await SendAsync(createClanWithChiefCommand), + PlayerId = player2Id, + Role = Domain.Enums.ClanRole.Commoner, + }; + + await FluentActions.Invoking (() => SendAsync (addPlayerCommand)).Should ().NotThrowAsync (); + } + + [Test] + public async Task ShouldNotAllowTwoChiefs () + { + var user2Id = await RunAsUserAsync ("seconduser@local", "SecondUs3r!", Array.Empty ()); + var user2 = await FindAsync (user2Id); + var player2Id = user2!.PlayerId!.Value; + + var userId = await RunAsDefaultUserAsync (); + var user = await FindAsync (userId); + var playerId = user!.PlayerId!.Value; + + var createClanWithChiefCommand = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var addPlayerCommand = new AddPlayerCommand + { + ClanId = await SendAsync(createClanWithChiefCommand), + PlayerId = player2Id, + Role = Domain.Enums.ClanRole.Chief, + }; + + await FluentActions.Invoking (() => SendAsync (addPlayerCommand)).Should ().ThrowAsync (); + } + } +} diff --git a/tests/Application.IntegrationTests/Clan/Commands/DeleteClan.cs b/tests/Application.IntegrationTests/Clan/Commands/DeleteClan.cs new file mode 100644 index 0000000..7a6de3a --- /dev/null +++ b/tests/Application.IntegrationTests/Clan/Commands/DeleteClan.cs @@ -0,0 +1,112 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Clans.Commands.CreateClanWithChief; +using DataClash.Application.Clans.Commands.DeleteClan; +using DataClash.Application.Common.Exceptions; +using DataClash.Domain.Entities; +using DataClash.Domain.ValueObjects; +using FluentAssertions; +using NUnit.Framework; + +namespace DataClash.Application.IntegrationTests.Clans.Commands +{ + using static Testing; + + public class DeleteClanTests : BaseTestFixture + { + [Test] + public async Task ShouldRequireAdministratorOrChief () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var command = new DeleteClanCommand (await SendAsync (createCommand)); + await FluentActions.Invoking (() => SendAsync (command)).Should ().ThrowAsync (); + } + + [Test] + public async Task ShouldRequireAdministrator () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var command = new DeleteClanCommand (await SendAsync (createCommand)); + await RunAsAdministratorAsync (); + await FluentActions.Invoking (() => SendAsync (command)).Should ().NotThrowAsync (); + } + + [Test] + public async Task ShouldRequireClanChief () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var command = new DeleteClanCommand (await SendAsync (createCommand)); + await FluentActions.Invoking (() => SendAsync (command)).Should ().NotThrowAsync (); + } + + [Test] + public async Task ShouldDeleteClan () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createCommand); + await SendAsync (new DeleteClanCommand (clanId)); + + var item = await FindAsync (clanId); + item.Should ().BeNull (); + } + } +} diff --git a/tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs b/tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs new file mode 100644 index 0000000..15c5a37 --- /dev/null +++ b/tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs @@ -0,0 +1,175 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Clans.Commands.CreateClanWithChief; +using DataClash.Application.Clans.Commands.UpdateClan; +using DataClash.Application.Common.Exceptions; +using DataClash.Domain.Entities; +using DataClash.Domain.ValueObjects; +using FluentAssertions; +using Namotion.Reflection; +using NUnit.Framework; + +namespace DataClash.Application.IntegrationTests.Clans.Commands +{ + using static Testing; + + public class UpdateClanTests : BaseTestFixture + { + [Test] + public async Task ShouldRequireAdministratorOrChief () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createCommand); + + var command = new UpdateClanCommand + { + Id = clanId, + + Description = "Test clan 2", + Name = "Test clan 2", + Region = Region.Somewhere, + TotalTrophiesToEnter = 2, + TotalTrophiesWonOnWar = 2, + Type = Domain.Enums.ClanType.Normal, + }; + + await FluentActions.Invoking (() => SendAsync (command)).Should ().ThrowAsync (); + } + + [Test] + public async Task ShouldRequireAdministrator () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createCommand); + + var command = new UpdateClanCommand + { + Id = clanId, + + Description = "Test clan 2", + Name = "Test clan 2", + Region = Region.Somewhere, + TotalTrophiesToEnter = 2, + TotalTrophiesWonOnWar = 2, + Type = Domain.Enums.ClanType.Normal, + }; + + await RunAsAdministratorAsync (); + await FluentActions.Invoking (() => SendAsync (command)).Should ().NotThrowAsync (); + } + + [Test] + public async Task ShouldRequireClanChief () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createCommand); + + var command = new UpdateClanCommand + { + Id = clanId, + + Description = "Test clan 2", + Name = "Test clan 2", + Region = Region.Somewhere, + TotalTrophiesToEnter = 2, + TotalTrophiesWonOnWar = 2, + Type = Domain.Enums.ClanType.Normal, + }; + + await FluentActions.Invoking (() => SendAsync (command)).Should ().NotThrowAsync (); + } + + [Test] + public async Task ShouldUpdateClan () + { + await RunAsDefaultUserAsync (); + + var createCommand = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var clanId = await SendAsync (createCommand); + + var command = new UpdateClanCommand + { + Id = clanId, + + Description = "Test clan 2", + Name = "Test clan 2", + Region = Region.Somewhere, + TotalTrophiesToEnter = 2, + TotalTrophiesWonOnWar = 2, + Type = Domain.Enums.ClanType.Normal, + }; + + await SendAsync (command); + var item = await FindAsync (command.Id); + + item.Should ().NotBeNull (); + item.Should ().HasProperty ("Description"); + item.Should ().HasProperty ("Name"); + item.Should ().HasProperty ("Region"); + + item!.Description.Should ().Be (command.Description); + item!.Name.Should ().Be (command.Name); + item!.Region.Should ().Be (command.Region); + item!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); + item!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); + item!.Type.Should ().Be (command.Type); + } + } +} diff --git a/tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs b/tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs new file mode 100644 index 0000000..c1ef4f1 --- /dev/null +++ b/tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs @@ -0,0 +1,73 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Clans.Commands.CreateClanWithChief; +using DataClash.Application.Clans.Queries.GetClanForCurrentPlayer; +using DataClash.Domain.ValueObjects; +using FluentAssertions; +using NUnit.Framework; + +namespace DataClash.Application.IntegrationTests.Clans.Queries.GetClanForCurrentPlayer +{ + using static Testing; + + public class GetClanForCurrentPlayerTests : BaseTestFixture + { + [Test] + public async Task ShouldWorkWithNoPlayer () + { + await RunAsDefaultUserAsync (); + + var command = new CreateClanCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var newId = await SendAsync (command); + var dto = await SendAsync (new GetClanForCurrentPlayerQuery ()); + + dto.Should ().BeNull (); + } + + [Test] + public async Task ShouldWorkWithPlayer () + { + await RunAsDefaultUserAsync (); + + var command = new CreateClanWithChiefCommand + { + Description = "Test clan", + Name = "Test clan", + Region = Region.Somewhere, + TotalTrophiesToEnter = 0, + TotalTrophiesWonOnWar = 0, + Type = Domain.Enums.ClanType.Normal, + }; + + var newId = await SendAsync (command); + var dto = await SendAsync (new GetClanForCurrentPlayerQuery ()); + + dto.Should ().NotBeNull (); + dto!.Id.Should ().Be (newId); + } + } +} diff --git a/tests/Application.IntegrationTests/Clan/Queries/GetClansWithPagination.cs b/tests/Application.IntegrationTests/Clan/Queries/GetClansWithPagination.cs new file mode 100644 index 0000000..b36b37c --- /dev/null +++ b/tests/Application.IntegrationTests/Clan/Queries/GetClansWithPagination.cs @@ -0,0 +1,36 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Queries.GetClansWithPagination; +using DataClash.Application.Common.Exceptions; +using FluentAssertions; +using NUnit.Framework; + +namespace DataClash.Application.IntegrationTests.Clans.Queries +{ + using static Testing; + + public class GetClansWithPagination : BaseTestFixture + { + [Test] + public async Task ShouldNotRequireAdministrator () + { + await RunAsDefaultUserAsync (); + var command = new GetClansWithPaginationQuery { PageNumber = 1, PageSize = 10, }; + await FluentActions.Invoking (() => SendAsync (command)).Should ().NotThrowAsync (); + } + } +} From 1b32fec6c1c5fc6fc1f4a833c9fe7157c0671261 Mon Sep 17 00:00:00 2001 From: JuanMiguel01 Date: Sun, 26 Nov 2023 08:35:55 -0500 Subject: [PATCH 06/43] Chalenge front --- .../CreateChallenge/CreateChallengeCommand.cs | 1 + ....cs => CreateChallengeCommandValidator.cs} | 8 +- .../DeleteChallenge/DeleteChallengeCommand.cs | 2 +- .../UpdateChallenge/UpdateChallengeCommand.cs | 1 + .../GetChallengeWithPaginationQuery.cs | 1 + src/WebAPI/ClientApp/src/AppRoutes.js | 7 +- .../ClientApp/src/components/Challenges.js | 239 ++++++++++++++++++ src/WebAPI/Controllers/ChallengeController.cs | 55 ++++ ...dController.cs => PlayerCardController.cs} | 0 9 files changed, 305 insertions(+), 9 deletions(-) rename src/Application/Challenges/Commands/CreateChallenge/{ChallengeCommandValidator.cs => CreateChallengeCommandValidator.cs} (83%) create mode 100644 src/WebAPI/ClientApp/src/components/Challenges.js create mode 100644 src/WebAPI/Controllers/ChallengeController.cs rename src/WebAPI/Controllers/{CardController.cs => PlayerCardController.cs} (100%) diff --git a/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs b/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs index 6a58f7d..1ee6df3 100644 --- a/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs +++ b/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs @@ -33,6 +33,7 @@ public record CreateChallengeCommand : IRequest public long MaxLooses { get; init; } public long MinLevel { get; init; } public string? Name { get; init; } + } public class CreateChallengeCommandHandler : IRequestHandler diff --git a/src/Application/Challenges/Commands/CreateChallenge/ChallengeCommandValidator.cs b/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommandValidator.cs similarity index 83% rename from src/Application/Challenges/Commands/CreateChallenge/ChallengeCommandValidator.cs rename to src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommandValidator.cs index 101818d..ad880a2 100644 --- a/src/Application/Challenges/Commands/CreateChallenge/ChallengeCommandValidator.cs +++ b/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommandValidator.cs @@ -24,12 +24,12 @@ public CreateChallengeCommandValidator () { RuleFor (v => v.BeginDay).NotEmpty (); - RuleFor (v => v.Bounty).NotEmpty (); - RuleFor (v => v.Cost).NotEmpty (); + RuleFor (v => v.Bounty).GreaterThanOrEqualTo(0); + RuleFor (v => v.Cost).GreaterThanOrEqualTo(0); RuleFor (v => v.Description).NotEmpty (); RuleFor (v => v.Duration).NotEmpty (); - RuleFor (v => v.MaxLooses).NotEmpty (); - RuleFor (v => v.MinLevel).NotEmpty (); + RuleFor (v => v.MaxLooses).GreaterThanOrEqualTo(0); + RuleFor (v => v.MinLevel).GreaterThanOrEqualTo(0); RuleFor (v => v.Name).NotEmpty (); diff --git a/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs b/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs index 4531339..47563d3 100644 --- a/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs +++ b/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs @@ -24,9 +24,9 @@ namespace DataClash.Application.Challenges.Commands.DeleteChallenge { - [Authorize (Roles = "Administrator")] public record DeleteChallengeCommand (long Id) : IRequest; + [Authorize (Roles = "Administrator")] public class DeleteChallengeCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; diff --git a/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs b/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs index 655dc93..4adcc83 100644 --- a/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs +++ b/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs @@ -60,6 +60,7 @@ public async Task Handle (UpdateChallengeCommand request, CancellationToken canc entity.MinLevel= request.MinLevel; entity.Name= request.Name; + entity.AddDomainEvent (new ChallengeUpdatedEvent (entity)); await _context.SaveChangesAsync (cancellationToken); } diff --git a/src/Application/Challenges/Queries/GetChallengesWithPagination/GetChallengeWithPaginationQuery.cs b/src/Application/Challenges/Queries/GetChallengesWithPagination/GetChallengeWithPaginationQuery.cs index ba368d5..af47edd 100644 --- a/src/Application/Challenges/Queries/GetChallengesWithPagination/GetChallengeWithPaginationQuery.cs +++ b/src/Application/Challenges/Queries/GetChallengesWithPagination/GetChallengeWithPaginationQuery.cs @@ -19,6 +19,7 @@ using DataClash.Application.Common.Interfaces; using DataClash.Application.Common.Mappings; using DataClash.Application.Common.Models; + using MediatR; namespace DataClash.Application.Challenges.Queries.GetChallengesWithPagination diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index c902aff..b9f15f7 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -20,11 +20,11 @@ import { Login } from './components/Login' import { LoginActions } from './services/AuthorizeConstants' import { Logout } from './components/Logout' import { LogoutActions } from './services/AuthorizeConstants' -import { Players } from './components/Players' -import { Profile } from './components/Profile' import { RequireAuth } from './components/RequireAuth' import { Route, Routes } from 'react-router-dom' +import { Players } from './components/Players' import { Wars } from './components/Wars' +import { Challenges } from './components/Challenges' const loginAction = (name) => () const logoutAction = (name) => () @@ -34,11 +34,10 @@ const AppRoutes = () => ( } index={true} />

Cards component placeholdes

}/> -

Challenges component placeholdes

}/> + }/>

Clans component placeholdes

}/>

Matches component placeholdes

}/> }/> - }/> }/> diff --git a/src/WebAPI/ClientApp/src/components/Challenges.js b/src/WebAPI/ClientApp/src/components/Challenges.js new file mode 100644 index 0000000..de3a744 --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Challenges.js @@ -0,0 +1,239 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +import { ApplicationPaths } from '../services/AuthorizeConstants' +import { Button, Table } from 'reactstrap' +import { CreateChallengeCommand } from '../webApiClient.ts' +import { DateTime } from './DateTime' +import { Navigate, useParams } from 'react-router-dom' +import { Pager } from './Pager' +import { TimeSpan } from './TimeSpan' +import { UpdateChallengeCommand } from '../webApiClient.ts' +import { useAuthorize } from '../services/AuthorizeProvider' +import { UserRoles } from '../services/AuthorizeConstants' +import { ChallengeClient } from '../webApiClient.ts' +import React, { useEffect, useState } from 'react' + +export function Challenges () +{ + const { initialPage } = useParams () + const { isAuthorized, inRole }= useAuthorize () + const [ activePage, setActivePage ] = useState (initialPage ? initialPage : 0) + const [ hasNextPage, setHasNextPage ] = useState (false) + const [ hasPreviousPage, setHasPreviousPage ] = useState (false) + const [ isLoading, setIsLoading ] = useState (false) + const [ items, setItems ] = useState (undefined) + const [ totalPages, setTotalPages ] = useState (0) + const [ challengeClient ] = useState (new ChallengeClient ()) + + const pageSize = 10 + const visibleIndices = 5 + + const addChallenege = async () => + { + const data = new CreateChallengeCommand () + data.beginDay = new Date () + data.duration = "00:00:01" + data.bounty=0 + data.cost=0 + data.description="" + data.maxLooses=0 + data.minLevel=0 + data.name="" + + await challengeClient.create (data) + setActivePage (-1) + } + + const removeChallenge = async (item) => + { + await challengeClient.delete (item.id) + setActivePage (-1) + } + + const updateChallenge = async (item) => + { + const data = new UpdateChallengeCommand () + data.id = item.id + data.beginDay = item.beginDay + data.bounty=item.bounty + data.duration = item.duration + data.cost=item.cost + data.description=item.description + data.maxLooses=item.maxLooses + data.minLevel=item.minLevel + data.name=item.name + + await challengeClient.update (item.id, data) + } + + useEffect (() => + { + const lastPage = async () => + { + const paginatedList = await challengeClient.getWithPagination (1, pageSize) + return paginatedList.totalPages + } + + const refreshPage = async () => + { + const paginatedList = await challengeClient.getWithPagination (activePage + 1, pageSize) + + setHasNextPage (paginatedList.hasNextPage) + setHasPreviousPage (paginatedList.hasPreviousPage) + setItems (paginatedList.items) + setTotalPages (paginatedList.totalPages) + } + + if (activePage >= 0) + { + setIsLoading (true) + refreshPage ().then (() => setIsLoading (false)) + } + else + { + lastPage ().then ((total) => + { + if (total === 0) + setActivePage (0) + else + setActivePage (total - 1) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activePage]) + + return ( + isLoading + ? (
) + : ( + !isAuthorized + ? () + : ( + <> +
+ setActivePage (index)} + totalPages={totalPages} + visibleIndices={visibleIndices} /> +
+
+ + + + + + + + + + + + + + + + + { (items ?? []).map ((item, index) => ( + + + + + + + + + + + + { + (!inRole[UserRoles.Administrator]) + ? () + } + )) + } + + + { + (!inRole[UserRoles.Administrator]) + ? () + : ( + + + ) + } + +
{'#'}{'Begin day'}{'Duration'}{'Bounty'}{'Cost'}{'Description'}{'MaxLooses'}{'MinLevel'}{'Name'} +
{ item.id } + { item.beginDay = date; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + + { item.duration = span; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + + { item.bounty = number; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + + { item.cost = number; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + + { item.description = string; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + + { item.maxLooses = number; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + + { item.minLevel = number; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + + { item.name = string; updateChallenge (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> + ) + : ( + +
+ + +
+
+ ))) +} diff --git a/src/WebAPI/Controllers/ChallengeController.cs b/src/WebAPI/Controllers/ChallengeController.cs new file mode 100644 index 0000000..137ab5f --- /dev/null +++ b/src/WebAPI/Controllers/ChallengeController.cs @@ -0,0 +1,55 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Models; +using DataClash.Application.Challenges.Commands.CreateChallenge; +using DataClash.Application.Challenges.Commands.DeleteChallenge; +using DataClash.Application.Challenges.Commands.UpdateChallenge; +using DataClash.Application.Challenges.Queries.GetChallengesWithPagination; +using DataClash.Domain.Entities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DataClash.WebUI.Controllers{ + public class ChallengeController : ApiControllerBase + { + [HttpGet] + public async Task>> GetWithPagination ([FromQuery] GetChallengesWithPaginationQuery query){ + return await Mediator.Send (query); + } + [HttpPost] + public async Task> Create (CreateChallengeCommand command){ + return await Mediator.Send (command); + } + [HttpDelete ("{id}")] + [ProducesResponseType (StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public async Task Delete (long id){ + await Mediator.Send (new DeleteChallengeCommand (id)); + return NoContent (); + } + [HttpPut("{id}")] + [ProducesResponseType (StatusCodes.Status204NoContent)] + [ProducesResponseType (StatusCodes.Status400BadRequest)] + [ProducesDefaultResponseType] + public async Task Update (long id, UpdateChallengeCommand command){ + if (id != command.Id) + return BadRequest (); + await Mediator.Send (command); + return NoContent (); + } + } +} \ No newline at end of file diff --git a/src/WebAPI/Controllers/CardController.cs b/src/WebAPI/Controllers/PlayerCardController.cs similarity index 100% rename from src/WebAPI/Controllers/CardController.cs rename to src/WebAPI/Controllers/PlayerCardController.cs From f7addde0f8cc69e151122344708fa1394f020005 Mon Sep 17 00:00:00 2001 From: JuanMiguel01 Date: Sun, 26 Nov 2023 08:45:22 -0500 Subject: [PATCH 07/43] Arreglado pasa test --- .../Commands/DeleteChallenge/DeleteChallengeCommand.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs b/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs index 47563d3..95a4afc 100644 --- a/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs +++ b/src/Application/Challenges/Commands/DeleteChallenge/DeleteChallengeCommand.cs @@ -24,9 +24,10 @@ namespace DataClash.Application.Challenges.Commands.DeleteChallenge { + [Authorize (Roles = "Administrator")] public record DeleteChallengeCommand (long Id) : IRequest; - [Authorize (Roles = "Administrator")] + public class DeleteChallengeCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; From 54f2f55381345248187e9693bee6f5fbe4a49930 Mon Sep 17 00:00:00 2001 From: JuanMiguel01 Date: Mon, 27 Nov 2023 11:20:55 -0500 Subject: [PATCH 08/43] Combios de playerChallenge --- .../Commands/AddPlayer/AddPlayerCommand.cs | 69 +++++++++++++++++++ .../AddPlayer/AddPlayerCommandValidator.cs | 30 ++++++++ .../RemovePlayerCommandHandler.cs | 68 ++++++++++++++++++ .../RemovePlayerCommandValidator.cs | 30 ++++++++ .../Interfaces/ICurrentPlayerService.cs | 24 +++++++ src/Domain/Events/PlayerAddedEvent.cs | 31 +++++++++ src/Domain/Events/PlayerRemovedEvent.cs | 31 +++++++++ .../Services/CurrentPlayerService.cs | 49 +++++++++++++ src/WebAPI/ClientApp/src/AppRoutes.js | 3 + .../src/components/Profile/Profile.js | 5 +- .../src/components/Profile/ProfileChalenge.js | 27 ++++++++ src/WebAPI/Pages/Error.cshtml | 26 ------- 12 files changed, 366 insertions(+), 27 deletions(-) create mode 100644 src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs create mode 100644 src/Application/Challenges/Commands/AddPlayer/AddPlayerCommandValidator.cs create mode 100644 src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs create mode 100644 src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandValidator.cs create mode 100644 src/Application/Common/Interfaces/ICurrentPlayerService.cs create mode 100644 src/Domain/Events/PlayerAddedEvent.cs create mode 100644 src/Domain/Events/PlayerRemovedEvent.cs create mode 100644 src/Framework/Services/CurrentPlayerService.cs create mode 100644 src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js delete mode 100644 src/WebAPI/Pages/Error.cshtml diff --git a/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs b/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs new file mode 100644 index 0000000..715ce12 --- /dev/null +++ b/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs @@ -0,0 +1,69 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.Events; +using MediatR; + +namespace DataClash.Application.Challenges.Commands.AddPlayer +{ + [Authorize] + public record AddPlayerCommand : IRequest + { + public long ChallengeId { get; init; } + public long PlayerId { get; init; } + public long WonThrophies { get; init; } + + } + + public class AddPlayerCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + private readonly ICurrentUserService _currentUser; + private readonly IIdentityService _identityService; + + public AddPlayerCommandHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer, ICurrentUserService currentUser, IIdentityService identityService) + { + _context = context; + _currentPlayer = currentPlayer; + _currentUser = currentUser; + _identityService = identityService; + } + + public async Task Handle (AddPlayerCommand request, CancellationToken cancellationToken) + { + var userId = _currentUser.UserId!; + var challenge = await _context.Challenges.FindAsync (new object[] { request.ChallengeId }, cancellationToken) ?? throw new NotFoundException (nameof (Challenge), request.ChallengeId); + var playerId = _currentPlayer.PlayerId!; + var playerChallenge = await _context.PlayerChallenges.FindAsync (new object[] { request.ChallengeId, playerId }, cancellationToken); + + + + var entity = new PlayerChallenge { ChallengeId = request.ChallengeId, PlayerId = request.PlayerId, WonThrophies=request.WonThrophies, }; + + challenge.AddDomainEvent (new PlayerAddedEvent (entity)); + _context.PlayerChallenges.Add (entity); + + await _context.SaveChangesAsync (cancellationToken); + + } + } +} diff --git a/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommandValidator.cs b/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommandValidator.cs new file mode 100644 index 0000000..39052d0 --- /dev/null +++ b/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommandValidator.cs @@ -0,0 +1,30 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Challenges.Commands.AddPlayer +{ + public class AddPlayerCommandValidator : AbstractValidator + { + public AddPlayerCommandValidator () + { + RuleFor (v => v.ChallengeId).NotEmpty (); + RuleFor (v => v.PlayerId).NotEmpty (); + RuleFor (v => v.WonThrophies).NotEmpty (); + } + } +} diff --git a/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs b/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs new file mode 100644 index 0000000..e4be98f --- /dev/null +++ b/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs @@ -0,0 +1,68 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; +using DataClash.Domain.Events; +using MediatR; + +namespace DataClash.Application.Challenges.Commands.RemovePlayer +{ + [Authorize] + public record RemovePlayerCommand : IRequest + { + public long ChallengeId { get; init; } + public long PlayerId { get; init; } + public long WonThrophies { get; init; } + + } + + public class RemovePlayerCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + private readonly ICurrentPlayerService _currentPlayer; + private readonly ICurrentUserService _currentUser; + private readonly IIdentityService _identityService; + + public RemovePlayerCommandHandler (IApplicationDbContext context, ICurrentPlayerService currentPlayer, ICurrentUserService currentUser, IIdentityService identityService) + { + _context = context; + _currentPlayer = currentPlayer; + _currentUser = currentUser; + _identityService = identityService; + } + + public async Task Handle (RemovePlayerCommand request, CancellationToken cancellationToken) + { + var userId = _currentUser.UserId!; + var challenge = await _context.Challenges.FindAsync (new object[] { request.ChallengeId }, cancellationToken) ?? throw new NotFoundException (nameof (Challenge), request.ChallengeId); + var playerChallenge= await _context.PlayerChallenges.FindAsync (new object[] { request.ChallengeId, request.PlayerId }, cancellationToken) ?? throw new NotFoundException (nameof (PlayerChallenge), new object[] { request.ChallengeId, request.PlayerId }); + + + var entity = await _context.PlayerChallenges.FindAsync (new object[] { request.ChallengeId, request.PlayerId }, cancellationToken) + ?? throw new NotFoundException (nameof (PlayerChallenge), new object[] { request.ChallengeId, request.PlayerId }); + + _context.PlayerChallenges.Remove (entity); + challenge.AddDomainEvent (new PlayerRemovedEvent (entity)); + + await _context.SaveChangesAsync (cancellationToken); + + } + } +} diff --git a/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandValidator.cs b/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandValidator.cs new file mode 100644 index 0000000..15ce962 --- /dev/null +++ b/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandValidator.cs @@ -0,0 +1,30 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Challenges.Commands.RemovePlayer +{ + public class RemovePlayerCommandValidator : AbstractValidator + { + public RemovePlayerCommandValidator () + { + RuleFor (v => v.ChallengeId).NotEmpty (); + RuleFor (v => v.PlayerId).NotEmpty (); + RuleFor(v=>v.WonThrophies).NotEmpty(); + } + } +} diff --git a/src/Application/Common/Interfaces/ICurrentPlayerService.cs b/src/Application/Common/Interfaces/ICurrentPlayerService.cs new file mode 100644 index 0000000..0465142 --- /dev/null +++ b/src/Application/Common/Interfaces/ICurrentPlayerService.cs @@ -0,0 +1,24 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +namespace DataClash.Application.Common.Interfaces +{ + public interface ICurrentPlayerService + { + public long? PlayerId { get; } + } +} diff --git a/src/Domain/Events/PlayerAddedEvent.cs b/src/Domain/Events/PlayerAddedEvent.cs new file mode 100644 index 0000000..ea617bf --- /dev/null +++ b/src/Domain/Events/PlayerAddedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class PlayerAddedEvent : BaseEvent + { + public PlayerChallenge Item { get; } + + public PlayerAddedEvent (PlayerChallenge item) + { + Item = item; + } + } +} diff --git a/src/Domain/Events/PlayerRemovedEvent.cs b/src/Domain/Events/PlayerRemovedEvent.cs new file mode 100644 index 0000000..2713fc1 --- /dev/null +++ b/src/Domain/Events/PlayerRemovedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class PlayerRemovedEvent : BaseEvent + { + public PlayerChallenge Item { get; } + + public PlayerRemovedEvent (PlayerChallenge item) + { + Item = item; + } + } +} diff --git a/src/Framework/Services/CurrentPlayerService.cs b/src/Framework/Services/CurrentPlayerService.cs new file mode 100644 index 0000000..ed37bcc --- /dev/null +++ b/src/Framework/Services/CurrentPlayerService.cs @@ -0,0 +1,49 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +using DataClash.Application.Common.Interfaces; +using DataClash.Framework.Persistence; + +namespace DataClash.Framework.Services +{ + public class CurrentPlayerService : ICurrentPlayerService + { + private readonly ApplicationDbContext _context; + private readonly ICurrentUserService _currentUser; + + public long? PlayerId + { + get + { + var userId = _currentUser.UserId; + if (userId == null) + return null; + else + { + var user = _context.Users.Find (new object[] { userId }); + return user?.PlayerId; + } + } + } + + public CurrentPlayerService (ApplicationDbContext context, ICurrentUserService currentUser) + { + _context = context; + _currentUser = currentUser; + } + } +} diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index b9f15f7..ce600c7 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -17,6 +17,7 @@ import { ApplicationPaths } from './services/AuthorizeConstants' import { Home } from './components/Home' import { Login } from './components/Login' +import { Profile } from './components/Profile' import { LoginActions } from './services/AuthorizeConstants' import { Logout } from './components/Logout' import { LogoutActions } from './services/AuthorizeConstants' @@ -26,6 +27,7 @@ import { Players } from './components/Players' import { Wars } from './components/Wars' import { Challenges } from './components/Challenges' + const loginAction = (name) => () const logoutAction = (name) => () @@ -38,6 +40,7 @@ const AppRoutes = () => (

Clans component placeholdes

}/>

Matches component placeholdes

}/> }/> + }/> }/> diff --git a/src/WebAPI/ClientApp/src/components/Profile/Profile.js b/src/WebAPI/ClientApp/src/components/Profile/Profile.js index ac7677d..19d4907 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/Profile.js +++ b/src/WebAPI/ClientApp/src/components/Profile/Profile.js @@ -19,6 +19,7 @@ import { Col, Container, Row } from 'reactstrap' import { Nav, NavItem, NavLink } from 'reactstrap' import { ProfileIdentity } from './ProfileIdentity' import { ProfilePlayer } from './ProfilePlayer' +import { ProfileChallenge } from './ProfileChalenge' import { useAuthorize } from '../../services/AuthorizeProvider' import { WaitSpinner } from '../WaitSpinner' import React, { useState } from 'react' @@ -29,9 +30,11 @@ export function Profile () const [ activeIndex, setActiveIndex ] = useState (0) const pages = - [ + [ + {title:'Chalenges', component: }, { title: 'Identity', component: }, { title: 'Player', component: }, + ] return ( diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js new file mode 100644 index 0000000..7a90031 --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js @@ -0,0 +1,27 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +import { Button, Form, FormGroup, Input, Label } from 'reactstrap' +import { ChallengeClient,UpdateChallengeCommand } from '../../webApiClient.ts' +import { ProfilePage } from './ProfilePage' +import { useErrorReporter } from '../ErrorReporter' +import { WaitSpinner } from '../WaitSpinner' +import { ChallengeClient } from '../../webApiClient.ts' +import React, { useEffect, useState } from 'react' + +/*necesito mostrar los chalenge existentes y que el usuario elija los que participa */ + +/*obtener todos los chalenge que estan en chalenge client y mostrarlos ponerlos como un boton que cuando seleccionas se añaden a los chalenge de los usuarios */ diff --git a/src/WebAPI/Pages/Error.cshtml b/src/WebAPI/Pages/Error.cshtml deleted file mode 100644 index 09da0d2..0000000 --- a/src/WebAPI/Pages/Error.cshtml +++ /dev/null @@ -1,26 +0,0 @@ -@page -@model ErrorModel -@{ - ViewData["Title"] = "Error"; -} - -

Error.

-

An error occurred while processing your request.

- -@if (Model.ShowRequestId) -{ -

- Request ID: @Model.RequestId -

-} - -

Development Mode

-

- Swapping to the Development environment displays detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

From 718af076d83316d7e71a63d5ac86e923f069955d Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Mon, 27 Nov 2023 15:50:32 -0500 Subject: [PATCH 09/43] ProfileClan component completed --- .../Command/CreateClan/CreateClanCommand.cs | 4 +- .../CreateClan/CreateClanCommandValidator.cs | 12 +- .../CreateClan/CreateClanWithChiefCommand.cs | 3 +- .../CreateClanWithChiefCommandValidator.cs | 12 +- .../Command/UpdateClan/UpdateClanCommand.cs | 4 +- .../UpdateClan/UpdateClanCommandValidator.cs | 13 +- .../GetClansWithPagination/ClanBriefDto.cs | 3 +- .../src/components/Profile/ProfileClan.js | 153 +++++++++++++++++- src/WebAPI/Controllers/ClanController.cs | 9 +- .../Clan/Commands/CreateClan.cs | 2 +- .../Clan/Commands/CreateClanWithChief.cs | 81 +++++----- .../Clan/Commands/UpdateClan.cs | 2 +- 12 files changed, 238 insertions(+), 60 deletions(-) diff --git a/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs b/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs index bfe3bc1..a3e0c67 100644 --- a/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs +++ b/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs @@ -29,7 +29,7 @@ public record CreateClanCommand : IRequest { public string? Description { get; init; } public string? Name { get; init; } - public Region? Region { get; init; } + public string? Region { get; init; } public long TotalTrophiesToEnter { get; init; } public long TotalTrophiesWonOnWar { get; init; } public ClanType Type { get; init; } @@ -50,7 +50,7 @@ public async Task Handle (CreateClanCommand request, CancellationToken can { Description = request.Description, Name = request.Name, - Region = request.Region, + Region = Region.From (request.Region!), TotalTrophiesToEnter = request.TotalTrophiesToEnter, TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar, Type = request.Type, diff --git a/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs b/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs index 24d3ca6..12960f5 100644 --- a/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs +++ b/src/Application/Clan/Command/CreateClan/CreateClanCommandValidator.cs @@ -14,17 +14,27 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Domain.Exceptions; +using DataClash.Domain.ValueObjects; using FluentValidation; namespace DataClash.Application.Clans.Commands.CreateClan { public class CreateClanCommandValidator : AbstractValidator { + private static Region? RegionTryFrom (string code) + { + try { return Region.From (code); } + catch (UnknownRegionException) {} + return null; + } + public CreateClanCommandValidator () { RuleFor (v => v.Description).NotEmpty ().MaximumLength (256); RuleFor (v => v.Name).NotEmpty ().MaximumLength (128); - RuleFor (v => v.Region).NotEmpty (); + RuleFor (v => v.Region).NotEmpty ().NotNull () + .Must (p => null != RegionTryFrom (p!)); RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); RuleFor (v => v.Type).IsInEnum (); diff --git a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs index 5ef48cb..ca38789 100644 --- a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs +++ b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs @@ -21,6 +21,7 @@ using DataClash.Domain.Entities; using DataClash.Domain.Enums; using DataClash.Domain.Events; +using DataClash.Domain.ValueObjects; using MediatR; namespace DataClash.Application.Clans.Commands.CreateClanWithChief @@ -53,7 +54,7 @@ public async Task Handle (CreateClanWithChiefCommand request, Cancellation { Description = request.Description, Name = request.Name, - Region = request.Region, + Region = (Region) request.Region!, TotalTrophiesToEnter = request.TotalTrophiesToEnter, TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar, Type = request.Type, diff --git a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs index 557958a..37f08b9 100644 --- a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs +++ b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommandValidator.cs @@ -14,17 +14,27 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Domain.Exceptions; +using DataClash.Domain.ValueObjects; using FluentValidation; namespace DataClash.Application.Clans.Commands.CreateClanWithChief { public class CreateClanWithChiefCommandValidator : AbstractValidator { + private static Region? RegionTryFrom (string code) + { + try { return Region.From (code); } + catch (UnknownRegionException) {} + return null; + } + public CreateClanWithChiefCommandValidator () { RuleFor (v => v.Description).NotEmpty ().MaximumLength (256); RuleFor (v => v.Name).NotEmpty ().MaximumLength (128); - RuleFor (v => v.Region).NotEmpty (); + RuleFor (v => v.Region).NotEmpty ().NotNull () + .Must (p => null != RegionTryFrom (p!)); RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); RuleFor (v => v.Type).IsInEnum (); diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs index 818e402..5db49b0 100644 --- a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs @@ -31,7 +31,7 @@ public record UpdateClanCommand : IRequest public long Id { get; init; } public string? Description { get; init; } public string? Name { get; init; } - public Region? Region { get; init; } + public string? Region { get; init; } public long TotalTrophiesToEnter { get; init; } public long TotalTrophiesWonOnWar { get; init; } public ClanType Type { get; init; } @@ -65,7 +65,7 @@ public async Task Handle (UpdateClanCommand request, CancellationToken cancellat { entity.Description = request.Description; entity.Name = request.Name; - entity.Region = request.Region; + entity.Region = (Region) request.Region!; entity.TotalTrophiesToEnter = request.TotalTrophiesToEnter; entity.TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar; entity.Type = request.Type; diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs index 81ba49f..5010032 100644 --- a/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommandValidator.cs @@ -14,18 +14,27 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Domain.Exceptions; +using DataClash.Domain.ValueObjects; using FluentValidation; namespace DataClash.Application.Clans.Commands.UpdateClan { public class UpdateClanCommandValidator : AbstractValidator { + private static Region? RegionTryFrom (string code) + { + try { return Region.From (code); } + catch (UnknownRegionException) {} + return null; + } + public UpdateClanCommandValidator () { - RuleFor (v => v.Id).NotEmpty (); RuleFor (v => v.Description).NotEmpty ().MaximumLength (256); RuleFor (v => v.Name).NotEmpty ().MaximumLength (128); - RuleFor (v => v.Region).NotEmpty (); + RuleFor (v => v.Region).NotEmpty ().NotNull () + .Must (p => null != RegionTryFrom (p!)); RuleFor (v => v.TotalTrophiesToEnter).GreaterThanOrEqualTo (0); RuleFor (v => v.TotalTrophiesWonOnWar).GreaterThanOrEqualTo (0); RuleFor (v => v.Type).IsInEnum (); diff --git a/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs b/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs index 97ed18b..2e1d453 100644 --- a/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs +++ b/src/Application/Clan/Queries/GetClansWithPagination/ClanBriefDto.cs @@ -17,7 +17,6 @@ using DataClash.Application.Common.Mappings; using DataClash.Domain.Entities; using DataClash.Domain.Enums; -using DataClash.Domain.ValueObjects; namespace DataClash.Application.Clans.Queries.GetClansWithPagination { @@ -26,7 +25,7 @@ public class ClanBriefDto : IMapFrom public long Id { get; init; } public string? Description { get; init; } public string? Name { get; init; } - public Region? Region { get; init; } + public string? Region { get; init; } public long TotalTrophiesToEnter { get; init; } public long TotalTrophiesWonOnWar { get; init; } public ClanType Type { get; init; } diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js index 81540e7..685fb7e 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js @@ -14,15 +14,110 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ -import { Alert } from 'reactstrap' -import { WaitSpinner } from '../WaitSpinner' -import React, { useState } from 'react' +import { Alert, Button, Form, FormGroup, Input, Label } from 'reactstrap' +import { ClanClient, ClanType } from '../../webApiClient.ts' +import { CreateClanWithChiefCommand } from '../../webApiClient.ts' import { ProfilePage } from './ProfilePage' +import { UpdateClanCommand } from '../../webApiClient.ts' +import { useErrorReporter } from '../ErrorReporter' +import { WaitSpinner } from '../WaitSpinner' +import React, { useEffect, useState } from 'react' export function ProfileClan (props) { const { playerProfile } = props + const [ clanClient ] = useState (new ClanClient ()) + const [ clanDescription, setClanDescription ] = useState () + const [ clanId, setClanId ] = useState () + const [ clanName, setClanName ] = useState () + const [ clanRegion, setClanRegion ] = useState () + const [ clanTotalTrophiesToEnter, setClanTotalTrophiesToEnter ] = useState () + const [ clanTotalTrophiesWonOnWar, setClanTotalTrophiesWonOnWar ] = useState () + const [ clanType, setClanType ] = useState () + const [ hasClan, setHasClan ] = useState (false) const [ isLoading, setIsLoading ] = useState (false) + const errorReporter = useErrorReporter () + + const refreshClan = async () => + { + if (!!playerProfile) try + { + const clan = await clanClient.getForCurrentPlayer () + + if (clan === null) + setHasClan (false) + else + { + setHasClan (true) + setClanId (clan.id) + + setClanDescription (clan.description) + setClanName (clan.name) + setClanRegion (clan.region) + setClanTotalTrophiesToEnter (clan.totalTrophiesToEnter) + setClanTotalTrophiesWonOnWar (clan.totalTrophiesWonOnWar) + setClanType (clan.type) + } + } + catch (error) { errorReporter (error) } + } + + const createClan = async () => + { + if (!!playerProfile) try + { + const command = new CreateClanWithChiefCommand () + + command.description = 'My clan' + command.name = `${playerProfile.nick ?? playerProfile.name}'s clan` + command.region = 'Somewhere' + command.totalTrophiesToEnter = 0 + command.totalTrophiesWonOnWar = 0 + command.type = ClanType.Normal + + await clanClient.createWithChief (command) + await refreshClan () + } + catch (error) { errorReporter (error) } + } + + const deleteClan = async () => + { + if (hasClan) try + { + await clanClient.delete (clanId) + await refreshClan () + } + catch (error) { errorReporter (error) } + } + + const onSubmit = async (e) => + { + if (hasClan) try + { + const command = new UpdateClanCommand () + + command.description = clanDescription + command.id = clanId + command.name = clanName + command.region = clanRegion + command.totalTrophiesToEnter = clanTotalTrophiesToEnter + command.totalTrophiesWonOnWar = clanTotalTrophiesWonOnWar + command.type = clanType + + await clanClient.update (clanId, command) + } + catch (error) { errorReporter (error) } + } + + useEffect (() => + { + setIsLoading (true) + refreshClan ().then (() => setIsLoading (false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [playerProfile]) + + const clanTypes = Object.keys (ClanType).filter (k => !isNaN (Number (ClanType[k]))) if (!playerProfile) return (User has not player status) @@ -31,6 +126,54 @@ export function ProfileClan (props) isLoading ? : -

Player clan placeholder

-
) + { !hasClan + ? <> + You do not belong to any clan +
+ +
+ + : (
{ e.preventDefault (); setIsLoading (true); onSubmit ().then (() => setIsLoading (false)) }}> + + setClanDescription (e.target.value)} /> + + + + setClanName (e.target.value)} /> + + + + setClanRegion (e.target.value)} /> + + + + setClanTotalTrophiesToEnter (e.target.value)} /> + + + + setClanTotalTrophiesWonOnWar (e.target.value)} /> + + + + k === ClanType[clanType])} + onChange={(e) => setClanType (ClanType[e.target.value])}> + { clanTypes.map ((type) => !isNaN (Number (type)) ? <> : ) } + + + + +
+ + + +
+
+
)} + ) } diff --git a/src/WebAPI/Controllers/ClanController.cs b/src/WebAPI/Controllers/ClanController.cs index 73578fe..7794bdf 100644 --- a/src/WebAPI/Controllers/ClanController.cs +++ b/src/WebAPI/Controllers/ClanController.cs @@ -15,6 +15,7 @@ * along with sep3cs. If not, see . */ using DataClash.Application.Clans.Commands.CreateClan; +using DataClash.Application.Clans.Commands.CreateClanWithChief; using DataClash.Application.Clans.Commands.DeleteClan; using DataClash.Application.Clans.Commands.UpdateClan; using DataClash.Application.Clans.Queries.GetClanForCurrentPlayer; @@ -36,7 +37,7 @@ public async Task>> GetWithPagination ( [HttpGet] [Route ("current")] - public async Task> GetForCurrentUser ([FromQuery] GetClanForCurrentPlayerQuery query) + public async Task> GetForCurrentPlayer ([FromQuery] GetClanForCurrentPlayerQuery query) { return await Mediator.Send (query); } @@ -47,6 +48,12 @@ public async Task> Create (CreateClanCommand command) return await Mediator.Send (command); } + [HttpPost ("withchief")] + public async Task> CreateWithChief (CreateClanWithChiefCommand command) + { + return await Mediator.Send (command); + } + [HttpDelete ("{id}")] [ProducesResponseType (StatusCodes.Status204NoContent)] [ProducesDefaultResponseType] diff --git a/tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs b/tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs index b5caec7..22a6e9c 100644 --- a/tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs +++ b/tests/Application.IntegrationTests/Clan/Commands/CreateClan.cs @@ -61,7 +61,7 @@ public async Task ShouldCreateClan () item!.Description.Should ().Be (command.Description); item!.Name.Should ().Be (command.Name); - item!.Region.Should ().Be (command.Region); + item!.Region.Should ().Be ((Region) command.Region); item!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); item!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); item!.Type.Should ().Be (command.Type); diff --git a/tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs b/tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs index 6e698ef..1ce585d 100644 --- a/tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs +++ b/tests/Application.IntegrationTests/Clan/Commands/CreateClanWithChief.cs @@ -21,7 +21,6 @@ using DataClash.Domain.ValueObjects; using DataClash.Framework.Identity; using FluentAssertions; -using Microsoft.EntityFrameworkCore; using Namotion.Reflection; using NUnit.Framework; @@ -41,13 +40,17 @@ public async Task ShouldRequireMinimunFields () } [Test] - public async Task ShouldCreateClanWithChief () + public async Task ShouldAllowAccessToChief () { + var user2Id = await RunAsUserAsync ("seconduser@local", "SecondUs3r!", Array.Empty ()); + var user2 = await FindAsync (user2Id); + var player2Id = user2!.PlayerId!.Value; + var userId = await RunAsDefaultUserAsync (); var user = await FindAsync (userId); var playerId = user!.PlayerId!.Value; - var command = new CreateClanWithChiefCommand + var createClanWithChiefCommand = new CreateClanWithChiefCommand { Description = "Test clan", Name = "Test clan", @@ -57,33 +60,18 @@ public async Task ShouldCreateClanWithChief () Type = Domain.Enums.ClanType.Normal, }; - var clanId = await SendAsync (command); - - var clanItem = await FindAsync (clanId); - var playerClanItem = await FindAsync (clanId, playerId); - - clanItem.Should ().NotBeNull (); - clanItem.Should ().HasProperty ("Description"); - clanItem.Should ().HasProperty ("Name"); - clanItem.Should ().HasProperty ("Region"); - - clanItem!.Description.Should ().Be (command.Description); - clanItem!.Name.Should ().Be (command.Name); - clanItem!.Region.Should ().Be (command.Region); - clanItem!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); - clanItem!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); - clanItem!.Type.Should ().Be (command.Type); - - playerClanItem.Should ().NotBeNull (); - playerClanItem.Should ().HasProperty ("ClanId"); - playerClanItem.Should ().HasProperty ("PlayerId"); + var addPlayerCommand = new AddPlayerCommand + { + ClanId = await SendAsync(createClanWithChiefCommand), + PlayerId = player2Id, + Role = Domain.Enums.ClanRole.Commoner, + }; - playerClanItem!.ClanId.Should ().Be (clanId); - playerClanItem!.PlayerId.Should ().Be (playerId); + await FluentActions.Invoking (() => SendAsync (addPlayerCommand)).Should ().NotThrowAsync (); } [Test] - public async Task ShouldAllowAccessToChief () + public async Task ShouldNotAllowTwoChiefs () { var user2Id = await RunAsUserAsync ("seconduser@local", "SecondUs3r!", Array.Empty ()); var user2 = await FindAsync (user2Id); @@ -107,24 +95,20 @@ public async Task ShouldAllowAccessToChief () { ClanId = await SendAsync(createClanWithChiefCommand), PlayerId = player2Id, - Role = Domain.Enums.ClanRole.Commoner, + Role = Domain.Enums.ClanRole.Chief, }; - await FluentActions.Invoking (() => SendAsync (addPlayerCommand)).Should ().NotThrowAsync (); + await FluentActions.Invoking (() => SendAsync (addPlayerCommand)).Should ().ThrowAsync (); } [Test] - public async Task ShouldNotAllowTwoChiefs () + public async Task ShouldCreateClanWithChief () { - var user2Id = await RunAsUserAsync ("seconduser@local", "SecondUs3r!", Array.Empty ()); - var user2 = await FindAsync (user2Id); - var player2Id = user2!.PlayerId!.Value; - var userId = await RunAsDefaultUserAsync (); var user = await FindAsync (userId); var playerId = user!.PlayerId!.Value; - var createClanWithChiefCommand = new CreateClanWithChiefCommand + var command = new CreateClanWithChiefCommand { Description = "Test clan", Name = "Test clan", @@ -134,14 +118,29 @@ public async Task ShouldNotAllowTwoChiefs () Type = Domain.Enums.ClanType.Normal, }; - var addPlayerCommand = new AddPlayerCommand - { - ClanId = await SendAsync(createClanWithChiefCommand), - PlayerId = player2Id, - Role = Domain.Enums.ClanRole.Chief, - }; + var clanId = await SendAsync (command); - await FluentActions.Invoking (() => SendAsync (addPlayerCommand)).Should ().ThrowAsync (); + var clanItem = await FindAsync (clanId); + var playerClanItem = await FindAsync (clanId, playerId); + + clanItem.Should ().NotBeNull (); + clanItem.Should ().HasProperty ("Description"); + clanItem.Should ().HasProperty ("Name"); + clanItem.Should ().HasProperty ("Region"); + + clanItem!.Description.Should ().Be (command.Description); + clanItem!.Name.Should ().Be (command.Name); + clanItem!.Region.Should ().Be ((Region) command.Region); + clanItem!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); + clanItem!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); + clanItem!.Type.Should ().Be (command.Type); + + playerClanItem.Should ().NotBeNull (); + playerClanItem.Should ().HasProperty ("ClanId"); + playerClanItem.Should ().HasProperty ("PlayerId"); + + playerClanItem!.ClanId.Should ().Be (clanId); + playerClanItem!.PlayerId.Should ().Be (playerId); } } } diff --git a/tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs b/tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs index 15c5a37..5615fd9 100644 --- a/tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs +++ b/tests/Application.IntegrationTests/Clan/Commands/UpdateClan.cs @@ -166,7 +166,7 @@ public async Task ShouldUpdateClan () item!.Description.Should ().Be (command.Description); item!.Name.Should ().Be (command.Name); - item!.Region.Should ().Be (command.Region); + item!.Region.Should ().Be ((Region) command.Region); item!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); item!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); item!.Type.Should ().Be (command.Type); From 553685f296a56670581d9e34c7f2e6d9b87e00bc Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Tue, 28 Nov 2023 12:54:29 -0500 Subject: [PATCH 10/43] Better CurrentPlayerClan query --- .../GetClanForCurrentPlayerQuery.cs | 29 +++++++------------ .../PlayerClanBriefDto.cs | 29 +++++++++++++++++++ .../src/components/Profile/ProfileClan.js | 16 ++++++---- src/WebAPI/Controllers/ClanController.cs | 2 +- .../Clan/Queries/GetClanForCurrentPlayer.cs | 15 ++++++++-- 5 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 src/Application/Clan/Queries/GetClanForCurrentUser/PlayerClanBriefDto.cs diff --git a/src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs b/src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs index 169f226..190f6ed 100644 --- a/src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs +++ b/src/Application/Clan/Queries/GetClanForCurrentUser/GetClanForCurrentPlayerQuery.cs @@ -14,22 +14,21 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ -using DataClash.Application.Clans.Queries.GetClansWithPagination; +using AutoMapper; +using DataClash.Application.Common.Exceptions; using DataClash.Application.Common.Interfaces; using DataClash.Application.Common.Security; -using MediatR; -using AutoMapper; using FluentValidation; -using System.Data; -using DataClash.Application.Common.Exceptions; +using MediatR; using Microsoft.EntityFrameworkCore; +using System.Data; namespace DataClash.Application.Clans.Queries.GetClanForCurrentPlayer { [Authorize] - public record GetClanForCurrentPlayerQuery () : IRequest; + public record GetClanForCurrentPlayerQuery () : IRequest; - public class GetClanForCurrentPlayerQueryHandler : IRequestHandler + public class GetClanForCurrentPlayerQueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; private readonly ICurrentPlayerService _currentPlayer; @@ -42,19 +41,11 @@ public GetClanForCurrentPlayerQueryHandler (IApplicationDbContext context, ICurr _mapper = mapper; } - public async Task Handle (GetClanForCurrentPlayerQuery query, CancellationToken cancellationToken) + public async Task Handle (GetClanForCurrentPlayerQuery query, CancellationToken cancellationToken) { - var playerIdProxy = _currentPlayer.PlayerId; - long playerId; - - if (!playerIdProxy.HasValue) - throw new ApplicationConstraintException ("User is not a player"); - else - playerId = playerIdProxy.Value; - - var playerClan = await _context.PlayerClans.Where (e => e.PlayerId == playerId).FirstOrDefaultAsync (cancellationToken); - var clan = playerClan == null ? null : await _context.Clans.FindAsync (new object[] { playerClan.ClanId }, cancellationToken); - return clan == null ? null : _mapper.Map (clan); + var playerId = _currentPlayer.PlayerId ?? throw new ApplicationConstraintException ("User is not a player"); + var playerClan = await _context.PlayerClans.Include (e => e.Clan).Where (e => e.PlayerId == playerId).FirstOrDefaultAsync (cancellationToken); + return playerClan == null ? null : _mapper.Map (playerClan); } } } diff --git a/src/Application/Clan/Queries/GetClanForCurrentUser/PlayerClanBriefDto.cs b/src/Application/Clan/Queries/GetClanForCurrentUser/PlayerClanBriefDto.cs new file mode 100644 index 0000000..e838fca --- /dev/null +++ b/src/Application/Clan/Queries/GetClanForCurrentUser/PlayerClanBriefDto.cs @@ -0,0 +1,29 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Clans.Queries.GetClansWithPagination; +using DataClash.Application.Common.Mappings; +using DataClash.Domain.Entities; +using DataClash.Domain.Enums; + +namespace DataClash.Application.Clans.Queries.GetClanForCurrentPlayer +{ + public class PlayerClanBriefDto : IMapFrom + { + public ClanBriefDto? Clan { get; init; } + public ClanRole Role { get; init; } + } +} diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js index 685fb7e..4a97311 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js @@ -15,7 +15,7 @@ * along with sep3cs. If not, see . */ import { Alert, Button, Form, FormGroup, Input, Label } from 'reactstrap' -import { ClanClient, ClanType } from '../../webApiClient.ts' +import { ClanClient, ClanRole, ClanType } from '../../webApiClient.ts' import { CreateClanWithChiefCommand } from '../../webApiClient.ts' import { ProfilePage } from './ProfilePage' import { UpdateClanCommand } from '../../webApiClient.ts' @@ -31,6 +31,7 @@ export function ProfileClan (props) const [ clanId, setClanId ] = useState () const [ clanName, setClanName ] = useState () const [ clanRegion, setClanRegion ] = useState () + const [ clanRole, setClanRole ] = useState () const [ clanTotalTrophiesToEnter, setClanTotalTrophiesToEnter ] = useState () const [ clanTotalTrophiesWonOnWar, setClanTotalTrophiesWonOnWar ] = useState () const [ clanType, setClanType ] = useState () @@ -42,18 +43,21 @@ export function ProfileClan (props) { if (!!playerProfile) try { - const clan = await clanClient.getForCurrentPlayer () + const playerClan = await clanClient.getForCurrentPlayer () - if (clan === null) + if (playerClan === null) setHasClan (false) else { setHasClan (true) - setClanId (clan.id) + const clan = playerClan.clan + const role = playerClan.role setClanDescription (clan.description) + setClanId (clan.id) setClanName (clan.name) setClanRegion (clan.region) + setClanRole (role) setClanTotalTrophiesToEnter (clan.totalTrophiesToEnter) setClanTotalTrophiesWonOnWar (clan.totalTrophiesWonOnWar) setClanType (clan.type) @@ -171,7 +175,9 @@ export function ProfileClan (props)
- + { clanRole !== ClanRole.Chief + ? <> + : }
)} diff --git a/src/WebAPI/Controllers/ClanController.cs b/src/WebAPI/Controllers/ClanController.cs index 7794bdf..01ee827 100644 --- a/src/WebAPI/Controllers/ClanController.cs +++ b/src/WebAPI/Controllers/ClanController.cs @@ -37,7 +37,7 @@ public async Task>> GetWithPagination ( [HttpGet] [Route ("current")] - public async Task> GetForCurrentPlayer ([FromQuery] GetClanForCurrentPlayerQuery query) + public async Task> GetForCurrentPlayer ([FromQuery] GetClanForCurrentPlayerQuery query) { return await Mediator.Send (query); } diff --git a/tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs b/tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs index c1ef4f1..64ab85a 100644 --- a/tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs +++ b/tests/Application.IntegrationTests/Clan/Queries/GetClanForCurrentPlayer.cs @@ -17,6 +17,7 @@ using DataClash.Application.Clans.Commands.CreateClan; using DataClash.Application.Clans.Commands.CreateClanWithChief; using DataClash.Application.Clans.Queries.GetClanForCurrentPlayer; +using DataClash.Domain.Enums; using DataClash.Domain.ValueObjects; using FluentAssertions; using NUnit.Framework; @@ -39,7 +40,7 @@ public async Task ShouldWorkWithNoPlayer () Region = Region.Somewhere, TotalTrophiesToEnter = 0, TotalTrophiesWonOnWar = 0, - Type = Domain.Enums.ClanType.Normal, + Type = ClanType.Normal, }; var newId = await SendAsync (command); @@ -60,14 +61,22 @@ public async Task ShouldWorkWithPlayer () Region = Region.Somewhere, TotalTrophiesToEnter = 0, TotalTrophiesWonOnWar = 0, - Type = Domain.Enums.ClanType.Normal, + Type = ClanType.Normal, }; var newId = await SendAsync (command); var dto = await SendAsync (new GetClanForCurrentPlayerQuery ()); dto.Should ().NotBeNull (); - dto!.Id.Should ().Be (newId); + + dto!.Clan!.Description.Should ().Be (command.Description); + dto!.Clan!.Id.Should ().Be (newId); + dto!.Clan!.Name.Should ().Be (command.Name); + dto!.Clan!.Region.Should ().Be ((Region) command.Region); + dto!.Clan!.TotalTrophiesToEnter.Should ().Be (command.TotalTrophiesToEnter); + dto!.Clan!.TotalTrophiesWonOnWar.Should ().Be (command.TotalTrophiesWonOnWar); + dto!.Clan!.Type.Should ().Be (command.Type); + dto!.Role.Should ().Be (ClanRole.Chief); } } } From 46c43d447503e01d56dd92915afa4bfc76f0931a Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Tue, 28 Nov 2023 13:12:24 -0500 Subject: [PATCH 11/43] Disabled controls for non-chiefs --- .../src/components/Profile/ProfileClan.js | 42 +++++++++++++------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js index 4a97311..2a63bd3 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileClan.js @@ -144,42 +144,58 @@ export function ProfileClan (props) : (
{ e.preventDefault (); setIsLoading (true); onSubmit ().then (() => setIsLoading (false)) }}> - setClanDescription (e.target.value)} /> + setClanDescription (e.target.value)} + value={clanDescription} /> - setClanName (e.target.value)} /> + setClanName (e.target.value)} + value={clanName} /> - setClanRegion (e.target.value)} /> + setClanRegion (e.target.value)} + value={clanRegion} /> - setClanTotalTrophiesToEnter (e.target.value)} /> + setClanTotalTrophiesToEnter (e.target.value)} + value={clanTotalTrophiesToEnter} /> - setClanTotalTrophiesWonOnWar (e.target.value)} /> + setClanTotalTrophiesWonOnWar (e.target.value)} + value={clanTotalTrophiesWonOnWar} /> k === ClanType[clanType])} - onChange={(e) => setClanType (ClanType[e.target.value])}> - { clanTypes.map ((type) => !isNaN (Number (type)) ? <> : ) } + disabled={clanRole !== ClanRole.Chief} + onChange={(e) => setClanType (ClanType[e.target.value])} + value={clanTypes.find (k => k === ClanType[clanType])} > + { clanTypes.map ((type) => !isNaN (Number (type)) ? <> : ) } - + { clanRole !== ClanRole.Chief + ? <> + :
- { clanRole !== ClanRole.Chief - ? <> - : } +
-
+
}
)} ) } From 953d1195e3156e386dd49caa96ebee3897bbf159 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Tue, 28 Nov 2023 13:19:38 -0500 Subject: [PATCH 12/43] Fixing profile route --- src/WebAPI/ClientApp/src/AppRoutes.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index b9f15f7..e4b33bb 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -20,11 +20,11 @@ import { Login } from './components/Login' import { LoginActions } from './services/AuthorizeConstants' import { Logout } from './components/Logout' import { LogoutActions } from './services/AuthorizeConstants' +import { Players } from './components/Players' +import { Profile } from './components/Profile' import { RequireAuth } from './components/RequireAuth' import { Route, Routes } from 'react-router-dom' -import { Players } from './components/Players' import { Wars } from './components/Wars' -import { Challenges } from './components/Challenges' const loginAction = (name) => () const logoutAction = (name) => () @@ -34,10 +34,11 @@ const AppRoutes = () => ( } index={true} />

Cards component placeholdes

}/> - }/> +

Challenges component placeholdes

}/>

Clans component placeholdes

}/>

Matches component placeholdes

}/> }/> + }/> }/> From 568d39c354bd39555b397af77adb5721313ebf92 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Tue, 28 Nov 2023 14:59:55 -0500 Subject: [PATCH 13/43] Fixing entities --- .../CreateChallenge/CreateChallengeCommand.cs | 2 +- .../UpdateChallenge/UpdateChallengeCommand.cs | 2 +- .../Command/CreateClan/CreateClanCommand.cs | 6 ++--- .../Command/UpdateClan/UpdateClanCommand.cs | 4 ++-- src/Domain/Entities/Card.cs | 4 ++-- src/Domain/Entities/CardGift.cs | 4 ++-- src/Domain/Entities/Challenge.cs | 2 +- src/Domain/Entities/Clan.cs | 4 ++-- src/Domain/Entities/Match.cs | 4 ++-- src/Domain/Entities/Player.cs | 2 +- src/Domain/Entities/PlayerCard.cs | 4 ++-- src/Domain/Entities/PlayerChallenge.cs | 10 ++++----- src/Domain/Entities/PlayerClan.cs | 4 ++-- src/Domain/Entities/WarClan.cs | 4 ++-- src/Framework/Identity/ProfileService.cs | 22 +++++++++++++++++-- .../Services/CurrentPlayerService.cs | 22 +++++++++---------- 16 files changed, 59 insertions(+), 41 deletions(-) diff --git a/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs b/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs index 1ee6df3..1309b67 100644 --- a/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs +++ b/src/Application/Challenges/Commands/CreateChallenge/CreateChallengeCommand.cs @@ -32,7 +32,7 @@ public record CreateChallengeCommand : IRequest public TimeSpan Duration { get; init; } public long MaxLooses { get; init; } public long MinLevel { get; init; } - public string? Name { get; init; } + public string Name { get; init; } = null!; } diff --git a/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs b/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs index 4adcc83..eb4cc0e 100644 --- a/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs +++ b/src/Application/Challenges/Commands/UpdateChallenge/UpdateChallengeCommand.cs @@ -34,7 +34,7 @@ public record UpdateChallengeCommand : IRequest public TimeSpan Duration { get; init; } public long MaxLooses { get; init; } public long MinLevel { get; init; } - public string? Name { get; init; } + public string Name { get; init; } = null!; } diff --git a/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs b/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs index a3e0c67..2348018 100644 --- a/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs +++ b/src/Application/Clan/Command/CreateClan/CreateClanCommand.cs @@ -28,8 +28,8 @@ namespace DataClash.Application.Clans.Commands.CreateClan public record CreateClanCommand : IRequest { public string? Description { get; init; } - public string? Name { get; init; } - public string? Region { get; init; } + public string Name { get; init; } = null!; + public string Region { get; init; } = null!; public long TotalTrophiesToEnter { get; init; } public long TotalTrophiesWonOnWar { get; init; } public ClanType Type { get; init; } @@ -50,7 +50,7 @@ public async Task Handle (CreateClanCommand request, CancellationToken can { Description = request.Description, Name = request.Name, - Region = Region.From (request.Region!), + Region = Region.From (request.Region), TotalTrophiesToEnter = request.TotalTrophiesToEnter, TotalTrophiesWonOnWar = request.TotalTrophiesWonOnWar, Type = request.Type, diff --git a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs index 5db49b0..2cd5694 100644 --- a/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs +++ b/src/Application/Clan/Command/UpdateClan/UpdateClanCommand.cs @@ -30,8 +30,8 @@ public record UpdateClanCommand : IRequest { public long Id { get; init; } public string? Description { get; init; } - public string? Name { get; init; } - public string? Region { get; init; } + public string Name { get; init; } = null!; + public string Region { get; init; } = null!; public long TotalTrophiesToEnter { get; init; } public long TotalTrophiesWonOnWar { get; init; } public ClanType Type { get; init; } diff --git a/src/Domain/Entities/Card.cs b/src/Domain/Entities/Card.cs index e617ebb..d8a56e5 100644 --- a/src/Domain/Entities/Card.cs +++ b/src/Domain/Entities/Card.cs @@ -24,9 +24,9 @@ public abstract class Card : BaseEntity public string? Description { get; set; } public double ElixirCost { get; set; } public long InitialLevel { get; set; } - public string? Name { get; set; } + public string Name { get; set; } = null!; public Quality Quality { get; set; } - public string? Picture { get; set; } + public string Picture { get; set; } = null!; } public class MagicCard : Card diff --git a/src/Domain/Entities/CardGift.cs b/src/Domain/Entities/CardGift.cs index 2445e9c..5c98092 100644 --- a/src/Domain/Entities/CardGift.cs +++ b/src/Domain/Entities/CardGift.cs @@ -23,7 +23,7 @@ public class CardGift public long ClanId { get; set; } public long PlayerId { get; set; } - public virtual Clan? Clan { get; set; } - public virtual PlayerCard? PlayerCard { get; set; } + public Clan Clan { get; set; } = null!; + public PlayerCard PlayerCard { get; set; } = null!; } } diff --git a/src/Domain/Entities/Challenge.cs b/src/Domain/Entities/Challenge.cs index fb7017a..71d72cc 100644 --- a/src/Domain/Entities/Challenge.cs +++ b/src/Domain/Entities/Challenge.cs @@ -27,6 +27,6 @@ public class Challenge : BaseEntity public TimeSpan Duration { get; set; } public long MaxLooses { get; set; } public long MinLevel { get; set; } - public string? Name { get; set; } + public string Name { get; set; } = null!; } } diff --git a/src/Domain/Entities/Clan.cs b/src/Domain/Entities/Clan.cs index 432e435..30aee95 100644 --- a/src/Domain/Entities/Clan.cs +++ b/src/Domain/Entities/Clan.cs @@ -23,8 +23,8 @@ namespace DataClash.Domain.Entities public class Clan : BaseEntity { public string? Description { get; set; } - public string? Name { get; set; } - public Region? Region { get; set; } + public string Name { get; set; } = null!; + public Region Region { get; set; } = null!; public long TotalTrophiesToEnter { get; set; } public long TotalTrophiesWonOnWar { get; set; } public ClanType Type { get; set; } diff --git a/src/Domain/Entities/Match.cs b/src/Domain/Entities/Match.cs index efde942..d3f67f4 100644 --- a/src/Domain/Entities/Match.cs +++ b/src/Domain/Entities/Match.cs @@ -24,7 +24,7 @@ public class Match public DateTime BeginDate { get; set; } public TimeSpan Duration { get; set; } - public virtual Player? LooserPlayer { get; set; } - public virtual Player? WinnerPlayer { get; set; } + public Player LooserPlayer { get; set; } = null!; + public Player WinnerPlayer { get; set; } = null!; } } diff --git a/src/Domain/Entities/Player.cs b/src/Domain/Entities/Player.cs index cbb59d5..2169d39 100644 --- a/src/Domain/Entities/Player.cs +++ b/src/Domain/Entities/Player.cs @@ -27,6 +27,6 @@ public class Player : BaseEntity public long TotalThrophies { get; set; } public long TotalWins { get; set; } - public virtual Card? FavoriteCard { get; set; } + public Card? FavoriteCard { get; set; } } } diff --git a/src/Domain/Entities/PlayerCard.cs b/src/Domain/Entities/PlayerCard.cs index 9081cb6..8424324 100644 --- a/src/Domain/Entities/PlayerCard.cs +++ b/src/Domain/Entities/PlayerCard.cs @@ -23,7 +23,7 @@ public class PlayerCard public long PlayerId { get; set; } public long Level { get; set; } - public virtual Card? Card { get; set; } - public virtual Player? Player { get; set; } + public Card Card { get; set; } = null!; + public Player Player { get; set; } = null!; } } diff --git a/src/Domain/Entities/PlayerChallenge.cs b/src/Domain/Entities/PlayerChallenge.cs index 5d2000d..0005881 100644 --- a/src/Domain/Entities/PlayerChallenge.cs +++ b/src/Domain/Entities/PlayerChallenge.cs @@ -19,11 +19,11 @@ namespace DataClash.Domain.Entities { public class PlayerChallenge { - public long ChallengeId { get; set; } - public long PlayerId { get; set; } - public long WonThrophies { get; set; } + public long ChallengeId { get; set; } + public long PlayerId { get; set; } + public long WonThrophies { get; set; } - public virtual Challenge? Challenge { get; set; } - public virtual Player? Player { get; set; } + public Challenge Challenge { get; set; } = null!; + public Player Player { get; set; } = null!; } } diff --git a/src/Domain/Entities/PlayerClan.cs b/src/Domain/Entities/PlayerClan.cs index 28038d2..adcf90f 100644 --- a/src/Domain/Entities/PlayerClan.cs +++ b/src/Domain/Entities/PlayerClan.cs @@ -24,7 +24,7 @@ public class PlayerClan public long PlayerId { get; set; } public ClanRole Role { get; set; } - public virtual Clan? Clan { get; set; } - public virtual Player? Player { get; set; } + public Clan Clan { get; set; } = null!; + public Player Player { get; set; } = null!; } } diff --git a/src/Domain/Entities/WarClan.cs b/src/Domain/Entities/WarClan.cs index ebf1660..aaacc6c 100644 --- a/src/Domain/Entities/WarClan.cs +++ b/src/Domain/Entities/WarClan.cs @@ -23,7 +23,7 @@ public class WarClan public long WarId { get; set; } public long WonThrophies { get; set; } - public virtual Clan? Clan { get; set; } - public virtual War? War { get; set; } + public Clan Clan { get; set; } = null!; + public War War { get; set; } = null!; } } diff --git a/src/Framework/Identity/ProfileService.cs b/src/Framework/Identity/ProfileService.cs index 6ff2130..dffed4b 100644 --- a/src/Framework/Identity/ProfileService.cs +++ b/src/Framework/Identity/ProfileService.cs @@ -14,6 +14,7 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Application.Common.Interfaces; using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; using IdentityModel; @@ -24,13 +25,16 @@ namespace DataClash.Framework.Identity { public class ProfileService : IProfileService { + private readonly IApplicationDbContext _context; private readonly IUserClaimsPrincipalFactory _userClaimsPrincipalFactory; private readonly UserManager _userManager; public ProfileService ( - UserManager userManager, - IUserClaimsPrincipalFactory userClaimsPrincipalFactory) + IApplicationDbContext context, + IUserClaimsPrincipalFactory userClaimsPrincipalFactory, + UserManager userManager) { + _context = context; _userClaimsPrincipalFactory = userClaimsPrincipalFactory; _userManager = userManager; } @@ -47,6 +51,20 @@ public async Task GetProfileDataAsync (ProfileDataRequestContext context) roleClaims.Add (new Claim (JwtClaimTypes.Role, role)); } + if (user!.PlayerId.HasValue) + { + var player = await _context.Players.FindAsync (user!.PlayerId); + var nickname = player?.Nickname; + + if (nickname != null) + { + var type = JwtClaimTypes.NickName; + var claim = new Claim (type, nickname); + + context.IssuedClaims.Add (claim); + } + } + context.IssuedClaims.AddRange (roleClaims); context.IssuedClaims.AddRange (claims.Claims); } diff --git a/src/Framework/Services/CurrentPlayerService.cs b/src/Framework/Services/CurrentPlayerService.cs index ed37bcc..cb074cc 100644 --- a/src/Framework/Services/CurrentPlayerService.cs +++ b/src/Framework/Services/CurrentPlayerService.cs @@ -26,19 +26,19 @@ public class CurrentPlayerService : ICurrentPlayerService private readonly ICurrentUserService _currentUser; public long? PlayerId - { - get { - var userId = _currentUser.UserId; - if (userId == null) - return null; - else - { - var user = _context.Users.Find (new object[] { userId }); - return user?.PlayerId; - } + get + { + var userId = _currentUser.UserId; + if (userId == null) + return null; + else + { + var user = _context.Users.Find (new object[] { userId }); + return user?.PlayerId; + } + } } - } public CurrentPlayerService (ApplicationDbContext context, ICurrentUserService currentUser) { From 3e341168585d6205218d262906c8d2fd3150c397 Mon Sep 17 00:00:00 2001 From: MaykolLuisMa Date: Wed, 29 Nov 2023 09:02:11 -0500 Subject: [PATCH 14/43] Working on it (cherry picked from commit a5a98f8b320bcba97c6b40b7b18973f692a0974e) --- .../CreateMatch/CreateMatchCommand.cs | 62 ++++++ .../CreateMatchCommandValidator.cs | 30 +++ .../DeleteMatch/DeleteMatchCommand.cs | 53 +++++ .../UpdateMatch/UpdateMatchCommand.cs | 61 ++++++ .../UpdateMatchCommandValidator.cs | 30 +++ .../EventHandlers/MatchCreateEventHandler.cs | 38 ++++ .../EventHandlers/MatchDeletedEventHandler.cs | 38 ++++ .../EventHandlers/MatchUpdatedEventHandler.cs | 38 ++++ .../GetMatchWithPaginationQuery.cs | 51 +++++ .../GetMatchsWithPaginationQueryValidator.cs | 29 +++ .../GetMatchsWithPagination/MatchBriefDto.cs | 30 +++ src/Domain/Events/MatchCreatedEvent.cs | 31 +++ src/Domain/Events/MatchDeletedEvent.cs | 31 +++ src/Domain/Events/MatchUpdatedEvent.cs | 31 +++ src/WebAPI/ClientApp/src/AppRoutes.js | 7 +- .../ClientApp/src/components/Matches.js | 196 ++++++++++++++++++ src/WebAPI/Controllers/MatchController.cs | 65 ++++++ 17 files changed, 817 insertions(+), 4 deletions(-) create mode 100644 src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs create mode 100644 src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs create mode 100644 src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs create mode 100644 src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs create mode 100644 src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs create mode 100644 src/Application/Matches/EventHandlers/MatchCreateEventHandler.cs create mode 100644 src/Application/Matches/EventHandlers/MatchDeletedEventHandler.cs create mode 100644 src/Application/Matches/EventHandlers/MatchUpdatedEventHandler.cs create mode 100644 src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs create mode 100644 src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchsWithPaginationQueryValidator.cs create mode 100644 src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs create mode 100644 src/Domain/Events/MatchCreatedEvent.cs create mode 100644 src/Domain/Events/MatchDeletedEvent.cs create mode 100644 src/Domain/Events/MatchUpdatedEvent.cs create mode 100644 src/WebAPI/ClientApp/src/components/Matches.js create mode 100644 src/WebAPI/Controllers/MatchController.cs diff --git a/src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs new file mode 100644 index 0000000..b97eee0 --- /dev/null +++ b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs @@ -0,0 +1,62 @@ +/* sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Events; +using MediatR; + +namespace DataClash.Application.Matches.Commands.CreateMatch +{ + [Authorize (Roles = "Administrator")] + public record CreateMatchCommand : IRequest + { + public long WinnerPlayerId { get; init; } + public long LooserPlayerId { get; init; } + public DateTime BeginDate { get; init; } + public TimeSpan Duration { get; init; } + public Player? LooserPlayer { get; init; } + public Player? WinnerPlayer { get; init; } + + } + public class CreateMatchCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public CreateMatchCommandHandler (IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle (CreateMatchCommand request, CancellationToken cancellationToken) + { + var entity = new Match + { + WinnerPlayerId = request.WinnerPlayerId, + LooserPlayerId = request.LooserPlayerId, + BeginDate = request.BeginDate, + Duration = request.Duration, + WinnerPlayer = request.WinnerPlayer, + LooserPlayer = request.LooserPlayer + }; + + entity.AddDomainEvent (new MatchCreatedEvent (entity)); + _context.Matches.Add (entity); + + await _context.SaveChangesAsync (cancellationToken); + return entity.Id; + } + } + +} \ No newline at end of file diff --git a/src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs new file mode 100644 index 0000000..6fd0481 --- /dev/null +++ b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs @@ -0,0 +1,30 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Matches.Commands.CreateMatch +{ + public class CreateMatchCommandValidator : AbstractValidator + { + public CreateMatchCommandValidator () + { + //falta validar los id de jugadores + RuleFor (v => v.BeginDate).NotEmpty (); + RuleFor (v => v.Duration).NotEmpty (); + } + } +} diff --git a/src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs b/src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs new file mode 100644 index 0000000..e4b3ae0 --- /dev/null +++ b/src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs @@ -0,0 +1,53 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Events; +using MediatR; +using Microsoft.EntityFrameworkCore; + +namespace DataClash.Application.Matches.Commands.DeleteMatch +{ + public record DeleteMatchCommand (long Id) : IRequest; + + [Authorize (Roles = "Administrator")] + public class DeleteMatchCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public DeleteMatchCommandHandler (IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle (DeleteMatchCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Matches + .Where (l => l.Id == request.Id) + .SingleOrDefaultAsync (cancellationToken) + ?? throw new NotFoundException (nameof (Match), request.Id); + + _context.Matches.Remove (entity); + entity.AddDomainEvent (new MatchDeletedEvent (entity)); + + await _context.SaveChangesAsync (cancellationToken); + } + } +} + diff --git a/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs new file mode 100644 index 0000000..b686752 --- /dev/null +++ b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs @@ -0,0 +1,61 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Exceptions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Security; +using DataClash.Domain.Entities; +using DataClash.Domain.Events; +using MediatR; + +namespace DataClash.Application.Matches.Commands.UpdateMatch +{ + [Authorize (Roles = "Administrator")] + public record UpdateMatchCommand : IRequest + { + public long Id { get; init; } + public long WinnerPlayerId { get; init; } + public long LooserPlayerId { get; init; } + public DateTime BeginDate { get; init; } + public TimeSpan Duration { get; init; } + public Player? LooserPlayer { get; init; } + public Player? WinnerPlayer { get; init; } + } + + public class UpdateMatchCommandHandler : IRequestHandler + { + private readonly IApplicationDbContext _context; + + public UpdateMatchCommandHandler (IApplicationDbContext context) + { + _context = context; + } + + public async Task Handle (UpdateMatchCommand request, CancellationToken cancellationToken) + { + var entity = await _context.Matches.FindAsync (new object [] { request.Id }, cancellationToken) ?? throw new NotFoundException (nameof (Match), request.Id); + entity.WinnerPlayerId = request.WinnerPlayerId; + entity.LooserPlayerId = request.LooserPlayerId; + entity.BeginDate = request.BeginDate; + entity.Duration = request.Duration; + entity.WinnerPlayer = request.WinnerPlayer; + entity.LooserPlayer = request.LooserPlayer; + + entity.AddDomainEvent (new MatchUpdatedEvent (entity)); + await _context.SaveChangesAsync (cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs new file mode 100644 index 0000000..0585260 --- /dev/null +++ b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs @@ -0,0 +1,30 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Matches.Commands.UpdateMatch +{ + public class UpdateMatchCommandValidator : AbstractValidator + { + public UpdateMatchCommandValidator () + { + //falta validar los id + RuleFor (v => v.BeginDate).NotEmpty (); + RuleFor (v => v.Duration).NotEmpty (); + } + } +} diff --git a/src/Application/Matches/EventHandlers/MatchCreateEventHandler.cs b/src/Application/Matches/EventHandlers/MatchCreateEventHandler.cs new file mode 100644 index 0000000..d2c9648 --- /dev/null +++ b/src/Application/Matches/EventHandlers/MatchCreateEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Matches.EventHandlers +{ + public class MatchCreatedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public MatchCreatedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (MatchCreatedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Matches/EventHandlers/MatchDeletedEventHandler.cs b/src/Application/Matches/EventHandlers/MatchDeletedEventHandler.cs new file mode 100644 index 0000000..0f3f8df --- /dev/null +++ b/src/Application/Matches/EventHandlers/MatchDeletedEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Matches.EventHandlers +{ + public class MatchDeletedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public MatchDeletedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (MatchDeletedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Matches/EventHandlers/MatchUpdatedEventHandler.cs b/src/Application/Matches/EventHandlers/MatchUpdatedEventHandler.cs new file mode 100644 index 0000000..1ac6fca --- /dev/null +++ b/src/Application/Matches/EventHandlers/MatchUpdatedEventHandler.cs @@ -0,0 +1,38 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Events; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace DataClash.Application.Matches.EventHandlers +{ + public class MatchUpdatedEventHandler : INotificationHandler + { + private readonly ILogger _logger; + + public MatchUpdatedEventHandler (ILogger logger) + { + _logger = logger; + } + + public Task Handle (MatchUpdatedEvent notification, CancellationToken cancellationToken) + { + _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); + return Task.CompletedTask; + } + } +} diff --git a/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs b/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs new file mode 100644 index 0000000..ca5f68b --- /dev/null +++ b/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs @@ -0,0 +1,51 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using AutoMapper; +using AutoMapper.QueryableExtensions; +using DataClash.Application.Common.Interfaces; +using DataClash.Application.Common.Mappings; +using DataClash.Application.Common.Models; +using DataClash.Application.Matches.Queries.GetMatch; +using MediatR; + +namespace DataClash.Application.Matches.Queries.GetMatchesWithPagination +{ + public record GetMatchesWithPaginationQuery : IRequest> + { + public int PageNumber { get; init; } = 1;//posible error + public int PageSize { get; init; } = 10; + } + + public class GetMatchesWithPaginationQueryHandler : IRequestHandler> + { + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + + public GetMatchesWithPaginationQueryHandler (IApplicationDbContext context, IMapper mapper) + { + _context = context; + _mapper = mapper; + } + + public async Task> Handle (GetMatchesWithPaginationQuery query, CancellationToken cancellationToken) + { + return await _context.Matches + .ProjectTo (_mapper.ConfigurationProvider) + .PaginatedListAsync (query.PageNumber, query.PageSize); + } + } +} diff --git a/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchsWithPaginationQueryValidator.cs b/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchsWithPaginationQueryValidator.cs new file mode 100644 index 0000000..57f5aed --- /dev/null +++ b/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchsWithPaginationQueryValidator.cs @@ -0,0 +1,29 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using FluentValidation; + +namespace DataClash.Application.Matches.Queries.GetMatchesWithPagination +{ + public class GetMatchesWithPaginationQueryValidator : AbstractValidator + { + public GetMatchesWithPaginationQueryValidator () + { + RuleFor (x => x.PageNumber).GreaterThanOrEqualTo (1).WithMessage ("PageNumber at least greater than or equal to 1."); + RuleFor (x => x.PageSize).GreaterThanOrEqualTo (1).WithMessage ("PageSize at least greater than or equal to 1."); + } + } +} diff --git a/src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs b/src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs new file mode 100644 index 0000000..33bfd7b --- /dev/null +++ b/src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs @@ -0,0 +1,30 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Mappings; +using DataClash.Domain.Entities; + +namespace DataClash.Application.Matches.Queries.GetMatch +{ + public class MatchBriefDto : IMapFrom + { + public long Id { get; init; } + public long WinnerPlayerId { get; init; } + public long LooserPlayerId { get; init; } + public DateTime BeginDate { get; init; } + public TimeSpan Duration { get; init; } + } +} diff --git a/src/Domain/Events/MatchCreatedEvent.cs b/src/Domain/Events/MatchCreatedEvent.cs new file mode 100644 index 0000000..467815b --- /dev/null +++ b/src/Domain/Events/MatchCreatedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class MatchCreatedEvent : BaseEvent + { + public Match Item { get; } + + public MatchCreatedEvent (Match item) + { + Item = item; + } + } +} diff --git a/src/Domain/Events/MatchDeletedEvent.cs b/src/Domain/Events/MatchDeletedEvent.cs new file mode 100644 index 0000000..ad18eec --- /dev/null +++ b/src/Domain/Events/MatchDeletedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class MatchDeletedEvent : BaseEvent + { + public Match Item { get; } + + public MatchDeletedEvent (Match item) + { + Item = item; + } + } +} diff --git a/src/Domain/Events/MatchUpdatedEvent.cs b/src/Domain/Events/MatchUpdatedEvent.cs new file mode 100644 index 0000000..1400f10 --- /dev/null +++ b/src/Domain/Events/MatchUpdatedEvent.cs @@ -0,0 +1,31 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Domain.Common; +using DataClash.Domain.Entities; + +namespace DataClash.Domain.Events +{ + public class MatchUpdatedEvent : BaseEvent + { + public Match Item { get; } + + public MatchUpdatedEvent (Match item) + { + Item = item; + } + } +} diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index e4b33bb..42ddf9a 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -20,11 +20,11 @@ import { Login } from './components/Login' import { LoginActions } from './services/AuthorizeConstants' import { Logout } from './components/Logout' import { LogoutActions } from './services/AuthorizeConstants' -import { Players } from './components/Players' -import { Profile } from './components/Profile' import { RequireAuth } from './components/RequireAuth' import { Route, Routes } from 'react-router-dom' +import { Players } from './components/Players' import { Wars } from './components/Wars' +import { Matches } from './components/Matches'//NEW const loginAction = (name) => () const logoutAction = (name) => () @@ -36,9 +36,8 @@ const AppRoutes = () => (

Cards component placeholdes

}/>

Challenges component placeholdes

}/>

Clans component placeholdes

}/> -

Matches component placeholdes

}/> + }/> }/> - }/> }/> diff --git a/src/WebAPI/ClientApp/src/components/Matches.js b/src/WebAPI/ClientApp/src/components/Matches.js new file mode 100644 index 0000000..b4c87b6 --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Matches.js @@ -0,0 +1,196 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +import { Button, Table } from 'reactstrap' +import { CreateMatchCommand } from '../webApiClient.ts'//probablemente de error +import { DateTime } from './DateTime.js' +import { Pager } from './Pager.js' +import { TimeSpan } from './TimeSpan.js' +import { UpdateMatchCommand } from '../webApiClient.ts' +import { useParams } from 'react-router-dom' +import { UserRoles } from '../services/AuthorizeConstants.js' +import { MatchClient } from '../webApiClient.ts' +import authService from '../services/AuthorizeService.ts' +import React, { useEffect, useState } from 'react' + +export function Matches () +{ + const { initialPage } = useParams () + const [ activePage, setActivePage ] = useState (initialPage ? initialPage : 0) + const [ hasNextPage, setHasNextPage ] = useState (false) + const [ hasPreviousPage, setHasPreviousPage ] = useState (false) + const [ isAdministrator, setIsAdministrator ] = useState (false) + const [ isLoading, setIsLoading ] = useState (false) + const [ items, setItems ] = useState (undefined) + const [ totalPages, setTotalPages ] = useState (0) + const [ matchClient ] = useState (new MatchClient ()) + + const pageSize = 10 + const visibleIndices = 5 + + const addMatch = async () => + { + const data = new CreateMatchCommand () + data.winnerPlayerId = 1 + data.looserPlayerId = 2 + data.beginDay = new Date () + data.duration = "00:00:01" + await matchClient.create (data) + setActivePage (-1) + } + + const removeMatch = async (item) => + { + await matchClient.delete (item.id) + setActivePage (-1) + } + + const updateMatch = async (item) => + { + const data = new UpdateMatchCommand () + data.id = item.id + data.winnerPlayerId = item.winnerPlayerId + data.looserPlayerId = item.looserPlayerId + data.beginDay = item.beginDay + data.duration = item.duration + await matchClient.update (item.id, data) + } + + useEffect (() => + { + const checkRole = async () => + { + const hasRole = await authService.hasRole (UserRoles.Administrator) + + setIsAdministrator (hasRole) + } + + setIsLoading (true) + checkRole ().then (() => setIsLoading (false)) + }, []) + + useEffect (() => + { + const lastPage = async () => + { + const paginatedList = await matchClient.getWithPagination (1, pageSize) + return paginatedList.totalPages + } + + const refreshPage = async () => + { + const paginatedList = await matchClient.getWithPagination (activePage + 1, pageSize) + + setHasNextPage (paginatedList.hasNextPage) + setHasPreviousPage (paginatedList.hasPreviousPage) + setItems (paginatedList.items) + setTotalPages (paginatedList.totalPages) + } + + if (activePage >= 0) + { + setIsLoading (true) + refreshPage ().then (() => setIsLoading (false)) + } + else + { + lastPage ().then ((total) => + { + if (total === 0) + setActivePage (0) + else + setActivePage (total - 1) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activePage]) + + return ( + isLoading + ? (
) + : ( + <> +
+ setActivePage (index)} + totalPages={totalPages} + visibleIndices={visibleIndices} /> +
+
+ + + + + + + + + + + + { (items ?? []).map ((item, index) => ( + + + + + + + { + (!isAdministrator) + ? () + } + )) + } + + + { + (!isAdministrator) + ? () + : ( + + + ) + } + +
{'#'}{'Winner player'}{'Looser player'}{'Begin day'}{'Duration'} +
{ item.id } +

{ item.winnerPlayerId }

+
+

{ item.looserPlayerId }

+
+ { item.beginDay = date; updateMatch (item) }} + readOnly={!isAdministrator} /> + + { item.duration = span; updateMatch (item) }} + readOnly={!isAdministrator} /> + ) + : ( + +
+ + +
+
+ )) +} diff --git a/src/WebAPI/Controllers/MatchController.cs b/src/WebAPI/Controllers/MatchController.cs new file mode 100644 index 0000000..6bedcd9 --- /dev/null +++ b/src/WebAPI/Controllers/MatchController.cs @@ -0,0 +1,65 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +using DataClash.Application.Common.Models; +using DataClash.Application.Matches.Commands.CreateMatch; +using DataClash.Application.Matches.Commands.DeleteMatch; +using DataClash.Application.Matches.Commands.UpdateMatch; +using DataClash.Application.Matches.Queries.GetMatch; +using DataClash.Application.Matches.Queries.GetMatchesWithPagination; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace DataClash.WebUI.Controllers +{ + [Authorize] + public class MatchController : ApiControllerBase + { + [HttpGet] + public async Task>> GetWithPagination ([FromQuery] GetMatchesWithPaginationQuery query) + { + return await Mediator.Send (query); + } + + [HttpPost] + public async Task> Create (CreateMatchCommand command) + { + return await Mediator.Send (command); + } + + [HttpDelete ("{id}")] + [ProducesResponseType (StatusCodes.Status204NoContent)] + [ProducesDefaultResponseType] + public async Task Delete (long id) + { + await Mediator.Send (new DeleteMatchCommand (id)); + return NoContent (); + } + + [HttpPut ("{id}")] + [ProducesResponseType (StatusCodes.Status204NoContent)] + [ProducesResponseType (StatusCodes.Status400BadRequest)] + [ProducesDefaultResponseType] + public async Task Update (long id, UpdateMatchCommand command) + { + if (id != command.Id) + return BadRequest (); + + await Mediator.Send (command); + return NoContent (); + } + } +} From 11660cd1ab6c14cdc49c118eca9c1e85eac5cf75 Mon Sep 17 00:00:00 2001 From: MaykolLuisMa Date: Wed, 29 Nov 2023 09:02:28 -0500 Subject: [PATCH 15/43] Component (partial) (cherry picked from commit 9a59a832b43b0f8d1609245192c68047d357770e) --- .../CreateMatch/CreateMatchCommand.cs | 18 +-- .../CreateMatchCommandValidator.cs | 3 +- .../DeleteMatch/DeleteMatchCommand.cs | 11 +- .../UpdateMatch/UpdateMatchCommand.cs | 10 +- .../UpdateMatchCommandValidator.cs | 3 +- .../GetMatchWithPaginationQuery.cs | 2 +- .../GetMatchsWithPagination/MatchBriefDto.cs | 2 +- src/Domain/Entities/Match.cs | 8 +- src/WebAPI/ClientApp/src/AppRoutes.js | 7 +- .../ClientApp/src/components/Matches.js | 112 ++++++++++++------ .../src/services/AuthorizeProvider.js | 4 +- src/WebAPI/Controllers/MatchController.cs | 13 +- 12 files changed, 121 insertions(+), 72 deletions(-) diff --git a/src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs index b97eee0..37392c6 100644 --- a/src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs +++ b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommand.cs @@ -20,17 +20,17 @@ namespace DataClash.Application.Matches.Commands.CreateMatch { [Authorize (Roles = "Administrator")] - public record CreateMatchCommand : IRequest + public record CreateMatchCommand : IRequest<(long, long, DateTime)> { public long WinnerPlayerId { get; init; } public long LooserPlayerId { get; init; } public DateTime BeginDate { get; init; } public TimeSpan Duration { get; init; } - public Player? LooserPlayer { get; init; } - public Player? WinnerPlayer { get; init; } + //public Player? LooserPlayer { get; init; } + //public Player? WinnerPlayer { get; init; } } - public class CreateMatchCommandHandler : IRequestHandler + public class CreateMatchCommandHandler : IRequestHandler { private readonly IApplicationDbContext _context; @@ -39,7 +39,7 @@ public CreateMatchCommandHandler (IApplicationDbContext context) _context = context; } - public async Task Handle (CreateMatchCommand request, CancellationToken cancellationToken) + public async Task<(long,long,DateTime)> Handle (CreateMatchCommand request, CancellationToken cancellationToken) { var entity = new Match { @@ -47,15 +47,15 @@ public async Task Handle (CreateMatchCommand request, CancellationToken ca LooserPlayerId = request.LooserPlayerId, BeginDate = request.BeginDate, Duration = request.Duration, - WinnerPlayer = request.WinnerPlayer, - LooserPlayer = request.LooserPlayer + //WinnerPlayer = request.WinnerPlayer, + //LooserPlayer = request.LooserPlayer }; - entity.AddDomainEvent (new MatchCreatedEvent (entity)); + //entity.AddDomainEvent (new MatchCreatedEvent (entity)); _context.Matches.Add (entity); await _context.SaveChangesAsync (cancellationToken); - return entity.Id; + return (entity.WinnerPlayerId,entity.LooserPlayerId,entity.BeginDate); } } diff --git a/src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs index 6fd0481..1d9d3e3 100644 --- a/src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs +++ b/src/Application/Matches/Commands/CreateMatch/CreateMatchCommandValidator.cs @@ -22,7 +22,8 @@ public class CreateMatchCommandValidator : AbstractValidator { public CreateMatchCommandValidator () { - //falta validar los id de jugadores + RuleFor(v => v.WinnerPlayerId).NotEmpty(); + RuleFor(v => v.LooserPlayerId).NotEmpty(); RuleFor (v => v.BeginDate).NotEmpty (); RuleFor (v => v.Duration).NotEmpty (); } diff --git a/src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs b/src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs index e4b3ae0..c079715 100644 --- a/src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs +++ b/src/Application/Matches/Commands/DeleteMatch/DeleteMatchCommand.cs @@ -24,7 +24,7 @@ namespace DataClash.Application.Matches.Commands.DeleteMatch { - public record DeleteMatchCommand (long Id) : IRequest; + public record DeleteMatchCommand ((long WinnerPlayerId,long LooserPlayerId, DateTime BeginDate) Key) : IRequest; [Authorize (Roles = "Administrator")] public class DeleteMatchCommandHandler : IRequestHandler @@ -39,12 +39,15 @@ public DeleteMatchCommandHandler (IApplicationDbContext context) public async Task Handle (DeleteMatchCommand request, CancellationToken cancellationToken) { var entity = await _context.Matches - .Where (l => l.Id == request.Id) + .Where (l => l.WinnerPlayerId == request.Key.WinnerPlayerId + && l.LooserPlayerId == request.Key.LooserPlayerId + && l.BeginDate == request.Key.BeginDate) .SingleOrDefaultAsync (cancellationToken) - ?? throw new NotFoundException (nameof (Match), request.Id); + ?? throw new NotFoundException (nameof (Match), + (request.Key.WinnerPlayerId,request.Key.LooserPlayerId,request.Key.BeginDate)); _context.Matches.Remove (entity); - entity.AddDomainEvent (new MatchDeletedEvent (entity)); + //entity.AddDomainEvent (new MatchDeletedEvent (entity)); await _context.SaveChangesAsync (cancellationToken); } diff --git a/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs index b686752..41e0c98 100644 --- a/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs +++ b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommand.cs @@ -31,8 +31,8 @@ public record UpdateMatchCommand : IRequest public long LooserPlayerId { get; init; } public DateTime BeginDate { get; init; } public TimeSpan Duration { get; init; } - public Player? LooserPlayer { get; init; } - public Player? WinnerPlayer { get; init; } + //public Player? LooserPlayer { get; init; } + //public Player? WinnerPlayer { get; init; } } public class UpdateMatchCommandHandler : IRequestHandler @@ -51,10 +51,10 @@ public async Task Handle (UpdateMatchCommand request, CancellationToken cancella entity.LooserPlayerId = request.LooserPlayerId; entity.BeginDate = request.BeginDate; entity.Duration = request.Duration; - entity.WinnerPlayer = request.WinnerPlayer; - entity.LooserPlayer = request.LooserPlayer; + //entity.WinnerPlayer = request.WinnerPlayer; + //entity.LooserPlayer = request.LooserPlayer; - entity.AddDomainEvent (new MatchUpdatedEvent (entity)); + //entity.AddDomainEvent (new MatchUpdatedEvent (entity)); await _context.SaveChangesAsync (cancellationToken); } } diff --git a/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs index 0585260..1f16c79 100644 --- a/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs +++ b/src/Application/Matches/Commands/UpdateMatch/UpdateMatchCommandValidator.cs @@ -22,7 +22,8 @@ public class UpdateMatchCommandValidator : AbstractValidator { public UpdateMatchCommandValidator () { - //falta validar los id + RuleFor(v => v.WinnerPlayerId).NotEmpty(); + RuleFor(v => v.LooserPlayerId).NotEmpty(); RuleFor (v => v.BeginDate).NotEmpty (); RuleFor (v => v.Duration).NotEmpty (); } diff --git a/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs b/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs index ca5f68b..7a494a2 100644 --- a/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs +++ b/src/Application/Matches/Queries/GetMatchsWithPagination/GetMatchWithPaginationQuery.cs @@ -26,7 +26,7 @@ namespace DataClash.Application.Matches.Queries.GetMatchesWithPagination { public record GetMatchesWithPaginationQuery : IRequest> { - public int PageNumber { get; init; } = 1;//posible error + public int PageNumber { get; init; } = 1; public int PageSize { get; init; } = 10; } diff --git a/src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs b/src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs index 33bfd7b..e81d957 100644 --- a/src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs +++ b/src/Application/Matches/Queries/GetMatchsWithPagination/MatchBriefDto.cs @@ -21,7 +21,7 @@ namespace DataClash.Application.Matches.Queries.GetMatch { public class MatchBriefDto : IMapFrom { - public long Id { get; init; } + //public long Id { get; init; } public long WinnerPlayerId { get; init; } public long LooserPlayerId { get; init; } public DateTime BeginDate { get; init; } diff --git a/src/Domain/Entities/Match.cs b/src/Domain/Entities/Match.cs index d3f67f4..425a381 100644 --- a/src/Domain/Entities/Match.cs +++ b/src/Domain/Entities/Match.cs @@ -14,17 +14,17 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Domain.Common; namespace DataClash.Domain.Entities { - public class Match + public class Match// : BaseEntity { public long WinnerPlayerId { get; set; } public long LooserPlayerId { get; set; } public DateTime BeginDate { get; set; } public TimeSpan Duration { get; set; } - - public Player LooserPlayer { get; set; } = null!; - public Player WinnerPlayer { get; set; } = null!; + public virtual Player? LooserPlayer { get; set; } + public virtual Player? WinnerPlayer { get; set; } } } diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index 42ddf9a..46d5afe 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -23,8 +23,10 @@ import { LogoutActions } from './services/AuthorizeConstants' import { RequireAuth } from './components/RequireAuth' import { Route, Routes } from 'react-router-dom' import { Players } from './components/Players' +import { Profile } from './components/Profile' import { Wars } from './components/Wars' -import { Matches } from './components/Matches'//NEW +import { Matches } from './components/Matches' +import { Challenges } from './components/Challenges' const loginAction = (name) => () const logoutAction = (name) => () @@ -34,10 +36,11 @@ const AppRoutes = () => ( } index={true} />

Cards component placeholdes

}/> -

Challenges component placeholdes

}/> + }/>

Clans component placeholdes

}/> }/> }/> + }/> }/> diff --git a/src/WebAPI/ClientApp/src/components/Matches.js b/src/WebAPI/ClientApp/src/components/Matches.js index b4c87b6..e3fbe04 100644 --- a/src/WebAPI/ClientApp/src/components/Matches.js +++ b/src/WebAPI/ClientApp/src/components/Matches.js @@ -15,7 +15,7 @@ * along with sep3cs. If not, see . */ import { Button, Table } from 'reactstrap' -import { CreateMatchCommand } from '../webApiClient.ts'//probablemente de error +import { CreateMatchCommand } from '../webApiClient.ts' import { DateTime } from './DateTime.js' import { Pager } from './Pager.js' import { TimeSpan } from './TimeSpan.js' @@ -25,6 +25,7 @@ import { UserRoles } from '../services/AuthorizeConstants.js' import { MatchClient } from '../webApiClient.ts' import authService from '../services/AuthorizeService.ts' import React, { useEffect, useState } from 'react' +import { WaitSpinner } from './WaitSpinner.js' export function Matches () { @@ -32,11 +33,14 @@ export function Matches () const [ activePage, setActivePage ] = useState (initialPage ? initialPage : 0) const [ hasNextPage, setHasNextPage ] = useState (false) const [ hasPreviousPage, setHasPreviousPage ] = useState (false) - const [ isAdministrator, setIsAdministrator ] = useState (false) + const { isAuthorized, inRole }= useAuthorize () const [ isLoading, setIsLoading ] = useState (false) const [ items, setItems ] = useState (undefined) const [ totalPages, setTotalPages ] = useState (0) const [ matchClient ] = useState (new MatchClient ()) + const errorReporter = useErrorReporter () + + const pageSize = 10 const visibleIndices = 5 @@ -44,31 +48,53 @@ export function Matches () const addMatch = async () => { const data = new CreateMatchCommand () - data.winnerPlayerId = 1 - data.looserPlayerId = 2 - data.beginDay = new Date () - data.duration = "00:00:01" - await matchClient.create (data) - setActivePage (-1) + data.winnerPlayerId = 1 + data.looserPlayerId = 2 + data.beginDate = new Date () + data.duration = "00:00:01" + try + { + await matchClient.create (data) + setActivePage (-1) + } + catch(error) + { + errorReporter(error) + } + } const removeMatch = async (item) => { - await matchClient.delete (item.id) - setActivePage (-1) + try + { + await matchClient.delete (item.id) + setActivePage (-1) + } + catch(error) + { + errorReporter(error) + } } const updateMatch = async (item) => { const data = new UpdateMatchCommand () - data.id = item.id - data.winnerPlayerId = item.winnerPlayerId - data.looserPlayerId = item.looserPlayerId - data.beginDay = item.beginDay - data.duration = item.duration - await matchClient.update (item.id, data) + //data.id = item.id + data.winnerPlayerId = item.winnerPlayerId + data.looserPlayerId = item.looserPlayerId + data.beginDate = item.beginDate + data.duration = item.duration + try + { + await matchClient.update (item.id, data) + } + catch(error) + { + errorReporter(error) + } } - +/* useEffect (() => { const checkRole = async () => @@ -81,23 +107,39 @@ export function Matches () setIsLoading (true) checkRole ().then (() => setIsLoading (false)) }, []) - +*/ useEffect (() => { const lastPage = async () => { - const paginatedList = await matchClient.getWithPagination (1, pageSize) - return paginatedList.totalPages + try + { + const paginatedList = await matchClient.getWithPagination (1, pageSize) + return paginatedList.totalPages + } + catch(error) + { + errorReporter(error) + return 0 + } + } const refreshPage = async () => { - const paginatedList = await matchClient.getWithPagination (activePage + 1, pageSize) + try + { + const paginatedList = await matchClient.getWithPagination (activePage + 1, pageSize) - setHasNextPage (paginatedList.hasNextPage) - setHasPreviousPage (paginatedList.hasPreviousPage) - setItems (paginatedList.items) - setTotalPages (paginatedList.totalPages) + setHasNextPage (paginatedList.hasNextPage) + setHasPreviousPage (paginatedList.hasPreviousPage) + setItems (paginatedList.items) + setTotalPages (paginatedList.totalPages) + } + catch(error) + { + errorReporter(error) + } } if (activePage >= 0) @@ -119,8 +161,8 @@ export function Matches () }, [activePage]) return ( - isLoading - ? (
) + isLoading || !isAuthorized + ? () : ( <>
@@ -136,10 +178,9 @@ export function Matches () - - + @@ -147,7 +188,6 @@ export function Matches () { (items ?? []).map ((item, index) => ( - @@ -156,18 +196,18 @@ export function Matches () { - (!isAdministrator) + (!inRole[UserRoles.Administrator]) ? ( { - (!isAdministrator) + (!inRole[UserRoles.Administrator]) ? () : ( diff --git a/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js b/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js index 40f8dcf..bf7bf5d 100644 --- a/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js +++ b/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js @@ -39,7 +39,9 @@ export function AuthorizeProvider (props) const promises = [ authService.isAuthenticated(), authService.getUser() ] const [ isAuthorized, userProfile ] = await Promise.all (promises) - const roles = typeof userProfile.role === 'string' + const roles = !isAuthorized + ? undefined + : typeof userProfile.role === 'string' ? ({ [userProfile.role] : true }) : (Array.isArray (userProfile.role) === false ? ({ }) diff --git a/src/WebAPI/Controllers/MatchController.cs b/src/WebAPI/Controllers/MatchController.cs index 6bedcd9..4228f36 100644 --- a/src/WebAPI/Controllers/MatchController.cs +++ b/src/WebAPI/Controllers/MatchController.cs @@ -25,7 +25,6 @@ namespace DataClash.WebUI.Controllers { - [Authorize] public class MatchController : ApiControllerBase { [HttpGet] @@ -35,27 +34,27 @@ public async Task>> GetWithPagination } [HttpPost] - public async Task> Create (CreateMatchCommand command) + public async Task> Create (CreateMatchCommand command) { return await Mediator.Send (command); } - [HttpDelete ("{id}")] + [HttpDelete ("{(winnerPlayerId,looserPlayerId,beginDate)}")] [ProducesResponseType (StatusCodes.Status204NoContent)] [ProducesDefaultResponseType] - public async Task Delete (long id) + public async Task Delete ((long,long,DateTime) id) { await Mediator.Send (new DeleteMatchCommand (id)); return NoContent (); } - [HttpPut ("{id}")] + [HttpPut ("{(winnerPlayerId,looserPlayerId,beginDate)}")] [ProducesResponseType (StatusCodes.Status204NoContent)] [ProducesResponseType (StatusCodes.Status400BadRequest)] [ProducesDefaultResponseType] - public async Task Update (long id, UpdateMatchCommand command) + public async Task Update ((long,long,DateTime) id, UpdateMatchCommand command) { - if (id != command.Id) + if (id.Item1 != command.WinnerPlayerId || id.Item2 != command.LooserPlayerId || id.Item3 != command.BeginDate) return BadRequest (); await Mediator.Send (command); From 3ebe2408033b6abfcfde826e5c07624ae908c232 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Thu, 30 Nov 2023 19:24:06 -0500 Subject: [PATCH 16/43] Logged off userProfile undefined bug --- src/WebAPI/ClientApp/src/services/AuthorizeProvider.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js b/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js index 40f8dcf..2e67ea1 100644 --- a/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js +++ b/src/WebAPI/ClientApp/src/services/AuthorizeProvider.js @@ -39,7 +39,8 @@ export function AuthorizeProvider (props) const promises = [ authService.isAuthenticated(), authService.getUser() ] const [ isAuthorized, userProfile ] = await Promise.all (promises) - const roles = typeof userProfile.role === 'string' + const roles = !userProfile ? undefined + : typeof userProfile.role === 'string' ? ({ [userProfile.role] : true }) : (Array.isArray (userProfile.role) === false ? ({ }) @@ -68,7 +69,7 @@ export function AuthorizeProvider (props) populateAuthenticationState () - return () => { authService.unsubscribe(subscription) } + return () => { authService.unsubscribe (subscription) } // eslint-disable-next-line react-hooks/exhaustive-deps }, []) From 1951cd3357de1e69b4cfbff67c1e5df5a0778823 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Thu, 30 Nov 2023 19:27:30 -0500 Subject: [PATCH 17/43] Non-player users' profile crash bug --- .../src/components/Profile/Profile.js | 69 ++++++++++--------- 1 file changed, 37 insertions(+), 32 deletions(-) diff --git a/src/WebAPI/ClientApp/src/components/Profile/Profile.js b/src/WebAPI/ClientApp/src/components/Profile/Profile.js index 4a31ff5..83dc7f2 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/Profile.js +++ b/src/WebAPI/ClientApp/src/components/Profile/Profile.js @@ -15,7 +15,7 @@ * along with sep3cs. If not, see . */ import { Avatar } from '../Avatar' -import { Col, Container, Row } from 'reactstrap' +import { Alert, Col, Container, Row } from 'reactstrap' import { Nav, NavItem, NavLink } from 'reactstrap' import { PlayerClient } from '../../webApiClient.ts' import { ProfileClan } from './ProfileClan' @@ -65,36 +65,41 @@ export function Profile () // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthorized]) - return ( - !isAuthorized - ? - : - -
- -
-

{ userProfile.name } { !userProfile.family_name ? null : `(${userProfile.family_name})` }

- Personal profile + if (!isAuthorized) + return + else if (!playerProfile) + return User is not a player + else + return ( + !isAuthorized + ? + : + +
+ +
+

{ userProfile.name } { !userProfile.family_name ? null : `(${userProfile.family_name})` }

+ Personal profile +
-
- - - -
- - - - { pages[activeIndex].component } - - - ) + + + + + + + + { pages[activeIndex].component } + + + ) } From 856ae22fc2220c3de7faffa4b4f832fc9fa54785 Mon Sep 17 00:00:00 2001 From: JuanMiguel01 Date: Thu, 30 Nov 2023 21:07:43 -0500 Subject: [PATCH 18/43] =?UTF-8?q?A=C3=B1adido=20ProfileChallenge=20falta?= =?UTF-8?q?=20que=20se=20a=C3=B1ada?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ClientApp/src/components/Challenges.js | 81 +++++++++++------ .../src/components/Profile/Profile.js | 2 +- .../src/components/Profile/ProfileChalenge.js | 27 ------ .../components/Profile/ProfileChallenge.js | 86 +++++++++++++++++++ src/WebAPI/Pages/Error.cshtml | 26 ++++++ 5 files changed, 167 insertions(+), 55 deletions(-) delete mode 100644 src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js create mode 100644 src/WebAPI/ClientApp/src/components/Profile/ProfileChallenge.js create mode 100644 src/WebAPI/Pages/Error.cshtml diff --git a/src/WebAPI/ClientApp/src/components/Challenges.js b/src/WebAPI/ClientApp/src/components/Challenges.js index de3a744..f0bb9b9 100644 --- a/src/WebAPI/ClientApp/src/components/Challenges.js +++ b/src/WebAPI/ClientApp/src/components/Challenges.js @@ -15,7 +15,7 @@ * along with sep3cs. If not, see . */ import { ApplicationPaths } from '../services/AuthorizeConstants' -import { Button, Table } from 'reactstrap' +import { Button, Table,Input } from 'reactstrap' import { CreateChallengeCommand } from '../webApiClient.ts' import { DateTime } from './DateTime' import { Navigate, useParams } from 'react-router-dom' @@ -26,6 +26,8 @@ import { useAuthorize } from '../services/AuthorizeProvider' import { UserRoles } from '../services/AuthorizeConstants' import { ChallengeClient } from '../webApiClient.ts' import React, { useEffect, useState } from 'react' +import { WaitSpinner } from './WaitSpinner' +import { useErrorReporter } from './ErrorReporter' export function Challenges () { @@ -38,7 +40,8 @@ export function Challenges () const [ items, setItems ] = useState (undefined) const [ totalPages, setTotalPages ] = useState (0) const [ challengeClient ] = useState (new ChallengeClient ()) - + const errorReporter = useErrorReporter () + var error const pageSize = 10 const visibleIndices = 5 @@ -47,21 +50,31 @@ export function Challenges () const data = new CreateChallengeCommand () data.beginDay = new Date () data.duration = "00:00:01" - data.bounty=0 - data.cost=0 + data.bounty=1 + data.cost=1 data.description="" - data.maxLooses=0 - data.minLevel=0 + data.maxLooses=1 + data.minLevel=1 data.name="" - + try{ await challengeClient.create (data) setActivePage (-1) + }catch(error) + { + errorReporter (error) + } } const removeChallenge = async (item) => { + try{ await challengeClient.delete (item.id) setActivePage (-1) + } + catch(error) + { + errorReporter (error) + } } const updateChallenge = async (item) => @@ -76,26 +89,42 @@ export function Challenges () data.maxLooses=item.maxLooses data.minLevel=item.minLevel data.name=item.name - - await challengeClient.update (item.id, data) + try{ + await challengeClient.update (item.id, data) + }catch(error) + { + errorReporter (error) + } } useEffect (() => { const lastPage = async () => { + try{ const paginatedList = await challengeClient.getWithPagination (1, pageSize) return paginatedList.totalPages + } + catch(error) + { + errorReporter (error) + return 0 + } } const refreshPage = async () => { + try{ const paginatedList = await challengeClient.getWithPagination (activePage + 1, pageSize) setHasNextPage (paginatedList.hasNextPage) setHasPreviousPage (paginatedList.hasPreviousPage) setItems (paginatedList.items) setTotalPages (paginatedList.totalPages) + } + catch{ + errorReporter (error) + } } if (activePage >= 0) @@ -117,11 +146,8 @@ export function Challenges () }, [activePage]) return ( - isLoading - ? (
) - : ( - !isAuthorized - ? () + isLoading||!isAuthorized + ? () : ( <>
@@ -167,45 +193,46 @@ export function Challenges () readOnly={!inRole[UserRoles.Administrator]} />
@@ -235,5 +262,5 @@ export function Challenges ()
{'#'} {'Winner player'} {'Looser player'}{'Begin day'}{'Begin date'} {'Duration'}
{ item.id }

{ item.winnerPlayerId }

{ item.beginDay = date; updateMatch (item) }} - readOnly={!isAdministrator} /> + defaultValue={item.beginDate} + onChanged={(date) => { item.beginDate = date; updateMatch (item) }} + readOnly={!inRole[UserRoles.Administrator]} /> { item.duration = span; updateMatch (item) }} - readOnly={!isAdministrator} /> + readOnly={!inRole[UserRoles.Administrator]} /> ) : ( @@ -179,7 +219,7 @@ export function Matches ()
- { item.bounty = number; updateChallenge (item) }} + onChanged={(e) => { e.preventDefault();item.bounty = e.target.value; updateChallenge (item) }} readOnly={!inRole[UserRoles.Administrator]} /> - { item.cost = number; updateChallenge (item) }} + onChanged={(e) => {e.preventDefault(); item.cost = e.target.value; updateChallenge (item) }} readOnly={!inRole[UserRoles.Administrator]} /> - { item.description = string; updateChallenge (item) }} + onChange={(e) => { e.preventDefault (); item.description = e.target.value; updateChallenge (item) }} readOnly={!inRole[UserRoles.Administrator]} /> - { item.maxLooses = number; updateChallenge (item) }} + onChanged={(e) => {e.preventDefault(); item.maxLooses = e.target.value; updateChallenge (item) }} readOnly={!inRole[UserRoles.Administrator]} /> + - { item.minLevel = number; updateChallenge (item) }} + onChanged={(e) => {e.preventDefault(); item.minLevel = e.target.value; updateChallenge (item) }} readOnly={!inRole[UserRoles.Administrator]} /> - { item.name = string; updateChallenge (item) }} + onChanged={(e) => {e.preventDefault(); item.name = e.target.value; updateChallenge (item) }} readOnly={!inRole[UserRoles.Administrator]} />
- ))) + )) } diff --git a/src/WebAPI/ClientApp/src/components/Profile/Profile.js b/src/WebAPI/ClientApp/src/components/Profile/Profile.js index 19d4907..4790253 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/Profile.js +++ b/src/WebAPI/ClientApp/src/components/Profile/Profile.js @@ -19,7 +19,7 @@ import { Col, Container, Row } from 'reactstrap' import { Nav, NavItem, NavLink } from 'reactstrap' import { ProfileIdentity } from './ProfileIdentity' import { ProfilePlayer } from './ProfilePlayer' -import { ProfileChallenge } from './ProfileChalenge' +import { ProfileChallenge } from './ProfileChallenge' import { useAuthorize } from '../../services/AuthorizeProvider' import { WaitSpinner } from '../WaitSpinner' import React, { useState } from 'react' diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js deleted file mode 100644 index 7a90031..0000000 --- a/src/WebAPI/ClientApp/src/components/Profile/ProfileChalenge.js +++ /dev/null @@ -1,27 +0,0 @@ -/* Copyright (c) 2023-2025 - * This file is part of sep3cs. - * - * sep3cs is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * sep3cs is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with sep3cs. If not, see . - */ -import { Button, Form, FormGroup, Input, Label } from 'reactstrap' -import { ChallengeClient,UpdateChallengeCommand } from '../../webApiClient.ts' -import { ProfilePage } from './ProfilePage' -import { useErrorReporter } from '../ErrorReporter' -import { WaitSpinner } from '../WaitSpinner' -import { ChallengeClient } from '../../webApiClient.ts' -import React, { useEffect, useState } from 'react' - -/*necesito mostrar los chalenge existentes y que el usuario elija los que participa */ - -/*obtener todos los chalenge que estan en chalenge client y mostrarlos ponerlos como un boton que cuando seleccionas se añaden a los chalenge de los usuarios */ diff --git a/src/WebAPI/ClientApp/src/components/Profile/ProfileChallenge.js b/src/WebAPI/ClientApp/src/components/Profile/ProfileChallenge.js new file mode 100644 index 0000000..c3e466b --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Profile/ProfileChallenge.js @@ -0,0 +1,86 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +import React, { useEffect, useState } from 'react'; +import { Button, Table, Form, FormGroup, Input, Label } from 'reactstrap'; +import { ChallengeClient, UpdateChallengeCommand } from '../../webApiClient.ts'; +import { useErrorReporter } from '../ErrorReporter.js'; +import { WaitSpinner } from '../WaitSpinner.js'; + +export function ProfileChallenge(props) { + const [challenges, setChallenges] = useState([]); + const [selectedChallenge, setSelectedChallenge] = useState(null); + const client = new ChallengeClient(); + const errorReporter = useErrorReporter(); + + useEffect(() => { + async function fetchChallenges() { + try { + const pageNumber = 1; + const pageSize = 10; + const paginatedChallenges = await client.getWithPagination(pageNumber, pageSize); + const challenges = paginatedChallenges.items; + console.log(challenges); + setChallenges(challenges); + } catch (error) { + errorReporter(error); + } + } + fetchChallenges(); + }, []); + + const handleJoinChallenge = async () => { + if (selectedChallenge) { + try { + await client.joinChallenge(selectedChallenge.id); + alert('Has unido el desafío exitosamente'); + } catch (error) { + errorReporter(error); + } + } + }; + + return ( +
+

Desafíos disponibles

+ + + + + + + + + {challenges.map((challenge) => ( + + + + + + ))} + +
NombreDescripción +
{challenge.name}{challenge.description} + +
+ +
+ ); +} diff --git a/src/WebAPI/Pages/Error.cshtml b/src/WebAPI/Pages/Error.cshtml new file mode 100644 index 0000000..6f92b95 --- /dev/null +++ b/src/WebAPI/Pages/Error.cshtml @@ -0,0 +1,26 @@ +@page +@model ErrorModel +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

+ +@if (Model.ShowRequestId) +{ +

+ Request ID: @Model.RequestId +

+} + +

Development Mode

+

+ Swapping to the Development environment displays detailed information about the error that occurred. +

+

+ The Development environment shouldn't be enabled for deployed applications. + It can result in displaying sensitive information from exceptions to end users. + For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development + and restarting the app. +

From 9d4f798fc704d06ef547ec6199c26f92d3288171 Mon Sep 17 00:00:00 2001 From: JuanMiguel01 Date: Thu, 30 Nov 2023 22:05:50 -0500 Subject: [PATCH 19/43] Faltaba una parte --- src/Application/Common/Seeders/CardsSeederService.cs | 10 +++++----- src/Framework/ConfigureServices.cs | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Application/Common/Seeders/CardsSeederService.cs b/src/Application/Common/Seeders/CardsSeederService.cs index 44fde8c..a22bfc6 100644 --- a/src/Application/Common/Seeders/CardsSeederService.cs +++ b/src/Application/Common/Seeders/CardsSeederService.cs @@ -80,7 +80,7 @@ private void AddCard (IApplicationDbContext context, public void SeedAsync(IApplicationDbContext context) { AddCard(context, "angry_barbarian description", 1, 1, "angry_barbarian", Quality.Normal, "angry_barbarian", 100, 100, 1); - AddCard(context, "archer description", 1, 1, "archer", Quality.Normal, "archer", 100, 100, 1); + AddCard(context, "A pair of lightly armored ranged attackers. They'll help you take down ground and air units, but you're on your own with hair coloring advice.", 3, 1, "archer", Quality.Normal, "archer.png", 42, 119, 2); AddCard(context, "archerqueen description", 1, 1, "archerqueen", Quality.Normal, "archerqueen", 100, 100, 1); AddCard(context, "baby_dragon description", 1, 1, "baby_dragon", Quality.Normal, "baby_dragon", 100, 100, 1); AddCard(context, "bandit description", 1, 1, "bandit", Quality.Normal, "bandit", 100, 100, 1); @@ -121,10 +121,10 @@ public void SeedAsync(IApplicationDbContext context) AddCard(context, "flying_machine description", 1, 1, "flying_machine", Quality.Normal, "flying_machine", 100, 100, 1); AddCard(context, "freeze description", 1, 1, "freeze", Quality.Normal, "freeze", 100, 100, 1); AddCard(context, "ghost description", 1, 1, "ghost", Quality.Normal, "ghost", 100, 100, 1); - AddCard(context, "giant description", 1, 1, "giant", Quality.Normal, "giant", 100, 100, 1); + AddCard(context, "Slow but durable, only attacks buildings. A real one-man wrecking crew!", 5, 1, "giant", Quality.Rare, "giant.png", 120, 1930, 1); AddCard(context, "giant_skeleton description", 1, 1, "giant_skeleton", Quality.Normal, "giant_skeleton", 100, 100, 1); AddCard(context, "goblindrill description", 1, 1, "goblindrill", Quality.Normal, "goblindrill", 100, 100, 1); - AddCard(context, "goblins description", 1, 1, "goblins", Quality.Normal, "goblins", 100, 100, 1); + AddCard(context, " Four fast, unarmored melee attackers. Small, fast, green and mean!", 2, 1, "goblins", Quality.Normal, "goblins", 47, 79, 4); AddCard(context, "goblin_archer description", 1, 1, "goblin_archer", Quality.Normal, "goblin_archer", 100, 100, 1); AddCard(context, "goblin_barrel description", 1, 1, "goblin_barrel", Quality.Normal, "goblin_barrel", 100, 100, 1); AddCard(context, "goblin_cage description", 1, 1, "goblin_cage", Quality.Normal, "goblin_cage", 100, 100, 1); @@ -139,7 +139,7 @@ public void SeedAsync(IApplicationDbContext context) AddCard(context, "icegolem description", 1, 1, "icegolem", Quality.Normal, "icegolem", 100, 100, 1); AddCard(context, "ice_wizard description", 1, 1, "ice_wizard", Quality.Normal, "ice_wizard", 100, 100, 1); AddCard(context, "inferno_dragon description", 1, 1, "inferno_dragon", Quality.Normal, "inferno_dragon", 100, 100, 1); - AddCard(context, "knight description", 1, 1, "knight", Quality.Normal, "knight", 100, 100, 1); + AddCard(context, "A tough melee fighter. The Barbarian's handsome, cultured cousin. Rumor has it that he was knighted based on the sheer awesomeness of his mustache alone.", 1, 1, "knight", Quality.Normal, "knight", 79, 690, 1); AddCard(context, "lava_hound description", 1, 1, "lava_hound", Quality.Normal, "lava_hound", 100, 100, 1); AddCard(context, "lightning description", 1, 1, "lightning", Quality.Normal, "lightning", 100, 100, 1); AddCard(context, "little_prince description", 1, 1, "little_prince", Quality.Normal, "little_prince", 100, 100, 1); @@ -159,7 +159,7 @@ public void SeedAsync(IApplicationDbContext context) AddCard(context, "order_volley description", 1, 1, "order_volley", Quality.Normal, "order_volley", 100, 100, 1); AddCard(context, "party_hut description", 1, 1, "party_hut", Quality.Normal, "party_hut", 100, 100, 1); AddCard(context, "party_rocket description", 1, 1, "party_rocket", Quality.Normal, "party_rocket", 100, 100, 1); - AddCard(context, "pekka description", 1, 1, "pekka", Quality.Normal, "pekka", 100, 100, 1); + AddCard(context, "A heavily armored, slow melee fighter. Swings from the hip, but packs a huge punch.", 7, 1, "pekka", Quality.Uncommon, "pekka", 510,2350, 1); AddCard(context, "phoenix description", 1, 1, "phoenix", Quality.Normal, "phoenix", 100, 100, 1); AddCard(context, "poison description", 1, 1, "poison", Quality.Normal, "poison", 100, 100, 1); AddCard(context, "prince description", 1, 1, "prince", Quality.Normal, "prince", 100, 100, 1); diff --git a/src/Framework/ConfigureServices.cs b/src/Framework/ConfigureServices.cs index b8326d0..376e8c2 100644 --- a/src/Framework/ConfigureServices.cs +++ b/src/Framework/ConfigureServices.cs @@ -44,6 +44,7 @@ public static IServiceCollection AddFrameworkServices (this IServiceCollection s services.AddScoped (provider => provider.GetRequiredService ()); services.AddScoped (); + services.AddScoped (); services .AddDefaultIdentity (options => From c23787c0ab16a0f38a90a4aaf63b9460678d08f4 Mon Sep 17 00:00:00 2001 From: JuanMiguel01 Date: Thu, 30 Nov 2023 22:32:21 -0500 Subject: [PATCH 20/43] PlayerRemovedEvent --- .../Challenges/Commands/AddPlayer/AddPlayerCommand.cs | 2 +- .../Commands/RemovePlayer/RemovePlayerCommandHandler.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs b/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs index 715ce12..9366c55 100644 --- a/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs +++ b/src/Application/Challenges/Commands/AddPlayer/AddPlayerCommand.cs @@ -59,7 +59,7 @@ public async Task Handle (AddPlayerCommand request, CancellationToken cancellati var entity = new PlayerChallenge { ChallengeId = request.ChallengeId, PlayerId = request.PlayerId, WonThrophies=request.WonThrophies, }; - challenge.AddDomainEvent (new PlayerAddedEvent (entity)); + challenge.AddDomainEvent (new PlayerAddedEvent (entity)); _context.PlayerChallenges.Add (entity); await _context.SaveChangesAsync (cancellationToken); diff --git a/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs b/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs index e4be98f..1d9ee68 100644 --- a/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs +++ b/src/Application/Challenges/Commands/RemovePlayer/RemovePlayerCommandHandler.cs @@ -59,7 +59,7 @@ public async Task Handle (RemovePlayerCommand request, CancellationToken cancell ?? throw new NotFoundException (nameof (PlayerChallenge), new object[] { request.ChallengeId, request.PlayerId }); _context.PlayerChallenges.Remove (entity); - challenge.AddDomainEvent (new PlayerRemovedEvent (entity)); + challenge.AddDomainEvent (new PlayerRemovedEvent (entity)); await _context.SaveChangesAsync (cancellationToken); From 1d6fe995bff056746f04f94bfff5905772362e41 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Thu, 30 Nov 2023 22:32:35 -0500 Subject: [PATCH 21/43] Generalizing PlayerAddedEvent and PlayerRemovedEvent --- .../Clan/Command/AddPlayer/AddPlayerCommand.cs | 2 +- .../Command/CreateClan/CreateClanWithChiefCommand.cs | 2 +- .../Command/RemovePlayer/RemovePlayerCommandHandler.cs | 2 +- .../Clan/EventHandlers/PlayerAddedEventHandler.cs | 9 +++++---- .../Clan/EventHandlers/PlayerRemovedEventHandler.cs | 9 +++++---- src/Domain/Events/PlayerAddedEvent.cs | 7 +++---- src/Domain/Events/PlayerRemovedEvent.cs | 7 +++---- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs b/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs index d2de869..66e25aa 100644 --- a/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs +++ b/src/Application/Clan/Command/AddPlayer/AddPlayerCommand.cs @@ -62,7 +62,7 @@ public async Task Handle (AddPlayerCommand request, CancellationToken cancellati { var entity = new PlayerClan { ClanId = request.ClanId, PlayerId = request.PlayerId, Role = request.Role, }; - clan.AddDomainEvent (new PlayerAddedEvent (entity)); + clan.AddDomainEvent (new PlayerAddedEvent (entity)); _context.PlayerClans.Add (entity); await _context.SaveChangesAsync (cancellationToken); diff --git a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs index ca38789..2efbb9e 100644 --- a/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs +++ b/src/Application/Clan/Command/CreateClan/CreateClanWithChiefCommand.cs @@ -72,7 +72,7 @@ public async Task Handle (CreateClanWithChiefCommand request, Cancellation Role = ClanRole.Chief, }; - clanEntity.AddDomainEvent (new PlayerAddedEvent (playerClanEntity)); + clanEntity.AddDomainEvent (new PlayerAddedEvent (playerClanEntity)); _context.PlayerClans.Add (playerClanEntity); await _context.SaveChangesAsync (cancellationToken); diff --git a/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs index 994ef9f..04ceadb 100644 --- a/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs +++ b/src/Application/Clan/Command/RemovePlayer/RemovePlayerCommandHandler.cs @@ -61,7 +61,7 @@ public async Task Handle (RemovePlayerCommand request, CancellationToken cancell ?? throw new NotFoundException (nameof (PlayerClan), new object[] { request.ClanId, request.PlayerId }); _context.PlayerClans.Remove (entity); - clan.AddDomainEvent (new PlayerRemovedEvent (entity)); + clan.AddDomainEvent (new PlayerRemovedEvent (entity)); await _context.SaveChangesAsync (cancellationToken); } diff --git a/src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs b/src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs index bbff507..25295a0 100644 --- a/src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs +++ b/src/Application/Clan/EventHandlers/PlayerAddedEventHandler.cs @@ -14,22 +14,23 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Domain.Entities; using DataClash.Domain.Events; using MediatR; using Microsoft.Extensions.Logging; namespace DataClash.Application.Clans.EventHandlers { - public class PlayerAddedEventHandler : INotificationHandler + public class PlayerAddedEventHandler : INotificationHandler> { - private readonly ILogger _logger; + private readonly ILogger> _logger; - public PlayerAddedEventHandler (ILogger logger) + public PlayerAddedEventHandler (ILogger> logger) { _logger = logger; } - public Task Handle (PlayerAddedEvent notification, CancellationToken cancellationToken) + public Task Handle (PlayerAddedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); return Task.CompletedTask; diff --git a/src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs b/src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs index f033722..b112d3f 100644 --- a/src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs +++ b/src/Application/Clan/EventHandlers/PlayerRemovedEventHandler.cs @@ -14,22 +14,23 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ +using DataClash.Domain.Entities; using DataClash.Domain.Events; using MediatR; using Microsoft.Extensions.Logging; namespace DataClash.Application.Clans.EventHandlers { - public class PlayerRemovedEventHandler : INotificationHandler + public class PlayerRemovedEventHandler : INotificationHandler> { - private readonly ILogger _logger; + private readonly ILogger> _logger; - public PlayerRemovedEventHandler (ILogger logger) + public PlayerRemovedEventHandler (ILogger> logger) { _logger = logger; } - public Task Handle (PlayerRemovedEvent notification, CancellationToken cancellationToken) + public Task Handle (PlayerRemovedEvent notification, CancellationToken cancellationToken) { _logger.LogInformation ("DataClash Domain Event: {DomainEvent}", notification.GetType ().Name); return Task.CompletedTask; diff --git a/src/Domain/Events/PlayerAddedEvent.cs b/src/Domain/Events/PlayerAddedEvent.cs index 67a250e..7c70887 100644 --- a/src/Domain/Events/PlayerAddedEvent.cs +++ b/src/Domain/Events/PlayerAddedEvent.cs @@ -15,15 +15,14 @@ * along with sep3cs. If not, see . */ using DataClash.Domain.Common; -using DataClash.Domain.Entities; namespace DataClash.Domain.Events { - public class PlayerAddedEvent : BaseEvent + public class PlayerAddedEvent : BaseEvent { - public PlayerClan Item { get; } + public T Item { get; } - public PlayerAddedEvent (PlayerClan item) + public PlayerAddedEvent (T item) { Item = item; } diff --git a/src/Domain/Events/PlayerRemovedEvent.cs b/src/Domain/Events/PlayerRemovedEvent.cs index c7046bb..9a280cf 100644 --- a/src/Domain/Events/PlayerRemovedEvent.cs +++ b/src/Domain/Events/PlayerRemovedEvent.cs @@ -15,15 +15,14 @@ * along with sep3cs. If not, see . */ using DataClash.Domain.Common; -using DataClash.Domain.Entities; namespace DataClash.Domain.Events { - public class PlayerRemovedEvent : BaseEvent + public class PlayerRemovedEvent : BaseEvent { - public PlayerClan Item { get; } + public T Item { get; } - public PlayerRemovedEvent (PlayerClan item) + public PlayerRemovedEvent (T item) { Item = item; } From c32da44c3ef4f9e039d77a944eb0647709685d83 Mon Sep 17 00:00:00 2001 From: JuanMiguel01 Date: Thu, 30 Nov 2023 22:57:42 -0500 Subject: [PATCH 22/43] Arreglando tipos --- src/Application/Common/Seeders/CardsSeederService.cs | 4 ++-- src/WebAPI/ClientApp/src/AppRoutes.js | 3 +-- src/WebAPI/ClientApp/src/components/Profile/Profile.js | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Application/Common/Seeders/CardsSeederService.cs b/src/Application/Common/Seeders/CardsSeederService.cs index a22bfc6..9a29043 100644 --- a/src/Application/Common/Seeders/CardsSeederService.cs +++ b/src/Application/Common/Seeders/CardsSeederService.cs @@ -80,7 +80,7 @@ private void AddCard (IApplicationDbContext context, public void SeedAsync(IApplicationDbContext context) { AddCard(context, "angry_barbarian description", 1, 1, "angry_barbarian", Quality.Normal, "angry_barbarian", 100, 100, 1); - AddCard(context, "A pair of lightly armored ranged attackers. They'll help you take down ground and air units, but you're on your own with hair coloring advice.", 3, 1, "archer", Quality.Normal, "archer.png", 42, 119, 2); + AddCard(context, "A pair of lightly armored ranged attackers. They'll help you take down ground and air units, but you're on your own with hair coloring advice.", 3, 1, "archer", Quality.Normal, "archer", 42, 119, 2); AddCard(context, "archerqueen description", 1, 1, "archerqueen", Quality.Normal, "archerqueen", 100, 100, 1); AddCard(context, "baby_dragon description", 1, 1, "baby_dragon", Quality.Normal, "baby_dragon", 100, 100, 1); AddCard(context, "bandit description", 1, 1, "bandit", Quality.Normal, "bandit", 100, 100, 1); @@ -121,7 +121,7 @@ public void SeedAsync(IApplicationDbContext context) AddCard(context, "flying_machine description", 1, 1, "flying_machine", Quality.Normal, "flying_machine", 100, 100, 1); AddCard(context, "freeze description", 1, 1, "freeze", Quality.Normal, "freeze", 100, 100, 1); AddCard(context, "ghost description", 1, 1, "ghost", Quality.Normal, "ghost", 100, 100, 1); - AddCard(context, "Slow but durable, only attacks buildings. A real one-man wrecking crew!", 5, 1, "giant", Quality.Rare, "giant.png", 120, 1930, 1); + AddCard(context, "Slow but durable, only attacks buildings. A real one-man wrecking crew!", 5, 1, "giant", Quality.Rare, "giant", 120, 1930, 1); AddCard(context, "giant_skeleton description", 1, 1, "giant_skeleton", Quality.Normal, "giant_skeleton", 100, 100, 1); AddCard(context, "goblindrill description", 1, 1, "goblindrill", Quality.Normal, "goblindrill", 100, 100, 1); AddCard(context, " Four fast, unarmored melee attackers. Small, fast, green and mean!", 2, 1, "goblins", Quality.Normal, "goblins", 47, 79, 4); diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index dccd796..53ac92f 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -17,12 +17,11 @@ import { ApplicationPaths } from './services/AuthorizeConstants' import { Home } from './components/Home' import { Login } from './components/Login' +import { Players } from './components/Players' import { Profile } from './components/Profile' import { LoginActions } from './services/AuthorizeConstants' import { Logout } from './components/Logout' import { LogoutActions } from './services/AuthorizeConstants' -import { Players } from './components/Players' -import { Profile } from './components/Profile' import { RequireAuth } from './components/RequireAuth' import { Route, Routes } from 'react-router-dom' import { Wars } from './components/Wars' diff --git a/src/WebAPI/ClientApp/src/components/Profile/Profile.js b/src/WebAPI/ClientApp/src/components/Profile/Profile.js index 90ae958..9c5d596 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/Profile.js +++ b/src/WebAPI/ClientApp/src/components/Profile/Profile.js @@ -43,7 +43,7 @@ export function Profile () { title: 'Identity', component: }, { title: 'Player', component: }, { separator : true }, - { title: 'Challenges', component: }, + { title: 'Challenges', component: }, { title: 'Clan', component: }, { title: 'Deck', component: }, ] From 1ecd703a4be0627cab04eb2be08319ad57e87c00 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Thu, 30 Nov 2023 23:13:27 -0500 Subject: [PATCH 23/43] Restore challenges path --- src/WebAPI/ClientApp/src/AppRoutes.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index 53ac92f..139fddd 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -15,13 +15,14 @@ * along with sep3cs. If not, see . */ import { ApplicationPaths } from './services/AuthorizeConstants' +import { Challenges } from './components/Challenges' import { Home } from './components/Home' import { Login } from './components/Login' -import { Players } from './components/Players' -import { Profile } from './components/Profile' import { LoginActions } from './services/AuthorizeConstants' import { Logout } from './components/Logout' import { LogoutActions } from './services/AuthorizeConstants' +import { Players } from './components/Players' +import { Profile } from './components/Profile' import { RequireAuth } from './components/RequireAuth' import { Route, Routes } from 'react-router-dom' import { Wars } from './components/Wars' @@ -35,7 +36,7 @@ const AppRoutes = () => ( } index={true} />

Cards component placeholdes

}/> -

Challenges component placeholdes

}/> + }/>

Clans component placeholdes

}/>

Matches component placeholdes

}/> }/> From 7e3d9e22bb29be90085eedef96de4ddda644c33f Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Thu, 30 Nov 2023 23:36:18 -0500 Subject: [PATCH 24/43] Clans component --- .../GetClansWithPaginationQuery.cs | 2 +- src/WebAPI/ClientApp/src/AppRoutes.js | 4 +- src/WebAPI/ClientApp/src/components/Clans.js | 137 ++++++++++++++++++ 3 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/WebAPI/ClientApp/src/components/Clans.js diff --git a/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs b/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs index 96be960..9eaf6a9 100644 --- a/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs +++ b/src/Application/Clan/Queries/GetClansWithPagination/GetClansWithPaginationQuery.cs @@ -44,7 +44,7 @@ public GetClansWithPaginationQueryHandler (IApplicationDbContext context, IMappe public async Task> Handle (GetClansWithPaginationQuery query, CancellationToken cancellationToken) { - return await _context.Wars + return await _context.Clans .ProjectTo (_mapper.ConfigurationProvider) .PaginatedListAsync (query.PageNumber, query.PageSize); } diff --git a/src/WebAPI/ClientApp/src/AppRoutes.js b/src/WebAPI/ClientApp/src/AppRoutes.js index 139fddd..597df59 100644 --- a/src/WebAPI/ClientApp/src/AppRoutes.js +++ b/src/WebAPI/ClientApp/src/AppRoutes.js @@ -16,6 +16,7 @@ */ import { ApplicationPaths } from './services/AuthorizeConstants' import { Challenges } from './components/Challenges' +import { Clans } from './components/Clans' import { Home } from './components/Home' import { Login } from './components/Login' import { LoginActions } from './services/AuthorizeConstants' @@ -27,7 +28,6 @@ import { RequireAuth } from './components/RequireAuth' import { Route, Routes } from 'react-router-dom' import { Wars } from './components/Wars' - const loginAction = (name) => () const logoutAction = (name) => () @@ -37,7 +37,7 @@ const AppRoutes = () => (

Cards component placeholdes

}/> }/> -

Clans component placeholdes

}/> + }/>

Matches component placeholdes

}/> }/> }/> diff --git a/src/WebAPI/ClientApp/src/components/Clans.js b/src/WebAPI/ClientApp/src/components/Clans.js new file mode 100644 index 0000000..40def71 --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Clans.js @@ -0,0 +1,137 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ +import { ClanClient, ClanType } from '../webApiClient.ts' +import { Pager } from './Pager' +import { Input, Table } from 'reactstrap' +import { useErrorReporter } from './ErrorReporter' +import { useParams } from 'react-router-dom' +import { WaitSpinner } from './WaitSpinner' +import React, { useEffect, useState } from 'react' + +export function Clans () +{ + const { initialPage } = useParams () + const [ activePage, setActivePage ] = useState (initialPage ? initialPage : 0) + const [ hasNextPage, setHasNextPage ] = useState (false) + const [ hasPreviousPage, setHasPreviousPage ] = useState (false) + const [ isLoading, setIsLoading ] = useState (false) + const [ items, setItems ] = useState (undefined) + const [ clanClient ] = useState (new ClanClient ()) + const [ totalPages, setTotalPages ] = useState (0) + const errorReporter = useErrorReporter () + + const pageSize = 10 + const visibleIndices = 5 + + useEffect (() => + { + const lastPage = async () => + { + try { + const paginatedList = await clanClient.getWithPagination (1, pageSize) + return paginatedList.totalPages + } catch (error) + { + errorReporter (error) + } + } + + const refreshPage = async () => + { + try { + const paginatedList = await clanClient.getWithPagination (activePage + 1, pageSize) + + setHasNextPage (paginatedList.hasNextPage) + setHasPreviousPage (paginatedList.hasPreviousPage) + setItems (paginatedList.items) + setTotalPages (paginatedList.totalPages) + } catch (error) + { + errorReporter (error) + } + } + + if (activePage >= 0) + { + setIsLoading (true) + refreshPage ().then (() => setIsLoading (false)) + } + else + { + lastPage ().then ((total) => + { + if (total === 0) + setActivePage (0) + else + setActivePage (total - 1) + }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activePage]) + + return ( + (isLoading) + ? () + : (<> +
+ setActivePage (index)} + totalPages={totalPages} + visibleIndices={visibleIndices} /> +
+ + + + + + + + + + + + + + { (items ?? []).map ((item, index) => ( + + + + + + + + + ))} + +
{'#'}{'Description'}{'Name'}{'Region'}{'Trophies to enter'}{'Trophies won on war'}{'Type'}
{ item.id } + + + + + + + + + + + +
+ )) +} From 1cdf002912dd155dd1b737917b241e56d38d93c9 Mon Sep 17 00:00:00 2001 From: MarcosHCK Date: Fri, 1 Dec 2023 17:32:33 -0500 Subject: [PATCH 25/43] Profile subsections --- .../src/components/Profile/Profile.css | 35 ++++++++ .../src/components/Profile/Profile.js | 79 +++++++++++++++---- 2 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 src/WebAPI/ClientApp/src/components/Profile/Profile.css diff --git a/src/WebAPI/ClientApp/src/components/Profile/Profile.css b/src/WebAPI/ClientApp/src/components/Profile/Profile.css new file mode 100644 index 0000000..963f672 --- /dev/null +++ b/src/WebAPI/ClientApp/src/components/Profile/Profile.css @@ -0,0 +1,35 @@ +/* Copyright (c) 2023-2025 + * This file is part of sep3cs. + * + * sep3cs is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * sep3cs is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with sep3cs. If not, see . + */ + +div.nav-section +div.accordion-item +div.accordion-header +button.accordion-button +{ + background-color: unset; + box-shadow: unset; + padding: 0px; +} + +div.nav-section +div.accordion-item +div.accordion-body +{ + padding-bottom: 0px; + padding-right: 0px; + padding-top: 0px; +} diff --git a/src/WebAPI/ClientApp/src/components/Profile/Profile.js b/src/WebAPI/ClientApp/src/components/Profile/Profile.js index 9c5d596..6f0ada4 100644 --- a/src/WebAPI/ClientApp/src/components/Profile/Profile.js +++ b/src/WebAPI/ClientApp/src/components/Profile/Profile.js @@ -14,15 +14,20 @@ * You should have received a copy of the GNU General Public License * along with sep3cs. If not, see . */ -import { Avatar } from '../Avatar' +import './Profile.css' +import { Accordion } from 'reactstrap' +import { AccordionBody } from 'reactstrap' +import { AccordionHeader } from 'reactstrap' +import { AccordionItem } from 'reactstrap' import { Alert, Col, Container, Row } from 'reactstrap' +import { Avatar } from '../Avatar' import { Nav, NavItem, NavLink } from 'reactstrap' import { PlayerClient } from '../../webApiClient.ts' +import { ProfileChallenge } from './ProfileChallenge' import { ProfileClan } from './ProfileClan' import { ProfileDeck } from './ProfileDeck' import { ProfileIdentity } from './ProfileIdentity' import { ProfilePlayer } from './ProfilePlayer' -import { ProfileChallenge } from './ProfileChallenge' import { useAuthorize } from '../../services/AuthorizeProvider' import { useErrorReporter } from '../ErrorReporter' import { WaitSpinner } from '../WaitSpinner' @@ -31,9 +36,10 @@ import React, { useEffect, useState } from 'react' export function Profile () { const { isAuthorized, userProfile } = useAuthorize () - const [ activeIndex, setActiveIndex ] = useState (0) - const [ playerProfile, setPlayerProfile ] = useState (-1) + const [ activeIndex, setActiveIndex ] = useState (String (0)) + const [ playerProfile, setPlayerProfile ] = useState () const [ playerClient ] = useState (new PlayerClient ()) + const [ sectionOpen, setSectionOpen ] = useState ([]) const errorReporter = useErrorReporter () const downProps = { playerProfile, userProfile } @@ -44,7 +50,11 @@ export function Profile () { title: 'Player', component: }, { separator : true }, { title: 'Challenges', component: }, - { title: 'Clan', component: }, + { title: 'Clan', component: , children : + [ + { title: 'Players', component:

PlayerClan.s

}, + { title: 'Wars', component:

WarClan.s

}, + ]}, { title: 'Deck', component: }, ] @@ -67,6 +77,52 @@ export function Profile () // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthorized]) + const createSection = (page, index) => + { + const createSectionNav = _ => + + { setActiveIndex (index) }}> + { page.title } + + + + if (page.separator) + return
+ else if (!page.children) + return createSectionNav () + else + { + const children = page.children + const target = `accordion${index}` + + const toggle = i => + { + const newOpen = [ ...sectionOpen ] + const oldOpen = sectionOpen[index] + newOpen[index] = i !== oldOpen ? i : undefined + setSectionOpen (newOpen) + } + + return ( + toggle (i)} > + + + { createSectionNav () } + + + + + + ) + } + } + if (!isAuthorized) return else if (!playerProfile) @@ -88,19 +144,12 @@ export function Profile () -