Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin notes/tournament endpoints #433

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions API/Configurations/MapperProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ public class MapperProfile : Profile
{
public MapperProfile()
{
CreateMap<AdminNoteEntityBase, AdminNoteDTO>()
.ForMember(x => x.AdminUsername,
opt => opt.MapFrom(x => x.AdminUser.Player == null ? null : x.AdminUser.Player.Username));

CreateMap<Beatmap, BeatmapDTO>();
CreateMap<Game, GameDTO>();
CreateMap<GameWinRecord, GameWinRecordDTO>();
Expand Down
162 changes: 162 additions & 0 deletions API/Controllers/TournamentsController.Admin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
using API.DTOs;
using API.Utilities;
using API.Utilities.Extensions;
using Database.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace API.Controllers;

public partial class TournamentsController
{
/// <summary>
/// Creates an admin note for a tournament
/// </summary>
/// <param name="id">Tournament id</param>
/// <response code="401">If the requester is not properly authorized</response>
/// <response code="404">If a tournament matching the given id does not exist</response>
/// <response code="400">If the authorized user does not exist</response>
/// <response code="200">Returns the created admin note</response>
[HttpPost("{id:int}/notes")]
[Authorize(Roles = $"{OtrClaims.Admin}")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType<AdminNoteDTO>(StatusCodes.Status200OK)]
public async Task<IActionResult> CreateAdminNoteAsync(int id, [FromBody] string note)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In testing, I hard-coded my user id (20). The adminUsername field returned an empty string. From what I can see in the current code, there isn't a way for the CreateAsync() method to link back to a user.
CleanShot 2024-09-28 at 11 48 20@2x

{
var adminUserId = User.AuthorizedIdentity();
if (!adminUserId.HasValue)
{
return Unauthorized();
}

if (!await tournamentsService.ExistsAsync(id))
{
return NotFound();
}

AdminNoteDTO? result = await adminNoteService.CreateAsync<TournamentAdminNote>(id, adminUserId.Value, note);
return result is not null
? Ok(result)
: BadRequest();
}

/// <summary>
/// List all admin notes from a tournament
/// </summary>
/// <param name="id">Tournament id</param>
/// <response code="404">If a tournament matching the given id does not exist</response>
/// <response code="200">Returns all admin notes from a tournament</response>
[HttpGet("{id:int}/notes")]
[Authorize(Roles = $"{OtrClaims.Admin}")]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType<IEnumerable<AdminNoteDTO>>(StatusCodes.Status200OK)]
public async Task<IActionResult> ListAdminNotesAsync(int id)
{
if (!await tournamentsService.ExistsAsync(id))
{
return NotFound();
}

return Ok(await adminNoteService.GetAsync<TournamentAdminNote>(id));
}

/// <summary>
/// Updates an admin note for a tournament
/// </summary>
/// <param name="id">Tournament id</param>
/// <param name="noteId">Admin note id</param>
/// <response code="401">If the requester is not properly authorized</response>
/// <response code="404">
/// If a tournament matching the given id does not exist.
/// If an admin note matching the given noteId does not exist
/// </response>
/// <response code="403">If the requester did not create the admin note</response>
/// <response code="200">Returns the updated admin note</response>
[HttpPatch("{id:int}/notes/{noteId:int}")]
[Authorize(Roles = $"{OtrClaims.Admin}")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType<AdminNoteDTO>(StatusCodes.Status200OK)]
public async Task<IActionResult> UpdateAdminNoteAsync(int id, int noteId, [FromBody] string note)
{
var adminUserId = User.AuthorizedIdentity();
if (!adminUserId.HasValue)
{
return Unauthorized();
}

if (!await tournamentsService.ExistsAsync(id))
{
return NotFound();
}

AdminNoteDTO? existingNote = await adminNoteService.GetAsync<TournamentAdminNote>(noteId);
if (existingNote is null)
{
return NotFound();
}

if (adminUserId != existingNote.AdminUserId)
{
return Forbid();
}

existingNote.Note = note;
AdminNoteDTO? result = await adminNoteService.UpdateAsync<TournamentAdminNote>(existingNote);

return result is not null
? Ok(result)
: NotFound();
}

/// <summary>
/// Deletes an admin note for a tournament
/// </summary>
/// <param name="id">Tournament id</param>
/// <param name="noteId">Admin note id</param>
/// <response code="401">If the requester is not properly authorized</response>
/// <response code="404">
/// If a tournament matching the given id does not exist.
/// If an admin note matching the given noteId does not exist
/// </response>
/// <response code="403">If the requester did not create the admin note</response>
/// <response code="200">Returns the updated admin note</response>
[HttpDelete("{id:int}/notes/{noteId:int}")]
[Authorize(Roles = $"{OtrClaims.Admin}")]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType<AdminNoteDTO>(StatusCodes.Status200OK)]
public async Task<IActionResult> DeleteAdminNoteAsync(int id, int noteId)
{
var adminUserId = User.AuthorizedIdentity();
if (!adminUserId.HasValue)
{
return Unauthorized();
}

if (!await tournamentsService.ExistsAsync(id))
{
return NotFound();
}

AdminNoteDTO? existingNote = await adminNoteService.GetAsync<TournamentAdminNote>(noteId);
if (existingNote is null)
{
return NotFound();
}

if (adminUserId != existingNote.AdminUserId)
{
return Forbid();
}

var result = await adminNoteService.DeleteAsync<TournamentAdminNote>(noteId);
return result
? Ok()
: NotFound();
}
}
5 changes: 4 additions & 1 deletion API/Controllers/TournamentsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ namespace API.Controllers;
[ApiController]
[ApiVersion(1)]
[Route("api/v{version:apiVersion}/[controller]")]
public class TournamentsController(ITournamentsService tournamentsService) : Controller
public partial class TournamentsController(
ITournamentsService tournamentsService,
IAdminNoteService adminNoteService
) : Controller
{
/// <summary>
/// List all tournaments
Expand Down
42 changes: 42 additions & 0 deletions API/DTOs/AdminNoteDTO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace API.DTOs;

/// <summary>
/// Represents a note for an entity created by an admin
/// </summary>
public class AdminNoteDTO
{
/// <summary>
/// Id
hburn7 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public int Id { get; init; }

/// <summary>
/// Timestamp of creation
/// </summary>
public DateTime Created { get; init; }

/// <summary>
/// Timestamp of the last update if available
/// </summary>
public DateTime? Updated { get; init; }

/// <summary>
/// Id of the parent entity
/// </summary>
public int ReferenceId { get; init; }

/// <summary>
/// Id of the admin user that created the note
/// </summary>
public int AdminUserId { get; set; }

/// <summary>
/// Username of the admin user that created the note
/// </summary>
public string? AdminUsername { get; init; }

/// <summary>
/// Content of the note
/// </summary>
public string Note { get; set; } = string.Empty;
}
2 changes: 2 additions & 0 deletions API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ await context.HttpContext.Response.WriteAsync(
builder.Services.AddScoped<IApiPlayerMatchStatsRepository, ApiPlayerMatchStatsRepository>();
builder.Services.AddScoped<IApiTournamentsRepository, ApiTournamentsRepository>();

builder.Services.AddScoped<IAdminNoteRepository, AdminNoteRepository>();
builder.Services.AddScoped<IBaseStatsRepository, BaseStatsRepository>();
builder.Services.AddScoped<IBeatmapsRepository, BeatmapsRepository>();
builder.Services.AddScoped<IGamesRepository, GamesRepository>();
Expand All @@ -446,6 +447,7 @@ await context.HttpContext.Response.WriteAsync(

#region Services

builder.Services.AddScoped<IAdminNoteService, AdminNoteService>();
builder.Services.AddScoped<IBaseStatsService, BaseStatsService>();
builder.Services.AddScoped<IBeatmapService, BeatmapService>();
builder.Services.AddScoped<IGamesService, GamesService>();
Expand Down
67 changes: 67 additions & 0 deletions API/Services/Implementations/AdminNoteService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
using API.DTOs;
using API.Services.Interfaces;
using AutoMapper;
using Database.Entities;
using Database.Repositories.Interfaces;

namespace API.Services.Implementations;

public class AdminNoteService(
IAdminNoteRepository adminNoteRepository,
IUserRepository userRepository,
IMapper mapper
) : IAdminNoteService
{
public async Task<AdminNoteDTO?> CreateAsync<TAdminNote>(int referenceId, int adminUserId, string note)
where TAdminNote : AdminNoteEntityBase, new()
{
if (!await userRepository.ExistsAsync(adminUserId))
{
return null;
}

var entity = new TAdminNote
{
ReferenceId = referenceId,
AdminUserId = adminUserId,
Note = note
};

await adminNoteRepository.CreateAsync(entity);
return mapper.Map<AdminNoteDTO>(entity);
}

public async Task<AdminNoteDTO?> GetAsync<TAdminNote>(int id) where TAdminNote : AdminNoteEntityBase =>
mapper.Map<AdminNoteDTO>(await adminNoteRepository.GetAsync<TAdminNote>(id));

public async Task<IEnumerable<AdminNoteDTO>> ListAsync<TAdminNote>(int referenceId) where TAdminNote : AdminNoteEntityBase =>
mapper.Map<IEnumerable<AdminNoteDTO>>(await adminNoteRepository.ListAsync<TAdminNote>(referenceId));

public async Task<AdminNoteDTO?> UpdateAsync<TAdminNote>(AdminNoteDTO updatedNote) where TAdminNote : AdminNoteEntityBase
{
TAdminNote? adminNote = await adminNoteRepository.GetAsync<TAdminNote>(updatedNote.Id);

if (adminNote is null)
{
return null;
}

adminNote.Note = updatedNote.Note;
await adminNoteRepository.UpdateAsync(adminNote);

return mapper.Map<AdminNoteDTO>(adminNote);
}

public async Task<bool> DeleteAsync<TAdminNote>(int id) where TAdminNote : AdminNoteEntityBase
{
TAdminNote? adminNote = await adminNoteRepository.GetAsync<TAdminNote>(id);

if (adminNote is null)
{
return false;
}

await adminNoteRepository.DeleteAsync(adminNote);
return true;
}
}
3 changes: 3 additions & 0 deletions API/Services/Implementations/TournamentsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ bool preApprove
return mapper.Map<TournamentCreatedResultDTO>(tournament);
}

public async Task<bool> ExistsAsync(int id) =>
await tournamentsRepository.ExistsAsync(id);

public async Task<bool> ExistsAsync(string name, Ruleset ruleset)
=> await tournamentsRepository.ExistsAsync(name, ruleset);

Expand Down
59 changes: 59 additions & 0 deletions API/Services/Interfaces/IAdminNoteService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using API.DTOs;
using Database.Entities;

namespace API.Services.Interfaces;

public interface IAdminNoteService
{
/// <summary>
/// Creates a <typeparamref name="TAdminNote"/>
/// </summary>
/// <param name="referenceId">Id of the parent entity</param>
/// <param name="adminUserId">Id of the admin <see cref="User"/> creating the <typeparamref name="TAdminNote"/></param>
/// <param name="note">Content of the <typeparamref name="TAdminNote"/></param>
/// <typeparam name="TAdminNote">The type of admin note being created</typeparam>
/// <returns>
/// The created <see cref="AdminNoteDTO"/> if successful, or null if a <see cref="User"/> for the
/// given <paramref name="adminUserId"/> does not exist
/// </returns>
/// <remarks>
/// This method checks for existence of the admin <see cref="User"/>, but checking for existence of the parent
/// entity should be handled by the caller
/// </remarks>
Task<AdminNoteDTO?> CreateAsync<TAdminNote>(int referenceId, int adminUserId, string note)
where TAdminNote : AdminNoteEntityBase, new();

/// <summary>
/// Gets an <see cref="AdminNoteDTO"/>
/// </summary>
/// <param name="id">Id of the <typeparamref name="TAdminNote"/></param>
/// <typeparam name="TAdminNote">The type of admin note being retrieved</typeparam>
/// <returns>The <see cref="AdminNoteDTO"/>, or null if not found</returns>
Task<AdminNoteDTO?> GetAsync<TAdminNote>(int id) where TAdminNote : AdminNoteEntityBase;

/// <summary>
/// Gets a collection of <see cref="AdminNoteDTO"/>s by their parent reference Id.
/// </summary>
/// <param name="referenceId">Id of the parent entity</param>
/// <typeparam name="TAdminNote">The type of admin note being retrieved</typeparam>
/// <returns>A collection of <see cref="AdminNoteDTO"/>s for the given referenceId</returns>
Task<IEnumerable<AdminNoteDTO>> ListAsync<TAdminNote>(int referenceId) where TAdminNote : AdminNoteEntityBase;

/// <summary>
/// Updates the <see cref="AdminNoteEntityBase.Note"/> of a <typeparamref name="TAdminNote"/>
/// </summary>
/// <param name="updatedNote">The updated admin note</param>
/// <typeparam name="TAdminNote">The type of admin note being updated</typeparam>
/// <returns>The updated <see cref="AdminNoteDTO"/>, or null if not found</returns>
Task<AdminNoteDTO?> UpdateAsync<TAdminNote>(AdminNoteDTO updatedNote) where TAdminNote : AdminNoteEntityBase;

/// <summary>
/// Deletes a <typeparamref name="TAdminNote"/>
/// </summary>
/// <param name="id">Id of the <typeparamref name="TAdminNote"/></param>
/// <typeparam name="TAdminNote">The type of admin note being deleted</typeparam>
/// <returns>
/// True if successful, false if a <see cref="AdminNoteDTO"/> for the given id does not exist
/// </returns>
Task<bool> DeleteAsync<TAdminNote>(int id) where TAdminNote : AdminNoteEntityBase;
}
6 changes: 6 additions & 0 deletions API/Services/Interfaces/ITournamentsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ Task<TournamentCreatedResultDTO> CreateAsync(
bool preApprove
);

/// <summary>
/// Denotes a tournament exists for the given id
/// </summary>
/// <param name="id">Tournament id</param>
Task<bool> ExistsAsync(int id);

/// <summary>
/// Denotes a tournament with matching name and ruleset exists
/// </summary>
Expand Down
Loading
Loading