Compare commits

..

2 commits

Author SHA1 Message Date
sam
6a4aa8064a
feat(backend): update flag endpoint 2024-09-27 00:38:34 +02:00
sam
758ab9ec5b
feat(backend): delete flag endpoint 2024-09-26 23:03:50 +02:00
3 changed files with 122 additions and 3 deletions

View file

@ -1,9 +1,11 @@
using Coravel.Queuing.Interfaces; using Coravel.Queuing.Interfaces;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Extensions;
using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Jobs;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -11,11 +13,15 @@ namespace Foxnouns.Backend.Controllers;
[Route("/api/v2/users/@me/flags")] [Route("/api/v2/users/@me/flags")]
public class FlagsController( public class FlagsController(
ILogger logger,
DatabaseContext db, DatabaseContext db,
UserRendererService userRenderer, UserRendererService userRenderer,
ObjectStorageService objectStorageService,
ISnowflakeGenerator snowflakeGenerator, ISnowflakeGenerator snowflakeGenerator,
IQueue queue) : ApiControllerBase IQueue queue) : ApiControllerBase
{ {
private readonly ILogger _logger = logger.ForContext<FlagsController>();
[HttpGet] [HttpGet]
[Authorize("identify")] [Authorize("identify")]
[ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)]
@ -31,6 +37,8 @@ public class FlagsController(
[ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status202Accepted)] [ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status202Accepted)]
public IActionResult CreateFlag([FromBody] CreateFlagRequest req) public IActionResult CreateFlag([FromBody] CreateFlagRequest req)
{ {
ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image));
var id = snowflakeGenerator.GenerateSnowflake(); var id = snowflakeGenerator.GenerateSnowflake();
queue.QueueInvocableWithPayload<CreateFlagInvocable, CreateFlagPayload>( queue.QueueInvocableWithPayload<CreateFlagInvocable, CreateFlagPayload>(
@ -41,7 +49,67 @@ public class FlagsController(
public record CreateFlagRequest(string Name, string Image, string? Description); public record CreateFlagRequest(string Name, string Image, string? Description);
public record CreateFlagResponse(Snowflake Id, string Name, string? Description); private record CreateFlagResponse(Snowflake Id, string Name, string? Description);
[HttpPatch("{id}")]
[Authorize("user.update")]
public async Task<IActionResult> 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<IActionResult> 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) => private PrideFlagResponse ToResponse(PrideFlag flag) =>
new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description); new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description);
@ -51,4 +119,52 @@ public class FlagsController(
string ImageUrl, string ImageUrl,
string Name, string Name,
string? Description); 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;
}
} }

View file

@ -24,6 +24,10 @@ public static class AvatarObjectExtensions
CancellationToken ct = default) => CancellationToken ct = default) =>
await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); 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) public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(this string uri, int size, bool crop)
{ {
if (!uri.StartsWith("data:image/")) if (!uri.StartsWith("data:image/"))

View file

@ -1,4 +1,3 @@
using System.Security.Cryptography;
using Coravel.Invocable; using Coravel.Invocable;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
@ -45,5 +44,5 @@ public class CreateFlagInvocable(DatabaseContext db, ObjectStorageService object
throw new NotImplementedException(); throw new NotImplementedException();
} }
private static string Path(string hash) => $"flags/{hash}.webp"; public static string Path(string hash) => $"flags/{hash}.webp";
} }