From 758ab9ec5b4b475c9a5a6dc1907cd865be65fc9b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 23:03:50 +0200 Subject: [PATCH 1/2] feat(backend): delete flag endpoint --- .../Controllers/FlagsController.cs | 39 ++++++++++++++++++- .../Extensions/AvatarObjectExtensions.cs | 4 ++ Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 3 +- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 7bf20e5..31f3400 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -1,6 +1,7 @@ using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; @@ -11,11 +12,15 @@ namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users/@me/flags")] public class FlagsController( + ILogger logger, DatabaseContext db, UserRendererService userRenderer, + ObjectStorageService objectStorageService, ISnowflakeGenerator snowflakeGenerator, IQueue queue) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpGet] [Authorize("identify")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] @@ -40,8 +45,40 @@ public class FlagsController( } public record CreateFlagRequest(string Name, string Image, string? Description); + private record CreateFlagResponse(Snowflake Id, string Name, string? Description); - public record CreateFlagResponse(Snowflake Id, string Name, string? Description); + [HttpDelete("{id}")] + [Authorize("user.update")] + public async Task DeleteFlagAsync(Snowflake id) + { + await using var tx = await db.Database.BeginTransactionAsync(); + + var flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id); + if (flag == null) throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); + + var hash = flag.Hash; + + db.PrideFlags.Remove(flag); + await db.SaveChangesAsync(); + + var flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); + if (flagCount == 0) + { + try + { + _logger.Information("Deleting flag file {Hash} as it is no longer used by any flags", hash); + await objectStorageService.DeleteFlagAsync(hash); + } + catch (Exception e) + { + _logger.Error(e, "Error deleting flag file {Hash}", hash); + } + } else _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); + + await tx.CommitAsync(); + + return NoContent(); + } private PrideFlagResponse ToResponse(PrideFlag flag) => new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description); diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index 063f835..6aa1626 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -24,6 +24,10 @@ public static class AvatarObjectExtensions CancellationToken ct = default) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); + public static async Task DeleteFlagAsync(this ObjectStorageService objectStorageService, string hash, + CancellationToken ct = default) => + await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct); + public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(this string uri, int size, bool crop) { if (!uri.StartsWith("data:image/")) diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 1acf054..0d99244 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -1,4 +1,3 @@ -using System.Security.Cryptography; using Coravel.Invocable; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -45,5 +44,5 @@ public class CreateFlagInvocable(DatabaseContext db, ObjectStorageService object throw new NotImplementedException(); } - private static string Path(string hash) => $"flags/{hash}.webp"; + public static string Path(string hash) => $"flags/{hash}.webp"; } \ No newline at end of file From 6a4aa8064a97da54be4791fab404e361821fca71 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 27 Sep 2024 00:38:34 +0200 Subject: [PATCH 2/2] feat(backend): update flag endpoint --- .../Controllers/FlagsController.cs | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 31f3400..5021d8e 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -5,6 +5,7 @@ using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; @@ -20,7 +21,7 @@ public class FlagsController( IQueue queue) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); - + [HttpGet] [Authorize("identify")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] @@ -36,6 +37,8 @@ public class FlagsController( [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] public IActionResult CreateFlag([FromBody] CreateFlagRequest req) { + ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); + var id = snowflakeGenerator.GenerateSnowflake(); queue.QueueInvocableWithPayload( @@ -45,14 +48,41 @@ public class FlagsController( } public record CreateFlagRequest(string Name, string Image, string? Description); + private record CreateFlagResponse(Snowflake Id, string Name, string? Description); + [HttpPatch("{id}")] + [Authorize("user.update")] + public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) + { + ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); + + var flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id); + if (flag == null) throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); + + if (req.Name != null) flag.Name = req.Name; + + if (req.HasProperty(nameof(req.Description))) + flag.Description = req.Description; + + db.Update(flag); + await db.SaveChangesAsync(); + + return Ok(ToResponse(flag)); + } + + public class UpdateFlagRequest : PatchRequest + { + public string? Name { get; init; } + public string? Description { get; init; } + } + [HttpDelete("{id}")] [Authorize("user.update")] public async Task DeleteFlagAsync(Snowflake id) { await using var tx = await db.Database.BeginTransactionAsync(); - + var flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id); if (flag == null) throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); @@ -73,8 +103,9 @@ public class FlagsController( { _logger.Error(e, "Error deleting flag file {Hash}", hash); } - } else _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); - + } + else _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); + await tx.CommitAsync(); return NoContent(); @@ -88,4 +119,52 @@ public class FlagsController( string ImageUrl, string Name, string? Description); + + private static List<(string, ValidationError?)> ValidateFlag(string? name, string? description, string? imageData) + { + var errors = new List<(string, ValidationError?)>(); + + if (name != null) + { + switch (name.Length) + { + case < 1: + errors.Add(("name", ValidationError.LengthError("Name is too short", 1, 100, name.Length))); + break; + case > 100: + errors.Add(("name", ValidationError.LengthError("Name is too long", 1, 100, name.Length))); + break; + } + } + + if (description != null) + { + switch (description.Length) + { + case < 1: + errors.Add(("description", + ValidationError.LengthError("Description is too short", 1, 100, description.Length))); + break; + case > 500: + errors.Add(("description", + ValidationError.LengthError("Description is too long", 1, 100, description.Length))); + break; + } + } + + if (imageData != null) + { + switch (imageData.Length) + { + case 0: + errors.Add(("image", ValidationError.GenericValidationError("Image cannot be empty", null))); + break; + case > 1_500_000: + errors.Add(("image", ValidationError.GenericValidationError("Image is too large", null))); + break; + } + } + + return errors; + } } \ No newline at end of file