diff --git a/WowsKarma.Api/Controllers/AuthController.cs b/WowsKarma.Api/Controllers/AuthController.cs index 9bc28a8..e0dc35a 100644 --- a/WowsKarma.Api/Controllers/AuthController.cs +++ b/WowsKarma.Api/Controllers/AuthController.cs @@ -35,8 +35,8 @@ public AuthController(IConfiguration config, UserService userService, WargamingA /// /// Authentication successful. /// Authentication failed. - [HttpHead, Authorize, ProducesResponseType(200), ProducesResponseType(401)] - public IActionResult ValidateAuth() => StatusCode(200); + [HttpHead, Authorize] + public ActionResult ValidateAuth() => Ok(); /// /// Provides redirection to Wargaming OpenID Authentication. @@ -52,13 +52,13 @@ public AuthController(IConfiguration config, UserService userService, WargamingA /// Authentication successful. /// Invalid callback request. [HttpGet("wg-callback"), ProducesResponseType(302), ProducesResponseType(200), ProducesResponseType(403)] - public async Task WgAuthCallback() + public async Task WgAuthCallbackAsync() { bool valid = await _wargamingAuthService.VerifyIdentity(Request); if (!valid) { - return StatusCode(403); + return Forbid(); } JwtSecurityToken token = await _userService.CreateTokenAsync(WargamingIdentity.FromUri(new(Request.Query["openid.identity"].FirstOrDefault() @@ -89,10 +89,9 @@ public async Task WgAuthCallback() /// Seed Token successfully reset. /// Authentication failed. [HttpPost("renew-seed"), Authorize, ProducesResponseType(200), ProducesResponseType(401)] - public async Task RenewSeed() + public async Task RenewSeed() { await _userService.RenewSeedTokenAsync(uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))); - return Ok(); } /// @@ -101,9 +100,9 @@ public async Task RenewSeed() /// Token successfully refreshed. /// Authentication failed. [HttpGet("refresh-token"), Authorize, ProducesResponseType(typeof(string), 200), ProducesResponseType(401)] - public async Task RefreshToken() + public async Task RefreshToken() { JwtSecurityToken token = await _userService.CreateTokenAsync(new(User.Claims)); - return StatusCode(200, _jwtService.TokenHandler.WriteToken(token)); + return _jwtService.TokenHandler.WriteToken(token); } } diff --git a/WowsKarma.Api/Controllers/PlayerController.cs b/WowsKarma.Api/Controllers/PlayerController.cs index 9a57cd9..d9ffb43 100644 --- a/WowsKarma.Api/Controllers/PlayerController.cs +++ b/WowsKarma.Api/Controllers/PlayerController.cs @@ -1,3 +1,4 @@ +using System.Collections; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; @@ -23,7 +24,7 @@ public PlayerController(PlayerService playerService) /// /// A list of all players in the database. /// Returns all players in the database. - [HttpGet, ProducesResponseType(typeof(IEnumerable), 200)] + [HttpGet] public IAsyncEnumerable ListPlayers() => _playerService.ListPlayerIds(); /// @@ -34,7 +35,7 @@ public PlayerController(PlayerService playerService) /// Account listings for given search query /// No results found for given search query [HttpGet("search/{query}"), ProducesResponseType(typeof(IEnumerable), 200), ProducesResponseType(204)] - public async Task SearchAccount([StringLength(100, MinimumLength = 3), RegularExpression(@"^[a-zA-Z0-9_]*$")] string query) + public async Task> SearchAccount([StringLength(100, MinimumLength = 3), RegularExpression(@"^[a-zA-Z0-9_]*$")] string query) => await _playerService.ListPlayersAsync(query) is { Length: not 0 } accounts ? Ok(accounts) : NoContent(); @@ -46,12 +47,13 @@ public async Task SearchAccount([StringLength(100, MinimumLength /// Include clan membership info while fetching player profile. /// Returns player profile /// No profile found - [HttpGet("{id}"), ProducesResponseType(typeof(PlayerProfileDTO), 200), ProducesResponseType(204)] - public async Task GetAccount(uint id, bool includeClanInfo = true) + [HttpGet("{id}")] + public async Task> GetAccount(uint id, bool includeClanInfo = true) { if (id is 0) { - return BadRequest(new ArgumentException(null, nameof(id))); + ModelState.AddModelError(nameof(id), "Account ID cannot be zero."); + return BadRequest(ModelState); } Player? playerProfile = await _playerService.GetPlayerAsync(id, false, includeClanInfo); @@ -67,15 +69,15 @@ public async Task GetAccount(uint id, bool includeClanInfo = true /// List of Account IDs /// Returns "Account":"SiteKarma" Dictionary of Karma metrics for available accounts (may be empty). [HttpPost("karmas"), ProducesResponseType(typeof(Dictionary), 200)] - public IActionResult FetchKarmas([FromBody] uint[] ids) => Ok(AccountKarmaDTO.ToDictionary(_playerService.GetPlayersKarma(ids))); + public Dictionary FetchKarmas([FromBody] uint[] ids) => AccountKarmaDTO.ToDictionary(_playerService.GetPlayersKarma(ids)); /// /// Fetches full Karma metrics (Site Karma and Flairs) for each provided Account ID, where available. /// /// List of Account IDs /// Returns Full Karma metrics for available accounts (may be empty). - [HttpPost("karmas-full"), ProducesResponseType(typeof(IEnumerable), 200)] - public IActionResult FetchFullKarmas([FromBody] uint[] ids) => Ok(_playerService.GetPlayersFullKarma(ids)); + [HttpPost("karmas-full")] + public IEnumerable FetchFullKarmas([FromBody] uint[] ids) => _playerService.GetPlayersFullKarma(ids); /// /// Triggers recalculation of Karma metrics for a given account. @@ -86,8 +88,10 @@ public async Task GetAccount(uint id, bool includeClanInfo = true /// Account ID of player profile /// /// Profile Karma recalculation was processed. - [HttpPatch("recalculate"), Authorize(Roles = ApiRoles.Administrator), ProducesResponseType(205), ProducesResponseType(401), ProducesResponseType(403)] - public IActionResult RecalculateMetrics([FromQuery] uint playerId, CancellationToken ct) + /// Unauthorized + /// Forbidden + [HttpPatch("recalculate"), Authorize(Roles = ApiRoles.Administrator)] + public AcceptedResult RecalculateMetrics([FromQuery] uint playerId, CancellationToken ct) { BackgroundJob.Enqueue(p => p.RecalculatePlayerMetrics(playerId, ct)); return Accepted(); diff --git a/WowsKarma.Api/Controllers/PostController.cs b/WowsKarma.Api/Controllers/PostController.cs index 7f63cfa..5d660c0 100644 --- a/WowsKarma.Api/Controllers/PostController.cs +++ b/WowsKarma.Api/Controllers/PostController.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Mvc; using System.Security.Claims; using System.Text.Json; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.EntityFrameworkCore; using WowsKarma.Api.Infrastructure.Attributes; using WowsKarma.Api.Infrastructure.Data; @@ -44,8 +45,8 @@ public PostController(PlayerService playerService, PostService postService, ILog /// Returns object of Post with specified ID /// No post was found with given ID. /// Post is locked by Community Managers. - [HttpGet("{postId:guid}"), ProducesResponseType(typeof(PlayerPostDTO), 200), ProducesResponseType(404), ProducesResponseType(410)] - public async Task GetPostAsync(Guid postId) + [HttpGet("{postId:guid}")] + public async Task> GetPostAsync(Guid postId) => await _postService.GetPostDTOAsync(postId) is { } post ? !post.ModLocked || post.Author.Id == User.ToAccountListing()?.Id || User.IsInRole(ApiRoles.CM) ? Ok(post) @@ -60,8 +61,8 @@ public async Task GetPostAsync(Guid postId) /// Number of posts per page /// List of posts received by player. /// No player found for given Account ID. - [HttpGet("{userId}/received"), ProducesResponseType(typeof(IEnumerable), 200)] - public IActionResult GetReceivedPosts( + [HttpGet("{userId}/received")] + public ActionResult GetReceivedPosts( [FromRoute] uint userId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10 @@ -89,7 +90,7 @@ public IActionResult GetReceivedPosts( } } - return Ok(pageResults.Items.Adapt>()); + return Ok(pageResults.Items.Adapt()); } /// @@ -101,8 +102,8 @@ public IActionResult GetReceivedPosts( /// List of posts sent by player /// No posts sent by given player. /// No player found for given Account ID. - [HttpGet("{userId}/sent"), ProducesResponseType(typeof(IEnumerable), 200), ProducesResponseType(204), ProducesResponseType(typeof(string), 404)] - public IActionResult GetSentPosts( + [HttpGet("{userId}/sent")] + public ActionResult GetSentPosts( [FromRoute] uint userId, [FromQuery] int page = 1, [FromQuery] int pageSize = 10 @@ -129,7 +130,7 @@ public IActionResult GetSentPosts( } } - return Ok(pageResults.Items.Adapt>()); + return Ok(pageResults.Items.Adapt()); } /// @@ -140,8 +141,8 @@ public IActionResult GetSentPosts( /// Filters returned posts by Replay attachment. /// Hides posts containing Mod Actions (visible only to CMs). /// List of latest posts, sorted by Submission time. - [HttpGet("latest"), ProducesResponseType(typeof(IEnumerable), 200)] - public IActionResult GetLatestPosts( + [HttpGet("latest")] + public ActionResult GetLatestPosts( [FromQuery] int page = 1, [FromQuery] int pageSize = 10, [FromQuery] bool? hasReplay = null, @@ -182,7 +183,7 @@ public IActionResult GetLatestPosts( } } - return Ok(pageResults.Items.Adapt>()); + return Ok(pageResults.Items.Adapt()); } /// @@ -192,18 +193,16 @@ public IActionResult GetLatestPosts( /// Optional replay file to attach to post /// Bypass API Validation for post creation (Admin only) /// Post was successfuly created. - /// Post contents validation has failed. + /// Post validation has failed. /// Attached replay is invalid. - /// Restrictions are in effect for one of the targeted accounts. /// One of the targeted accounts was not found. [HttpPost, Authorize(RequireNoPlatformBans), UserAtomicLock] - [ProducesResponseType(201), ProducesResponseType(400), ProducesResponseType(422), ProducesResponseType(typeof(string), 403), ProducesResponseType(typeof(string), 404)] - public async Task CreatePost( + public async Task> CreatePostAsync( [FromForm] string postDto, [FromServices] ReplaysIngestService replaysIngestService, IFormFile? replay = null, - [FromQuery] bool ignoreChecks = false) - { + [FromQuery] bool ignoreChecks = false + ) { PlayerPostDTO post; try @@ -212,52 +211,60 @@ public async Task CreatePost( } catch (Exception e) { - return BadRequest(e.ToString()); + ModelState.TryAddModelException(nameof(postDto), e); + return BadRequest(ModelState); } if (await _playerService.GetPlayerAsync(post.Author.Id) is not { } author) { - return StatusCode(404, $"Account {post.Author.Id} not found."); + ModelState.AddModelError(nameof(post.Author.Id), $"Account {post.Author.Id} not found."); + return BadRequest(ModelState); } if (await _playerService.GetPlayerAsync(post.Player.Id) is not { } player) { - return StatusCode(404, $"Account {post.Player.Id} not found."); + ModelState.AddModelError(nameof(post.Player.Id), $"Account {post.Player.Id} not found."); + return BadRequest(ModelState); } if (ignoreChecks) { if (!(User.IsInRole(ApiRoles.CM) || User.IsInRole(ApiRoles.Administrator))) { - return StatusCode(403, "Post Author is not authorized to bypass Post checks."); + ModelState.AddModelError(nameof(ignoreChecks), "Post Author is not authorized to bypass Post checks."); + return BadRequest(ModelState); } } else { if (post.Author.Id != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))) { - return StatusCode(403, "Author is not authorized to post on behalf of other users."); + ModelState.AddModelError(nameof(post.Author.Id), "Author is not authorized to post on behalf of other users."); + return BadRequest(ModelState); } if (author.OptedOut) { - return StatusCode(403, "Post Author has opted-out from using this platform."); + ModelState.AddModelError(nameof(post.Author.Id), "Post Author has opted-out from using this platform."); + return BadRequest(ModelState); } if (player.OptedOut) { - return StatusCode(403, "Targeted player has opted-out from using this platform."); + ModelState.AddModelError(nameof(post.Player.Id), "Targeted player has opted-out from using this platform."); + return BadRequest(ModelState); } } try { using Post created = await _postService.CreatePostAsync(post, replay, ignoreChecks); - return StatusCode(201, created.Id); + return CreatedAtAction("GetPost", new { postId = created.Id }, created.Adapt()); } - catch (ArgumentException) + catch (ArgumentException e) { - return BadRequest(); + ModelState.TryAddModelException(nameof(post), e); + return BadRequest(ModelState); } catch (InvalidReplayException e) when (e.InnerException is Nodsoft.WowsReplaysUnpack.Core.Exceptions.InvalidReplayException) @@ -267,8 +274,8 @@ public async Task CreatePost( // Handle InvalidReplayException when the Inner exception is a SecurityException and its Data contains an exploit with value "CVE-2022-31265". catch (InvalidReplayException e) when (e.InnerException is SecurityException se && se.Data["exploit"] is "CVE-2022-31265") { - // Log this exception, and store the replay with the RCE the samples. - _logger.LogWarning(se, "Replay upload failed for post author {author} due to CVE-2022-31265 exploit detection.", post.Author.Id); + // Log this exception, and store the replay with the RCE samples. + _logger.LogWarning(se, "Replay upload failed for post author {Author} due to CVE-2022-31265 exploit detection", post.Author.Id); await replaysIngestService.IngestRceFileAsync(replay!); throw se; @@ -285,31 +292,34 @@ public async Task CreatePost( /// Restrictions are in effect for the existing post. /// Targeted post was not found. [HttpPut, Authorize(RequireNoPlatformBans), ETag(false)] - [ProducesResponseType(200), ProducesResponseType(400), ProducesResponseType(typeof(string), 403), ProducesResponseType(typeof(string), 404)] - public async Task EditPost([FromBody] PlayerPostDTO post, [FromQuery] bool ignoreChecks = false) + public async Task EditPost([FromBody] PlayerPostDTO post, [FromQuery] bool ignoreChecks = false) { if (_postService.GetPost(post.Id ?? Guid.Empty) is not { } current) { - return StatusCode(404, $"No post with ID {post.Id} found."); + ModelState.AddModelError(nameof(post.Id), $"No post with ID {post.Id} found."); + return NotFound(ModelState); } if (ignoreChecks) { if (!(User.IsInRole(ApiRoles.CM) || User.IsInRole(ApiRoles.Administrator))) { - return StatusCode(403, "Post Author is not authorized to bypass Post checks."); + ModelState.AddModelError(nameof(ignoreChecks), "Post Author is not authorized to bypass Post checks."); + return BadRequest(ModelState); } } else { if (current.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))) { - return StatusCode(403, "Author is not authorized to edit posts on behalf of other users."); + ModelState.AddModelError(nameof(post.Id), "Author is not authorized to edit posts on behalf of other users."); + return BadRequest(ModelState); } if (current is { ModLocked: true } or { ReadOnly: true }) { - return StatusCode(403, "Post has been locked by Community Managers. No modification is possible."); + ModelState.AddModelError(nameof(post.Id), "Post has been locked by Community Managers. No modification is possible."); + return BadRequest(ModelState); } } @@ -320,7 +330,8 @@ public async Task EditPost([FromBody] PlayerPostDTO post, [FromQu } catch (ArgumentException e) { - return BadRequest(e); + ModelState.TryAddModelException(nameof(post), e); + return BadRequest(ModelState); } } @@ -333,34 +344,37 @@ public async Task EditPost([FromBody] PlayerPostDTO post, [FromQu /// Restrictions are in effect for the existing post. /// Targeted post was not found. [HttpDelete("{postId:guid}"), Authorize(RequireNoPlatformBans)] - [ProducesResponseType(205), ProducesResponseType(typeof(string), 403), ProducesResponseType(typeof(string), 404)] - public async Task DeletePost(Guid postId, [FromQuery] bool ignoreChecks = false) + public async Task DeletePost(Guid postId, [FromQuery] bool ignoreChecks = false) { if (_postService.GetPost(postId) is not { } post) { - return StatusCode(404, $"No post with ID {postId} found."); + ModelState.AddModelError(nameof(postId), $"No post with ID {postId} found."); + return NotFound(ModelState); } if (ignoreChecks) { if (!(User.IsInRole(ApiRoles.CM) || User.IsInRole(ApiRoles.Administrator))) { - return StatusCode(403, "Post Author is not authorized to bypass Post checks."); + ModelState.AddModelError(nameof(ignoreChecks), "Post Author is not authorized to bypass Post checks."); + return BadRequest(ModelState); } } else { if (post.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new BadHttpRequestException("Missing NameIdentifier claim."))) { - return StatusCode(403, "Author is not authorized to delete posts on behalf of other users."); + ModelState.AddModelError(nameof(postId), "Author is not authorized to delete posts on behalf of other users."); + return BadRequest(ModelState); } if (post.ModLocked) { - return StatusCode(403, "Post has been locked by Community Managers. No deletion is possible."); + ModelState.AddModelError(nameof(postId), "Post has been locked by Community Managers. No deletion is possible."); + return BadRequest(ModelState); } } await _postService.DeletePostAsync(postId); - return StatusCode(205); + return StatusCode(StatusCodes.Status205ResetContent); } } \ No newline at end of file diff --git a/WowsKarma.Api/Controllers/ProfileController.cs b/WowsKarma.Api/Controllers/ProfileController.cs index 3ce393a..cbd88ad 100644 --- a/WowsKarma.Api/Controllers/ProfileController.cs +++ b/WowsKarma.Api/Controllers/ProfileController.cs @@ -28,8 +28,8 @@ public ProfileController(PlayerService playerService, UserService userService) /// Player ID to fetch profile flags from. /// Returns player profile flags for given ID. /// No player Profile was found. - [HttpGet("{id}"), ProducesResponseType(typeof(UserProfileFlagsDTO), 200), ProducesResponseType(404)] - public async Task GetProfileFlagsAsync(uint id) => await _playerService.GetPlayerAsync(id, true) is { } player + [HttpGet("{id}")] + public async Task> GetProfileFlagsAsync(uint id) => await _playerService.GetPlayerAsync(id, true) is { } player ? Ok(player.Adapt() with { PostsBanned = player.IsBanned(), @@ -48,23 +48,24 @@ public async Task GetProfileFlagsAsync(uint id) => await _playerS /// User cannot update a profile other than their own. /// User profile was not found. /// A cooldown is currently in effect for one of the values edited. - [HttpPut, Authorize(RequireNoPlatformBans), ETag(false), ProducesResponseType(typeof(UserProfileFlagsDTO), 200)] - [ProducesResponseType(423), ProducesResponseType(typeof(string), 403), ProducesResponseType(404)] - public async Task UpdateProfileFlagsAsync([FromBody] UserProfileFlagsDTO flags) + [HttpPut, Authorize(RequireNoPlatformBans), ETag(false)] + public async Task UpdateProfileFlagsAsync([FromBody] UserProfileFlagsDTO flags) { try { if (flags.Id != User.ToAccountListing()!.Id && !User.IsInRole(ApiRoles.Administrator)) { - return StatusCode(403, "User can only update their own profile."); + ModelState.AddModelError(nameof(flags.Id), "User can only update their own profile."); + return BadRequest(ModelState); } await _playerService.UpdateProfileFlagsAsync(flags); - return Ok(); + return NoContent(); } catch (CooldownException e) { - return StatusCode(423, e); + ModelState.TryAddModelException(nameof(flags), e); + return StatusCode(StatusCodes.Status423Locked, ModelState); } catch (ArgumentException) { diff --git a/WowsKarma.Api/Controllers/ReplayController.cs b/WowsKarma.Api/Controllers/ReplayController.cs index 70bebfc..1081184 100644 --- a/WowsKarma.Api/Controllers/ReplayController.cs +++ b/WowsKarma.Api/Controllers/ReplayController.cs @@ -49,37 +49,41 @@ ILogger logger [HttpGet("{replayId:guid}"), ProducesResponseType(typeof(ReplayDTO), 200)] public Task GetAsync(Guid replayId) => _ingestService.GetReplayDTOAsync(replayId); - [HttpPost("{postId:guid}"), Authorize, RequestSizeLimit(ReplaysIngestService.MaxReplaySize), ProducesResponseType(201)] - public async Task UploadReplayAsync(Guid postId, IFormFile replay, CancellationToken ct, [FromQuery] bool ignoreChecks = false) + [HttpPost("{postId:guid}"), Authorize, RequestSizeLimit(ReplaysIngestService.MaxReplaySize)] + public async Task UploadReplayAsync(Guid postId, IFormFile replay, CancellationToken ct, [FromQuery] bool ignoreChecks = false) { if (_postService.GetPost(postId) is not { } current) { - return StatusCode(404, $"No post with GUID {postId} found."); + ModelState.AddModelError("postId", $"No post with GUID {postId} found."); + return BadRequest(ModelState); } if (ignoreChecks) { if (!(User.IsInRole(ApiRoles.CM) || User.IsInRole(ApiRoles.Administrator))) { - return StatusCode(403, "Post Author is not authorized to bypass Replay checks."); + ModelState.AddModelError("ignoreChecks", "Only Community Managers and Administrators are allowed to bypass Replay checks."); + return BadRequest(ModelState); } } else { if (current.AuthorId != uint.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier) ?? throw new InvalidOperationException("Missing NameIdentifier claim."))) { - return StatusCode(403, "Post Author is not authorized to edit post replays on behalf of other users."); + ModelState.AddModelError("postId", "Post Author is not authorized to upload replays on behalf of other users."); + return BadRequest(ModelState); } if (current.ModLocked) { - return StatusCode(403, "Specified Post has been locked by Community Managers. No modification is possible."); + ModelState.AddModelError("postId", "Specified Post has been locked by Community Managers. No modification is possible."); + return BadRequest(ModelState); } } try { Replay ingested = await _ingestService.IngestReplayAsync(postId, replay, ct); - return StatusCode(201, _ingestService.GetReplayDTOAsync(ingested.Id)); + return CreatedAtAction("Get", new { replayId = ingested.Id }, _ingestService.GetReplayDTOAsync(ingested.Id)); } catch (InvalidReplayException e) { @@ -89,7 +93,7 @@ public async Task UploadReplayAsync(Guid postId, IFormFile replay catch (SecurityException se) when (se.Data["exploit"] is "CVE-2022-31265") { // Log this exception, and store the replay with the RCE the samples. - _logger.LogWarning(se, "Replay upload failed due to CVE-2022-31265 exploit detection."); + _logger.LogWarning(se, "Replay upload failed due to CVE-2022-31265 exploit detection"); await _ingestService.IngestRceFileAsync(replay); return BadRequest(se); } @@ -101,7 +105,7 @@ public async Task UploadReplayAsync(Guid postId, IFormFile replay /// Start of date/time range /// End of date/time range [HttpPatch("reprocess/replay/all"), Authorize(Roles = ApiRoles.Administrator)] - public IActionResult ReprocessPosts(DateTime start = default, DateTime end = default, CancellationToken ct = default) + public AcceptedResult ReprocessPosts(DateTime start = default, DateTime end = default) { if (start == default) { @@ -113,25 +117,24 @@ public IActionResult ReprocessPosts(DateTime start = default, DateTime end = def end = DateTime.UtcNow; } - BackgroundJob.Enqueue(s => s.ReprocessAllReplaysAsync(start.ToUniversalTime(), end.ToUniversalTime(), ct)); - return StatusCode(202); + BackgroundJob.Enqueue(s => s.ReprocessAllReplaysAsync(start.ToUniversalTime(), end.ToUniversalTime(), HttpContext.RequestAborted)); + return Accepted(); } /// /// Triggers reporessing on a replay (Usable only by Administrators) /// - /// [HttpPatch("reprocess/replay/{replayId:guid}"), Authorize(Roles = ApiRoles.Administrator)] - public IActionResult ReprocessReplay(Guid replayId, CancellationToken ct = default) + public IActionResult ReprocessReplay(Guid replayId) { try { - BackgroundJob.Enqueue(s => s.ReprocessReplayAsync(replayId, ct)); - return StatusCode(202); + BackgroundJob.Enqueue(s => s.ReprocessReplayAsync(replayId, HttpContext.RequestAborted)); + return AcceptedAtAction("Get", routeValues: new { replayId }, null); } catch (ArgumentException) { - return StatusCode(404, $"No replay with GUID {replayId} found."); + return NotFound(); } } @@ -142,32 +145,30 @@ public IActionResult ReprocessReplay(Guid replayId, CancellationToken ct = defau /// /// Whether to force rendering the minimap, even if it has already been rendered. /// Whether to wait for the job to complete before returning. - /// The cancellation token. /// The minimap was rendered successfully. /// The job was enqueued successfully. /// No post with the specified GUID was found. [HttpPatch("reprocess/minimap/{postId:guid}"), Authorize(Roles = ApiRoles.Administrator)] - public async ValueTask RenderMinimap(Guid postId, + public async ValueTask RenderMinimap(Guid postId, [FromServices] MinimapRenderingService minimapRenderingService, [FromQuery] bool force = false, - [FromQuery] bool waitForCompletion = false, - CancellationToken ct = default + [FromQuery] bool waitForCompletion = false ) { if (_postService.GetPost(postId) is not { } post) { - return StatusCode(404, $"No post with GUID {postId} found."); + return NotFound(); } if (waitForCompletion) { - await minimapRenderingService.RenderPostReplayMinimapAsync(post.Id, force, ct); + await minimapRenderingService.RenderPostReplayMinimapAsync(post.Id, force, HttpContext.RequestAborted); + return Ok(); } else { - BackgroundJob.Enqueue(s => s.RenderPostReplayMinimapAsync(post.Id, force, ct)); + BackgroundJob.Enqueue(s => s.RenderPostReplayMinimapAsync(post.Id, force, HttpContext.RequestAborted)); + return AcceptedAtAction("Get", routeValues: new { postId }, null); } - - return StatusCode(waitForCompletion ? 200 : 202); } /// @@ -176,10 +177,9 @@ public async ValueTask RenderMinimap(Guid postId, /// Start of date/time range /// End of date/time range /// Whether to force rendering a minimap, even if it has already been rendered. - /// Cancellation token /// The job was enqueued successfully. [HttpPatch("reprocess/minimap/all"), Authorize(Roles = ApiRoles.Administrator)] - public IActionResult RenderMinimaps(DateTime start = default, DateTime end = default, bool force = false, CancellationToken ct = default) + public AcceptedResult RenderMinimaps(DateTime start = default, DateTime end = default, bool force = false) { if (start == default) { @@ -191,7 +191,7 @@ public IActionResult RenderMinimaps(DateTime start = default, DateTime end = def end = DateTime.UtcNow; } - BackgroundJob.Enqueue(s => s.ReprocessAllMinimapsAsync(start.ToUniversalTime(), end.ToUniversalTime(), force, ct)); - return StatusCode(202); + BackgroundJob.Enqueue(s => s.ReprocessAllMinimapsAsync(start.ToUniversalTime(), end.ToUniversalTime(), force, HttpContext.RequestAborted)); + return Accepted(); } } diff --git a/WowsKarma.Api/Controllers/StatusController.cs b/WowsKarma.Api/Controllers/StatusController.cs index 0cb983b..cd77c4a 100644 --- a/WowsKarma.Api/Controllers/StatusController.cs +++ b/WowsKarma.Api/Controllers/StatusController.cs @@ -15,15 +15,15 @@ public sealed class StatusController : Controller /// /// Service is healthy. [HttpGet, ProducesResponseType(200)] - public IActionResult Status() => Ok(); + public OkResult Status() => Ok(); [Route("/error"), ApiExplorerSettings(IgnoreApi = true)] - public IActionResult HandleError() + public ObjectResult HandleError() { - if (HttpContext.Features.Get() is { Error: not null } exceptionHandlerFeature) + if (HttpContext.Features.Get() is { } exceptionHandlerFeature) { Uri fullPath = new UriBuilder(Request.Scheme, Request.Host.Host, Request.Host.Port ?? 80, exceptionHandlerFeature.Path).Uri; - + return Problem( detail: exceptionHandlerFeature.Error.StackTrace, instance: fullPath.ToString(),