feat(backend): user lookup
This commit is contained in:
		
							parent
							
								
									8713279d3d
								
							
						
					
					
						commit
						12eddb9949
					
				
					 4 changed files with 156 additions and 0 deletions
				
			
		
							
								
								
									
										90
									
								
								Foxnouns.Backend/Controllers/Moderation/LookupController.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								Foxnouns.Backend/Controllers/Moderation/LookupController.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,90 @@ | |||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Controllers.Moderation; | ||||
| 
 | ||||
| [Route("/api/v2/moderation/lookup")] | ||||
| [Authorize("user.moderation")] | ||||
| [Limit(RequireModerator = true)] | ||||
| public class LookupController( | ||||
|     DatabaseContext db, | ||||
|     UserRendererService userRenderer, | ||||
|     ModerationService moderationService, | ||||
|     ModerationRendererService moderationRenderer | ||||
| ) : ApiControllerBase | ||||
| { | ||||
|     [HttpPost] | ||||
|     public async Task<IActionResult> QueryUsersAsync( | ||||
|         [FromBody] QueryUsersRequest req, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         var query = db.Users.Select(u => new { u.Id, u.Username }); | ||||
|         query = req.Fuzzy | ||||
|             ? query.Where(u => u.Username.Contains(req.Query)) | ||||
|             : query.Where(u => u.Username == req.Query); | ||||
| 
 | ||||
|         var users = await query.OrderBy(u => u.Id).Take(100).ToListAsync(ct); | ||||
|         return Ok(users); | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("{id}")] | ||||
|     public async Task<IActionResult> QueryUserAsync(Snowflake id, CancellationToken ct = default) | ||||
|     { | ||||
|         User user = await db.ResolveUserAsync(id, ct); | ||||
| 
 | ||||
|         bool showSensitiveData = await moderationService.ShowSensitiveDataAsync( | ||||
|             CurrentUser!, | ||||
|             user, | ||||
|             ct | ||||
|         ); | ||||
| 
 | ||||
|         List<AuthMethod> authMethods = showSensitiveData | ||||
|             ? await db | ||||
|                 .AuthMethods.Where(a => a.UserId == user.Id) | ||||
|                 .Include(a => a.FediverseApplication) | ||||
|                 .ToListAsync(ct) | ||||
|             : []; | ||||
| 
 | ||||
|         return Ok( | ||||
|             new QueryUserResponse( | ||||
|                 User: await userRenderer.RenderUserAsync( | ||||
|                     user, | ||||
|                     renderMembers: false, | ||||
|                     renderAuthMethods: false, | ||||
|                     ct: ct | ||||
|                 ), | ||||
|                 MemberListHidden: user.ListHidden, | ||||
|                 LastActive: user.LastActive, | ||||
|                 LastSidReroll: user.LastSidReroll, | ||||
|                 Suspended: user is { Deleted: true, DeletedBy: not null }, | ||||
|                 Deleted: user.Deleted, | ||||
|                 AuthMethods: showSensitiveData | ||||
|                     ? authMethods.Select(UserRendererService.RenderAuthMethod) | ||||
|                     : null | ||||
|             ) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("{id}/sensitive")] | ||||
|     public async Task<IActionResult> QuerySensitiveUserDataAsync( | ||||
|         Snowflake id, | ||||
|         [FromBody] QuerySensitiveUserDataRequest req | ||||
|     ) | ||||
|     { | ||||
|         User user = await db.ResolveUserAsync(id); | ||||
| 
 | ||||
|         AuditLogEntry entry = await moderationService.QuerySensitiveDataAsync( | ||||
|             CurrentUser!, | ||||
|             user, | ||||
|             req.Reason | ||||
|         ); | ||||
| 
 | ||||
|         return Ok(moderationRenderer.RenderAuditLogEntry(entry)); | ||||
|     } | ||||
| } | ||||
|  | @ -41,4 +41,5 @@ public enum AuditLogEntryType | |||
|     WarnUser, | ||||
|     WarnUserAndClearProfile, | ||||
|     SuspendUser, | ||||
|     QuerySensitiveUserData, | ||||
| } | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ using Foxnouns.Backend.Database; | |||
| using Foxnouns.Backend.Database.Models; | ||||
| using Newtonsoft.Json; | ||||
| using Newtonsoft.Json.Linq; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
|  | @ -94,3 +95,18 @@ public enum FieldsToClear | |||
|     Flags, | ||||
|     CustomPreferences, | ||||
| } | ||||
| 
 | ||||
| public record QueryUsersRequest(string Query, bool Fuzzy); | ||||
| 
 | ||||
| public record QueryUserResponse( | ||||
|     UserResponse User, | ||||
|     bool MemberListHidden, | ||||
|     Instant LastActive, | ||||
|     Instant LastSidReroll, | ||||
|     bool Suspended, | ||||
|     bool Deleted, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|         IEnumerable<AuthMethodResponse>? AuthMethods | ||||
| ); | ||||
| 
 | ||||
| public record QuerySensitiveUserDataRequest(string Reason); | ||||
|  |  | |||
|  | @ -18,6 +18,7 @@ using Foxnouns.Backend.Database.Models; | |||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Jobs; | ||||
| using Humanizer; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Services; | ||||
|  | @ -63,6 +64,54 @@ public class ModerationService( | |||
|         return entry; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<AuditLogEntry> QuerySensitiveDataAsync( | ||||
|         User moderator, | ||||
|         User target, | ||||
|         string reason | ||||
|     ) | ||||
|     { | ||||
|         _logger.Information( | ||||
|             "Moderator {ModeratorId} is querying sensitive data for {TargetId}", | ||||
|             moderator.Id, | ||||
|             target.Id | ||||
|         ); | ||||
| 
 | ||||
|         var entry = new AuditLogEntry | ||||
|         { | ||||
|             Id = snowflakeGenerator.GenerateSnowflake(), | ||||
|             ModeratorId = moderator.Id, | ||||
|             ModeratorUsername = moderator.Username, | ||||
|             TargetUserId = target.Id, | ||||
|             TargetUsername = target.Username, | ||||
|             Type = AuditLogEntryType.QuerySensitiveUserData, | ||||
|             Reason = reason, | ||||
|         }; | ||||
|         db.AuditLog.Add(entry); | ||||
| 
 | ||||
|         await db.SaveChangesAsync(); | ||||
|         return entry; | ||||
|     } | ||||
| 
 | ||||
|     public async Task<bool> ShowSensitiveDataAsync( | ||||
|         User moderator, | ||||
|         User target, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|         Snowflake cutoff = snowflakeGenerator.GenerateSnowflake( | ||||
|             clock.GetCurrentInstant() - Duration.FromDays(1) | ||||
|         ); | ||||
| 
 | ||||
|         return await db.AuditLog.AnyAsync( | ||||
|             e => | ||||
|                 e.ModeratorId == moderator.Id | ||||
|                 && e.TargetUserId == target.Id | ||||
|                 && e.Type == AuditLogEntryType.QuerySensitiveUserData | ||||
|                 && e.Id > cutoff, | ||||
|             ct | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public async Task<AuditLogEntry> ExecuteSuspensionAsync( | ||||
|         User moderator, | ||||
|         User target, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue