feat(backend): add update member endpoint
This commit is contained in:
		
							parent
							
								
									8fe8755183
								
							
						
					
					
						commit
						e11e60e16b
					
				
					 4 changed files with 133 additions and 1 deletions
				
			
		|  | @ -92,6 +92,100 @@ public class MembersController( | |||
|         return Ok(memberRenderer.RenderMember(member, CurrentToken)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPatch("/api/v2/users/@me/members/{memberRef}")] | ||||
|     [Authorize("member.update")] | ||||
|     public async Task<IActionResult> UpdateMemberAsync(string memberRef, [FromBody] UpdateMemberRequest req) | ||||
|     { | ||||
|         await using var tx = await db.Database.BeginTransactionAsync(); | ||||
|         var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
|         var errors = new List<(string, ValidationError?)>(); | ||||
| 
 | ||||
|         if (req.Name != null) | ||||
|         { | ||||
|             errors.Add(("name", ValidationUtils.ValidateMemberName(req.Name))); | ||||
|             member.Name = req.Name; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.DisplayName))) | ||||
|         { | ||||
|             errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName))); | ||||
|             member.DisplayName = req.DisplayName; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Bio))) | ||||
|         { | ||||
|             errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio))); | ||||
|             member.Bio = req.Bio; | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Links))) | ||||
|         { | ||||
|             errors.AddRange(ValidationUtils.ValidateLinks(req.Links)); | ||||
|             member.Links = req.Links ?? []; | ||||
|         } | ||||
| 
 | ||||
|         if (req.Names != null) | ||||
|         { | ||||
|             errors.AddRange(ValidationUtils.ValidateFieldEntries(req.Names, CurrentUser!.CustomPreferences, "names")); | ||||
|             member.Names = req.Names.ToList(); | ||||
|         } | ||||
| 
 | ||||
|         if (req.Pronouns != null) | ||||
|         { | ||||
|             errors.AddRange(ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)); | ||||
|             member.Pronouns = req.Pronouns.ToList(); | ||||
|         } | ||||
| 
 | ||||
|         if (req.Fields != null) | ||||
|         { | ||||
|             errors.AddRange(ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)); | ||||
|             member.Fields = req.Fields.ToList(); | ||||
|         } | ||||
| 
 | ||||
|         if (req.Flags != null) | ||||
|         { | ||||
|             var flagError = await db.SetMemberFlagsAsync(CurrentUser!.Id, member.Id, req.Flags); | ||||
|             if (flagError != null) errors.Add(("flags", flagError)); | ||||
|         } | ||||
| 
 | ||||
|         if (req.HasProperty(nameof(req.Avatar))) | ||||
|             errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); | ||||
| 
 | ||||
|         ValidationUtils.Validate(errors); | ||||
|         // This is fired off regardless of whether the transaction is committed | ||||
|         // (atomic operations are hard when combined with background jobs) | ||||
|         // so it's in a separate block to the validation above. | ||||
|         if (req.HasProperty(nameof(req.Avatar))) | ||||
|             queue.QueueInvocableWithPayload<MemberAvatarUpdateInvocable, AvatarUpdatePayload>( | ||||
|                 new AvatarUpdatePayload(member.Id, req.Avatar)); | ||||
|         try | ||||
|         { | ||||
|             await db.SaveChangesAsync(); | ||||
|         } | ||||
|         catch (UniqueConstraintException) | ||||
|         { | ||||
|             _logger.Debug("Could not update member {Id} due to name conflict ({CurrentName} / {NewName})", member.Id, | ||||
|                 member.Name, req.Name); | ||||
|             throw new ApiError.BadRequest("A member with that name already exists", "name", req.Name!); | ||||
|         } | ||||
| 
 | ||||
|         await tx.CommitAsync(); | ||||
|         return Ok(memberRenderer.RenderMember(member, CurrentToken)); | ||||
|     } | ||||
| 
 | ||||
