diff --git a/GameOn.Application/FIFA/Tournaments/Commands/UpdateTournamentPicture/UpdateTournamentPictureCommand.cs b/GameOn.Application/FIFA/Tournaments/Commands/UpdateTournamentPicture/UpdateTournamentPictureCommand.cs new file mode 100644 index 0000000..6f4ae1c --- /dev/null +++ b/GameOn.Application/FIFA/Tournaments/Commands/UpdateTournamentPicture/UpdateTournamentPictureCommand.cs @@ -0,0 +1,29 @@ +// +// Copyright (c) LeadOn's Corp'. All rights reserved. +// + +namespace GameOn.Application.FIFA.Tournaments.Commands.UpdateTournamentPicture +{ + using GameOn.Common.DTOs; + using GameOn.Domain; + using MediatR; + using Microsoft.AspNetCore.Http; + + /// + /// UpdateTournamentPictureCommand class. + /// + public class UpdateTournamentPictureCommand : IRequest + { + /// + /// Gets or sets Tournament ID. + /// + public int TournamentId { get; set; } + + /// + /// Gets or sets File. + /// +#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + public IFormFile File { get; set; } +#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable. + } +} diff --git a/GameOn.Application/FIFA/Tournaments/Commands/UpdateTournamentPicture/UpdateTournamentPictureCommandHandler.cs b/GameOn.Application/FIFA/Tournaments/Commands/UpdateTournamentPicture/UpdateTournamentPictureCommandHandler.cs new file mode 100644 index 0000000..b884ccf --- /dev/null +++ b/GameOn.Application/FIFA/Tournaments/Commands/UpdateTournamentPicture/UpdateTournamentPictureCommandHandler.cs @@ -0,0 +1,66 @@ +// +// Copyright (c) LeadOn's Corp'. All rights reserved. +// + +using GameOn.Application.FIFA.Tournaments.Queries.GetTournamentById; + +namespace GameOn.Application.FIFA.Tournaments.Commands.UpdateTournamentPicture +{ + using GameOn.Application.Common.Players.Queries.GetPlayerById; + using GameOn.Common.Exceptions; + using GameOn.Common.Interfaces; + using GameOn.Domain; + using GameOn.External.NetworkStorage.Interfaces; + using MediatR; + using Microsoft.EntityFrameworkCore; + + /// + /// UpdateTournamentPictureCommandHandler class. + /// + public class UpdateTournamentPictureCommandHandler : IRequestHandler + { + private readonly IMediator mediator; + private readonly IApplicationDbContext context; + private readonly INetworkStorageService nsService; + private readonly string bucketName = Environment.GetEnvironmentVariable("S3_BUCKET_NAME") ?? throw new MissingEnvironmentVariableException("S3_BUCKET_NAME"); + private readonly string tpBasePath = Environment.GetEnvironmentVariable("S3_TP_BASE_PATH") ?? throw new MissingEnvironmentVariableException("S3_TP_BASE_PATH"); + + /// + /// Initializes a new instance of the class. + /// + /// DbContext, injected. + /// NetworkStorageService, injected. + /// Mediator, injected. + public UpdateTournamentPictureCommandHandler(IApplicationDbContext context, INetworkStorageService nsService, IMediator mediator) + { + this.context = context; + this.nsService = nsService; + this.mediator = mediator; + } + + /// + public async Task Handle(UpdateTournamentPictureCommand request, CancellationToken cancellationToken) + { + try + { + await this.nsService.UploadFile(this.bucketName, this.tpBasePath + "/" + request.TournamentId + Path.GetExtension(request.File.FileName), request.File); + + // now that file is uploaded, updating user + var tournamentInDb = await this.context.Tournaments.FirstOrDefaultAsync(x => x.Id == request.TournamentId, cancellationToken); + + if (tournamentInDb is not null) + { + tournamentInDb.LogoUrl = request.TournamentId + Path.GetExtension(request.File.FileName); + this.context.Tournaments.Update(tournamentInDb); + await this.context.SaveChangesAsync(cancellationToken); + } + + return true; + } + catch + { + return false; + } + } + } +} diff --git a/GameOn.Application/FIFA/Tournaments/Queries/GetTournamentLogo/GetTournamentLogoQuery.cs b/GameOn.Application/FIFA/Tournaments/Queries/GetTournamentLogo/GetTournamentLogoQuery.cs new file mode 100644 index 0000000..27c305a --- /dev/null +++ b/GameOn.Application/FIFA/Tournaments/Queries/GetTournamentLogo/GetTournamentLogoQuery.cs @@ -0,0 +1,20 @@ +// +// Copyright (c) LeadOn's Corp'. All rights reserved. +// + +namespace GameOn.Application.FIFA.Tournaments.Queries.GetTournamentLogo +{ + using GameOn.Common.DTOs; + using MediatR; + + /// + /// GetTournamentLogoQuery class. + /// + public class GetTournamentLogoQuery : IRequest + { + /// + /// Gets or sets Tournament ID. + /// + public int TournamentId { get; set; } + } +} diff --git a/GameOn.Application/FIFA/Tournaments/Queries/GetTournamentLogo/GetTournamentLogoQueryHandler.cs b/GameOn.Application/FIFA/Tournaments/Queries/GetTournamentLogo/GetTournamentLogoQueryHandler.cs new file mode 100644 index 0000000..a00e01f --- /dev/null +++ b/GameOn.Application/FIFA/Tournaments/Queries/GetTournamentLogo/GetTournamentLogoQueryHandler.cs @@ -0,0 +1,63 @@ +// +// Copyright (c) LeadOn's Corp'. All rights reserved. +// + +using GameOn.Application.FIFA.Tournaments.Queries.GetTournamentById; + +namespace GameOn.Application.FIFA.Tournaments.Queries.GetTournamentLogo +{ + using GameOn.Application.Common.Players.Queries.GetPlayerById; + using GameOn.Common.DTOs; + using GameOn.Common.Exceptions; + using GameOn.External.NetworkStorage.Interfaces; + using MediatR; + + /// + /// GetTournamentLogoQueryHandler class. + /// + public class GetTournamentLogoQueryHandler : IRequestHandler + { + private readonly IMediator mediator; + private readonly INetworkStorageService nsService; + private readonly string bucketName = Environment.GetEnvironmentVariable("S3_BUCKET_NAME") ?? throw new MissingEnvironmentVariableException("S3_BUCKET_NAME"); + private readonly string tpBasePath = Environment.GetEnvironmentVariable("S3_TP_BASE_PATH") ?? throw new MissingEnvironmentVariableException("S3_TP_BASE_PATH"); + + /// + /// Initializes a new instance of the class. + /// + /// NetworkStorageService, injected. + /// Mediator, injected. + public GetTournamentLogoQueryHandler(INetworkStorageService nsService, IMediator mediator) + { + this.nsService = nsService; + this.mediator = mediator; + } + + /// + public async Task Handle(GetTournamentLogoQuery request, CancellationToken cancellationToken) + { + var tpDto = new TournamentLogoDto(); + + // First, getting tournament + var tournamentInDb = await this.mediator.Send(new GetTournamentByIdQuery { TournamentId = request.TournamentId }, cancellationToken); + + if (tournamentInDb is null) + { + return tpDto; + } + + if (tournamentInDb.LogoUrl is null || tournamentInDb.LogoUrl == string.Empty) + { + tpDto.FileName = Environment.GetEnvironmentVariable("DEFAULT_PROFILE_PIC") ?? throw new MissingEnvironmentVariableException("DEFAULT_PROFILE_PIC"); + } + else + { + tpDto.FileName = tournamentInDb.LogoUrl; + } + + // Getting profile picture + tpDto.FileStream = await this.nsService.GetFile(this.bucketName, this.tpBasePath + "/" + tpDto.FileName); + return tpDto; + } + } +} diff --git a/GameOn.Application/GameOn.Application.csproj b/GameOn.Application/GameOn.Application.csproj index c4ae6e0..96277df 100644 --- a/GameOn.Application/GameOn.Application.csproj +++ b/GameOn.Application/GameOn.Application.csproj @@ -9,13 +9,14 @@ Game On! Application LeadOn LeadOn's Corp - Application layer of the Game On! project. + Application layer of the GameOn! app. LeadOn's Corp https://www.valentinvirot.fr gameon-icon.png README.md https://github.com/LeadOn/GameOn-API git + 6.0.0 diff --git a/GameOn.Common/DTOs/TournamentLogoDto.cs b/GameOn.Common/DTOs/TournamentLogoDto.cs new file mode 100644 index 0000000..8f48bcb --- /dev/null +++ b/GameOn.Common/DTOs/TournamentLogoDto.cs @@ -0,0 +1,22 @@ +// +// Copyright (c) LeadOn's Corp'. All rights reserved. +// + +namespace GameOn.Common.DTOs +{ + /// + /// TournamentLogoDto class. + /// + public class TournamentLogoDto + { + /// + /// Gets or sets File Stream. + /// + public Stream? FileStream { get; set; } + + /// + /// Gets or sets File name. + /// + public string FileName { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/GameOn.Common/GameOn.Common.csproj b/GameOn.Common/GameOn.Common.csproj index 8ef22a6..c8ad082 100644 --- a/GameOn.Common/GameOn.Common.csproj +++ b/GameOn.Common/GameOn.Common.csproj @@ -7,7 +7,7 @@ true GameOn! Common LeadOn's Corp - Common utilities used in the GameOn! project. + Common utilities used in the GameOn! app. LeadOn's Corp https://www.valentinvirot.fr LeadOn's Corp @@ -16,6 +16,7 @@ https://github.com/LeadOn/GameOn-API git gameon-icon.ico + 6.0.0 diff --git a/GameOn.Domain/GameOn.Domain.csproj b/GameOn.Domain/GameOn.Domain.csproj index 91f4fbc..60c5ad7 100644 --- a/GameOn.Domain/GameOn.Domain.csproj +++ b/GameOn.Domain/GameOn.Domain.csproj @@ -9,13 +9,14 @@ GameOn! Domain LeadOn LeadOn's Corp - Domain layer of the GameOn! project. + Domain layer of the GameOn! app. LeadOn's Corp https://www.valentinvirot.fr gameon-icon.png README.md https://github.com/LeadOn/GameOn-API git + 6.0.0 diff --git a/GameOn.External/GameOn.External.csproj b/GameOn.External/GameOn.External.csproj index 56bc8b1..f22390b 100644 --- a/GameOn.External/GameOn.External.csproj +++ b/GameOn.External/GameOn.External.csproj @@ -9,13 +9,14 @@ GameOn! External LeadOn LeadOn's Corp - External layer of the GameOn! project. + External layer of the GameOn! app. LeadOn's Corp https://www.valentinvirot.fr gameon-icon.png README.md https://github.com/LeadOn/GameOn-API git + 6.0.0 diff --git a/GameOn.Persistence/GameOn.Persistence.csproj b/GameOn.Persistence/GameOn.Persistence.csproj index 9aa8847..c4471fc 100644 --- a/GameOn.Persistence/GameOn.Persistence.csproj +++ b/GameOn.Persistence/GameOn.Persistence.csproj @@ -9,13 +9,14 @@ GameOn! Persistence LeadOn LeadOn's Corp - Persistance layer of the GameOn! Project + Persistance layer of the GameOn! app. LeadOn's Corp https://www.valentinvirot.fr gameon-icon.png README.md https://github.com/LeadOn/GameOn-API git + 6.0.0 diff --git a/GameOn.Presentation/Controllers/FIFA/TournamentController.cs b/GameOn.Presentation/Controllers/FIFA/TournamentController.cs index c2968bb..bbbc4fd 100644 --- a/GameOn.Presentation/Controllers/FIFA/TournamentController.cs +++ b/GameOn.Presentation/Controllers/FIFA/TournamentController.cs @@ -13,12 +13,15 @@ namespace GameOn.Presentation.Controllers.FIFA using GameOn.Application.FIFA.Tournaments.Commands.SavePhase1Score; using GameOn.Application.FIFA.Tournaments.Commands.SubscribeTournament; using GameOn.Application.FIFA.Tournaments.Commands.UpdateTournament; + using GameOn.Application.FIFA.Tournaments.Commands.UpdateTournamentPicture; using GameOn.Application.FIFA.Tournaments.Queries.CheckTournamentSubscription; using GameOn.Application.FIFA.Tournaments.Queries.GetAllTournaments; using GameOn.Application.FIFA.Tournaments.Queries.GetFeaturedTournaments; using GameOn.Application.FIFA.Tournaments.Queries.GetTournamentById; + using GameOn.Application.FIFA.Tournaments.Queries.GetTournamentLogo; using GameOn.Common.DTOs; using GameOn.Domain; + using GameOn.External.NetworkStorage.Interfaces; using GameOn.Presentation.Classes; using MediatR; using Microsoft.AspNetCore.Authorization; @@ -33,14 +36,17 @@ namespace GameOn.Presentation.Controllers.FIFA public class TournamentController : ControllerBase { private readonly ISender mediator; + private readonly INetworkStorageService nsService; /// /// Initializes a new instance of the class. /// /// MediatR interface, injected. - public TournamentController(ISender mediator) + /// NetworkStorage interface, injected. + public TournamentController(ISender mediator, INetworkStorageService nsService) { this.mediator = mediator; + this.nsService = nsService; } /// @@ -331,5 +337,65 @@ public async Task Delete(int id) return this.NoContent(); } } + + /// + /// Upload tournament picture. + /// + /// Tournament ID. + /// Tournament picture file. + /// IActionResult. + [HttpPost] + [Authorize(Roles = "gameon_admin")] + [Route("{tournamentId}/logo")] + [SwaggerOperation(Summary = "Updates tournament picture.")] + [SwaggerResponse(201, "Tournament's picture created on the server.")] + [SwaggerResponse(401, "Unauthorized.")] + [SwaggerResponse(406, "Wrong file format.")] + [SwaggerResponse(404, "No profile picture found.")] + [SwaggerResponse(500, "Unknown error happened.")] + public async Task UpdateTournamentPicture(int tournamentId, IFormFile tournamentPicture) + { + if (!CheckIfPictureFormatIsAuthorized(tournamentPicture.ContentType)) + { + return this.StatusCode(406); + } + + var currentPlayer = await this.mediator.Send(new UpdateTournamentPictureCommand { TournamentId = tournamentId, File = tournamentPicture }); + + return currentPlayer ? this.Created() : this.Problem(); + } + + /// + /// Gets tournament logo from network attached storage. + /// + /// Tournament ID. + /// File. + [HttpGet] + [Route("{tournamentId}/logo")] + [SwaggerOperation(Summary = "Get tournament logo from server.")] + [SwaggerResponse(200, "Tournament logo from server.", typeof(FileStreamResult))] + [SwaggerResponse(404, "No tournament logo found.")] + [SwaggerResponse(500, "Unknown error happened.")] + public async Task GetTournamentLogo(int tournamentId) + { + var tpDto = await this.mediator.Send(new GetTournamentLogoQuery { TournamentId = tournamentId }); + + if (tpDto.FileStream is null) + { + return this.NotFound(); + } + + return this.File(tpDto.FileStream, this.nsService.GetContentType(tpDto.FileName)); + } + + /// + /// Checks if file type is valid. + /// + /// File content type. + /// True if valid, false if not. + private static bool CheckIfPictureFormatIsAuthorized(string contentType) + { + return contentType is "image/jpeg" or "image/png" or "image/gif" or "image/webp"; + } } } diff --git a/GameOn.Presentation/GameOn.Presentation.csproj b/GameOn.Presentation/GameOn.Presentation.csproj index 19cd582..f0c0258 100644 --- a/GameOn.Presentation/GameOn.Presentation.csproj +++ b/GameOn.Presentation/GameOn.Presentation.csproj @@ -9,13 +9,14 @@ GameOn! API LeadOn LeadOn's Corp - Back-end of the GameOn! project. + Back-end of the GameOn! app. LeadOn's Corp https://www.valentinvirot.fr gameon-icon.png README.md https://github.com/LeadOn/GameOn-API git + 6.0.0 diff --git a/GameOn.Presentation/Properties/launchSettings.json b/GameOn.Presentation/Properties/launchSettings.json index 0796a9d..2a35ae1 100644 --- a/GameOn.Presentation/Properties/launchSettings.json +++ b/GameOn.Presentation/Properties/launchSettings.json @@ -20,6 +20,7 @@ "S3_SECRET_KEY": "", "S3_BUCKET_NAME": "", "S3_PP_BASE_PATH": "", + "S3_TP_BASE_PATH": "", "DEFAULT_PROFILE_PIC": "" }, "dotnetRunMessages": true, diff --git a/README.md b/README.md index 60b0c03..38e3b6d 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Here are all of the environment variables currently used :
  • S3_SECRET_KEY: S3 Bucket Secret key.
  • S3_BUCKET_NAME: S3 Bucket Name.
  • S3_PP_BASE_PATH: Base path to profile pictures in S3 bucket.
  • +
  • S3_TP_BASE_PATH: Base path to tournament pictures in S3 bucket.
  • DEFAULT_PROFILE_PIC: Default profile picture name.