From 12eddb99490e2c6331f874c1393d36c7d66283c9 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 27 Dec 2024 17:48:37 -0500 Subject: [PATCH] feat(backend): user lookup --- .../Moderation/LookupController.cs | 90 +++++++++++++++++++ .../Database/Models/AuditLogEntry.cs | 1 + Foxnouns.Backend/Dto/Moderation.cs | 16 ++++ .../Services/ModerationService.cs | 49 ++++++++++ 4 files changed, 156 insertions(+) create mode 100644 Foxnouns.Backend/Controllers/Moderation/LookupController.cs diff --git a/Foxnouns.Backend/Controllers/Moderation/LookupController.cs b/Foxnouns.Backend/Controllers/Moderation/LookupController.cs new file mode 100644 index 0000000..ba5018c --- /dev/null +++ b/Foxnouns.Backend/Controllers/Moderation/LookupController.cs @@ -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 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 QueryUserAsync(Snowflake id, CancellationToken ct = default) + { + User user = await db.ResolveUserAsync(id, ct); + + bool showSensitiveData = await moderationService.ShowSensitiveDataAsync( + CurrentUser!, + user, + ct + ); + + List 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 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)); + } +} diff --git a/Foxnouns.Backend/Database/Models/AuditLogEntry.cs b/Foxnouns.Backend/Database/Models/AuditLogEntry.cs index c65e675..84e1a43 100644 --- a/Foxnouns.Backend/Database/Models/AuditLogEntry.cs +++ b/Foxnouns.Backend/Database/Models/AuditLogEntry.cs @@ -41,4 +41,5 @@ public enum AuditLogEntryType WarnUser, WarnUserAndClearProfile, SuspendUser, + QuerySensitiveUserData, } diff --git a/Foxnouns.Backend/Dto/Moderation.cs b/Foxnouns.Backend/Dto/Moderation.cs index 7d6b5b8..266c275 100644 --- a/Foxnouns.Backend/Dto/Moderation.cs +++ b/Foxnouns.Backend/Dto/Moderation.cs @@ -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? AuthMethods +); + +public record QuerySensitiveUserDataRequest(string Reason); diff --git a/Foxnouns.Backend/Services/ModerationService.cs b/Foxnouns.Backend/Services/ModerationService.cs index 5444657..ff86a05 100644 --- a/Foxnouns.Backend/Services/ModerationService.cs +++ b/Foxnouns.Backend/Services/ModerationService.cs @@ -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 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 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 ExecuteSuspensionAsync( User moderator, User target,