|     public class UpdateMemberRequest : PatchRequest | ||||
|     { | ||||
|         public string? Name { get; init; } | ||||
|         public string? DisplayName { get; init; } | ||||
|         public string? Bio { get; init; } | ||||
|         public string? Avatar { get; init; } | ||||
|         public string[]? Links { get; init; } | ||||
|         public FieldEntry[]? Names { get; init; } | ||||
|         public Pronoun[]? Pronouns { get; init; } | ||||
|         public Field[]? Fields { get; init; } | ||||
|         public Snowflake[]? Flags { get; init; } | ||||
|     } | ||||
| 
 | ||||
|     [HttpDelete("/api/v2/users/@me/members/{memberRef}")] | ||||
|     [Authorize("member.update")] | ||||
|     public async Task<IActionResult> DeleteMemberAsync(string memberRef) | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Coravel.Queuing.Interfaces; | ||||
| using EntityFramework.Exceptions.Common; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Jobs; | ||||
|  | @ -15,11 +16,14 @@ namespace Foxnouns.Backend.Controllers; | |||
| [Route("/api/v2/users")] | ||||
| public class UsersController( | ||||
|     DatabaseContext db, | ||||
|     ILogger logger, | ||||
|     UserRendererService userRenderer, | ||||
|     ISnowflakeGenerator snowflakeGenerator, | ||||
|     IQueue queue, | ||||
|     IClock clock) : ApiControllerBase | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<UsersController>(); | ||||
| 
 | ||||
|     [HttpGet("{userRef}")] | ||||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default) | ||||
|  | @ -103,7 +107,17 @@ public class UsersController( | |||
|             queue.QueueInvocableWithPayload<UserAvatarUpdateInvocable, AvatarUpdatePayload>( | ||||
|                 new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); | ||||
| 
 | ||||
|         await db.SaveChangesAsync(ct); | ||||
|         try | ||||
|         { | ||||
|             await db.SaveChangesAsync(ct); | ||||
|         } | ||||
|         catch (UniqueConstraintException) | ||||
|         { | ||||
|             _logger.Debug("Could not update user {Id} due to name conflict ({CurrentName} / {NewName})", user.Id, | ||||
|                 user.Username, req.Username); | ||||
|             throw new ApiError.BadRequest("That username is already taken.", "username", req.Username!); | ||||
|         } | ||||
| 
 | ||||
|         await tx.CommitAsync(ct); | ||||
|         return Ok(await userRenderer.RenderUserAsync(user, CurrentUser, renderMembers: false, | ||||
|             renderAuthMethods: false, ct: ct)); | ||||
|  |  | |||
|  | @ -33,4 +33,25 @@ public static class FlagQueryExtensions | |||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public static async Task<ValidationError?> SetMemberFlagsAsync(this DatabaseContext db, Snowflake userId, | ||||
|         Snowflake memberId, Snowflake[] flagIds) | ||||
|     { | ||||
|         var currentFlags = await db.MemberFlags.Where(f => f.MemberId == memberId).ToListAsync(); | ||||
|         foreach (var flag in currentFlags) | ||||
|             db.MemberFlags.Remove(flag); | ||||
| 
 | ||||
|         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 memberFlags = flagIds.Select(id => new MemberFlag { PrideFlagId = id, MemberId = memberId }); | ||||
|         db.MemberFlags.AddRange(memberFlags); | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -6,6 +6,9 @@ namespace Foxnouns.Backend.Utils; | |||
| 
 | ||||
| /// <summary> | ||||
| /// A base class used for PATCH requests which stores information on whether a key is explicitly set to null or not passed at all. | ||||
| /// | ||||
| /// HasProperty() should not be used for properties that cannot be set to null--a null value should be treated | ||||
| /// as an unset value in those cases. | ||||
| /// </summary> | ||||
| public abstract class PatchRequest | ||||
| { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue