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)); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue