feat(backend): ability to set profile flags, return profile flags in get user endpoint

This commit is contained in:
sam 2024-09-27 14:48:09 +02:00
parent 6a4aa8064a
commit a3cbdc1a08
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
4 changed files with 62 additions and 15 deletions

View file

@ -24,17 +24,17 @@ public class FlagsController(
[HttpGet] [HttpGet]
[Authorize("identify")] [Authorize("identify")]
[ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<IEnumerable<UserRendererService.PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default) public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default)
{ {
var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); 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] [HttpPost]
[Authorize("user.update")] [Authorize("user.update")]
[ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status202Accepted)] [ProducesResponseType<UserRendererService.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)); ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image));
@ -68,7 +68,7 @@ public class FlagsController(
db.Update(flag); db.Update(flag);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return Ok(ToResponse(flag)); return Ok(userRenderer.RenderPrideFlag(flag));
} }
public class UpdateFlagRequest : PatchRequest public class UpdateFlagRequest : PatchRequest
@ -111,15 +111,6 @@ public class FlagsController(
return NoContent(); 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) private static List<(string, ValidationError?)> ValidateFlag(string? name, string? description, string? imageData)
{ {
var errors = new List<(string, ValidationError?)>(); var errors = new List<(string, ValidationError?)>();

View file

@ -86,6 +86,12 @@ public class UsersController(
user.Fields = req.Fields.ToList(); 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))) if (req.HasProperty(nameof(req.Avatar)))
errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar)));
@ -182,6 +188,7 @@ public class UsersController(
public FieldEntry[]? Names { get; init; } public FieldEntry[]? Names { get; init; }
public Pronoun[]? Pronouns { get; init; } public Pronoun[]? Pronouns { get; init; }
public Field[]? Fields { get; init; } public Field[]? Fields { get; init; }
public Snowflake[]? Flags { get; init; }
} }

View file

@ -0,0 +1,36 @@
using Foxnouns.Backend.Database.Models;
using Microsoft.EntityFrameworkCore;
namespace Foxnouns.Backend.Database;
public static class FlagQueryExtensions
{
private static async Task<List<PrideFlag>> GetFlagsAsync(this DatabaseContext db, Snowflake userId) =>
await db.PrideFlags.Where(f => f.UserId == userId).OrderBy(f => f.Id).ToListAsync();
/// <summary>
/// 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.
/// </summary>
public static async Task<ValidationError?> 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;
}
}

View file

@ -25,10 +25,12 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
renderAuthMethods = renderAuthMethods && tokenPrivileged; renderAuthMethods = renderAuthMethods && tokenPrivileged;
IEnumerable<Member> members = IEnumerable<Member> 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. // 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); 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 var authMethods = renderAuthMethods
? await db.AuthMethods ? await db.AuthMethods
.Where(a => a.UserId == user.Id) .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.Id, user.Sid, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user),
user.Links, user.Links,
user.Names, user.Pronouns, user.Fields, user.CustomPreferences, user.Names, user.Pronouns, user.Fields, user.CustomPreferences,
flags.Select(f => RenderPrideFlag(f.PrideFlag)),
renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null,
renderAuthMethods renderAuthMethods
? authMethods.Select(a => new AuthenticationMethodResponse( ? authMethods.Select(a => new AuthenticationMethodResponse(
@ -58,7 +61,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
private string? AvatarUrlFor(User user) => private string? AvatarUrlFor(User user) =>
user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; 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 string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp";
public record UserResponse( public record UserResponse(
@ -74,6 +77,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
IEnumerable<Pronoun> Pronouns, IEnumerable<Pronoun> Pronouns,
IEnumerable<Field> Fields, IEnumerable<Field> Fields,
Dictionary<Snowflake, User.CustomPreference> CustomPreferences, Dictionary<Snowflake, User.CustomPreference> CustomPreferences,
IEnumerable<PrideFlagResponse> Flags,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
IEnumerable<MemberRendererService.PartialMember>? Members, IEnumerable<MemberRendererService.PartialMember>? Members,
[property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
@ -105,4 +109,13 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe
string? AvatarUrl, string? AvatarUrl,
Dictionary<Snowflake, User.CustomPreference> CustomPreferences Dictionary<Snowflake, User.CustomPreference> 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);
} }