diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 5021d8e..efae036 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -24,17 +24,17 @@ public class FlagsController( [HttpGet] [Authorize("identify")] - [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] public async Task GetFlagsAsync(CancellationToken ct = default) { var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); - return Ok(flags.Select(ToResponse)); + return Ok(flags.Select(userRenderer.RenderPrideFlag)); } [HttpPost] [Authorize("user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] + [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] public IActionResult CreateFlag([FromBody] CreateFlagRequest req) { ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); @@ -68,7 +68,7 @@ public class FlagsController( db.Update(flag); await db.SaveChangesAsync(); - return Ok(ToResponse(flag)); + return Ok(userRenderer.RenderPrideFlag(flag)); } public class UpdateFlagRequest : PatchRequest @@ -111,15 +111,6 @@ public class FlagsController( return NoContent(); } - private PrideFlagResponse ToResponse(PrideFlag flag) => - new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description); - - private record PrideFlagResponse( - Snowflake Id, - string ImageUrl, - string Name, - string? Description); - private static List<(string, ValidationError?)> ValidateFlag(string? name, string? description, string? imageData) { var errors = new List<(string, ValidationError?)>(); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index fddd798..05bccdd 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -86,6 +86,12 @@ public class UsersController( user.Fields = req.Fields.ToList(); } + if (req.Flags != null) + { + var flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags); + if (flagError != null) errors.Add(("flags", flagError)); + } + if (req.HasProperty(nameof(req.Avatar))) errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); @@ -182,6 +188,7 @@ public class UsersController( public FieldEntry[]? Names { get; init; } public Pronoun[]? Pronouns { get; init; } public Field[]? Fields { get; init; } + public Snowflake[]? Flags { get; init; } } diff --git a/Foxnouns.Backend/Database/FlagQueryExtensions.cs b/Foxnouns.Backend/Database/FlagQueryExtensions.cs new file mode 100644 index 0000000..39272af --- /dev/null +++ b/Foxnouns.Backend/Database/FlagQueryExtensions.cs @@ -0,0 +1,36 @@ +using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore; + +namespace Foxnouns.Backend.Database; + +public static class FlagQueryExtensions +{ + private static async Task> GetFlagsAsync(this DatabaseContext db, Snowflake userId) => + await db.PrideFlags.Where(f => f.UserId == userId).OrderBy(f => f.Id).ToListAsync(); + + /// + /// Sets the user's profile flags to the given IDs. Returns a validation error if any of the flag IDs are unknown + /// or if too many IDs are given. Duplicates are allowed. + /// + public static async Task SetUserFlagsAsync(this DatabaseContext db, Snowflake userId, + Snowflake[] flagIds) + { + var currentFlags = await db.UserFlags.Where(f => f.UserId == userId).ToListAsync(); + foreach (var flag in currentFlags) + db.UserFlags.Remove(flag); + + // If there's no new flags to set, we're done + if (flagIds.Length == 0) return null; + if (flagIds.Length > 100) return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length); + + var flags = await db.GetFlagsAsync(userId); + var unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); + if (unknownFlagIds.Length != 0) + return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds); + + var userFlags = flagIds.Select(id => new UserFlag { PrideFlagId = id, UserId = userId }); + db.UserFlags.AddRange(userFlags); + + return null; + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 688214d..95d40d3 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -25,10 +25,12 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe renderAuthMethods = renderAuthMethods && tokenPrivileged; IEnumerable members = - renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync(ct) : []; + renderMembers ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct) : []; // Unless the user is requesting their own members AND the token can read hidden members, we filter out unlisted members. if (!(isSelfUser && tokenCanReadHiddenMembers)) members = members.Where(m => !m.Unlisted); + var flags = await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct); + var authMethods = renderAuthMethods ? await db.AuthMethods .Where(a => a.UserId == user.Id) @@ -40,6 +42,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe user.Id, user.Sid, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, + flags.Select(f => RenderPrideFlag(f.PrideFlag)), renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderAuthMethods ? authMethods.Select(a => new AuthenticationMethodResponse( @@ -58,7 +61,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - + public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; public record UserResponse( @@ -74,6 +77,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe IEnumerable Pronouns, IEnumerable Fields, Dictionary CustomPreferences, + IEnumerable Flags, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] @@ -105,4 +109,13 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe string? AvatarUrl, Dictionary CustomPreferences ); + + public PrideFlagResponse RenderPrideFlag(PrideFlag flag) => + new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); + + public record PrideFlagResponse( + Snowflake Id, + string ImageUrl, + string Name, + string? Description); } \ No newline at end of file