From 14e6e35cb7aa4b7ebc1754582ee1036d51438a5b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 22:26:40 +0200 Subject: [PATCH] feat(backend): add create flag endpoint and job --- .../Controllers/FlagsController.cs | 32 +++++++++-- .../Extensions/WebApplicationExtensions.cs | 3 +- Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 53 +++++++++++++++++++ Foxnouns.Backend/Jobs/Payloads.cs | 4 +- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 Foxnouns.Backend/Jobs/CreateFlagInvocable.cs diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index b08dcef..7bf20e5 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -1,4 +1,7 @@ +using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; @@ -7,7 +10,11 @@ using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users/@me/flags")] -public class FlagsController(DatabaseContext db, UserRendererService userRenderer) : ApiControllerBase +public class FlagsController( + DatabaseContext db, + UserRendererService userRenderer, + ISnowflakeGenerator snowflakeGenerator, + IQueue queue) : ApiControllerBase { [HttpGet] [Authorize("identify")] @@ -16,10 +23,29 @@ public class FlagsController(DatabaseContext db, UserRendererService userRendere { var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); - return Ok(flags.Select(f => new PrideFlagResponse( - f.Id, userRenderer.ImageUrlFor(f), f.Name, f.Description))); + return Ok(flags.Select(ToResponse)); } + [HttpPost] + [Authorize("user.update")] + [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] + public IActionResult CreateFlag([FromBody] CreateFlagRequest req) + { + var id = snowflakeGenerator.GenerateSnowflake(); + + queue.QueueInvocableWithPayload( + new CreateFlagPayload(id, CurrentUser!.Id, req.Name, req.Image, req.Description)); + + return Accepted(new CreateFlagResponse(id, req.Name, req.Description)); + } + + public record CreateFlagRequest(string Name, string Image, string? Description); + + public record CreateFlagResponse(Snowflake Id, string Name, string? Description); + + private PrideFlagResponse ToResponse(PrideFlag flag) => + new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description); + private record PrideFlagResponse( Snowflake Id, string ImageUrl, diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index a5b2af6..e249fd7 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -99,7 +99,8 @@ public static class WebApplicationExtensions .AddHostedService() // Transient jobs .AddTransient() - .AddTransient(); + .AddTransient() + .AddTransient(); if (!config.Logging.EnableMetrics) services.AddHostedService(); diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs new file mode 100644 index 0000000..6f69794 --- /dev/null +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -0,0 +1,53 @@ +using System.Security.Cryptography; +using Coravel.Invocable; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; +using Foxnouns.Backend.Services; + +namespace Foxnouns.Backend.Jobs; + +public class CreateFlagInvocable(DatabaseContext db, ObjectStorageService objectStorageService, ILogger logger) + : IInvocable, IInvocableWithPayload +{ + private readonly ILogger _logger = logger.ForContext(); + public required CreateFlagPayload Payload { get; set; } + + public async Task Invoke() + { + _logger.Information("Creating flag {FlagId} for user {UserId} with image data length {DataLength}", Payload.Id, + Payload.UserId, Payload.ImageData.Length); + + try + { + var image = await Payload.ImageData.ConvertBase64UriToImage(size: 256, crop: false); + image.Seek(0, SeekOrigin.Begin); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); + image.Seek(0, SeekOrigin.Begin); + + await objectStorageService.PutObjectAsync(Path(hash), image, "image/webp"); + + var flag = new PrideFlag + { + Id = Payload.Id, + UserId = Payload.UserId, + Hash = hash, + Name = Payload.Name, + Description = Payload.Description + }; + db.Add(flag); + + await db.SaveChangesAsync(); + + _logger.Information("Uploaded flag {FlagId} with hash {Hash}", flag.Id, flag.Hash); + } + catch (ArgumentException ae) + { + _logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", Payload.Id, ae.Message); + } + + throw new NotImplementedException(); + } + + private static string Path(string hash) => $"flags/{hash}.webp"; +} \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/Payloads.cs b/Foxnouns.Backend/Jobs/Payloads.cs index f28254a..672f6d6 100644 --- a/Foxnouns.Backend/Jobs/Payloads.cs +++ b/Foxnouns.Backend/Jobs/Payloads.cs @@ -2,4 +2,6 @@ using Foxnouns.Backend.Database; namespace Foxnouns.Backend.Jobs; -public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); \ No newline at end of file +public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); + +public record CreateFlagPayload(Snowflake Id, Snowflake UserId, string Name, string ImageData, string? Description); \ No newline at end of file