refactor(backend): move all request/response types to a new Dto namespace
This commit is contained in:
		
							parent
							
								
									f8e6032449
								
							
						
					
					
						commit
						8bd4449804
					
				
					 21 changed files with 310 additions and 316 deletions
				
			
		|  | @ -2,14 +2,12 @@ using System.Net; | |||
| using System.Web; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Extensions; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Controllers.Authentication; | ||||
| 
 | ||||
|  | @ -47,28 +45,6 @@ public class AuthController( | |||
|         return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); | ||||
|     } | ||||
| 
 | ||||
|     private record UrlsResponse(bool EmailEnabled, string? Discord, string? Google, string? Tumblr); | ||||
| 
 | ||||
|     public record AuthResponse( | ||||
|         UserRendererService.UserResponse User, | ||||
|         string Token, | ||||
|         Instant ExpiresAt | ||||
|     ); | ||||
| 
 | ||||
|     public record SingleUrlResponse(string Url); | ||||
| 
 | ||||
|     public record AddOauthAccountResponse( | ||||
|         Snowflake Id, | ||||
|         [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, | ||||
|         string RemoteId, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|             string? RemoteUsername | ||||
|     ); | ||||
| 
 | ||||
|     public record OauthRegisterRequest(string Ticket, string Username); | ||||
| 
 | ||||
|     public record CallbackRequest(string Code, string State); | ||||
| 
 | ||||
|     [HttpPost("force-log-out")] | ||||
|     [Authorize("identify")] | ||||
|     public async Task<IActionResult> ForceLogoutAsync() | ||||
|  | @ -83,9 +59,7 @@ public class AuthController( | |||
| 
 | ||||
|     [HttpGet("methods/{id}")] | ||||
|     [Authorize("*")] | ||||
|     [ProducesResponseType<UserRendererService.AuthMethodResponse>( | ||||
|         statusCode: StatusCodes.Status200OK | ||||
|     )] | ||||
|     [ProducesResponseType<AuthMethodResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetAuthMethodAsync(Snowflake id) | ||||
|     { | ||||
|         AuthMethod? authMethod = await db | ||||
|  | @ -143,13 +117,3 @@ public class AuthController( | |||
|         return NoContent(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| public record CallbackResponse( | ||||
|     bool HasAccount, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|         UserRendererService.UserResponse? User, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt | ||||
| ); | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ using System.Web; | |||
| using EntityFramework.Exceptions.Common; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Extensions; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
|  | @ -29,7 +30,7 @@ public class DiscordAuthController( | |||
| 
 | ||||
|     [HttpPost("callback")] | ||||
|     [ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> CallbackAsync([FromBody] AuthController.CallbackRequest req) | ||||
|     public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req) | ||||
|     { | ||||
|         CheckRequirements(); | ||||
|         await keyCacheService.ValidateAuthStateAsync(req.State); | ||||
|  | @ -58,10 +59,8 @@ public class DiscordAuthController( | |||
|     } | ||||
| 
 | ||||
|     [HttpPost("register")] | ||||
|     [ProducesResponseType<AuthController.AuthResponse>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RegisterAsync( | ||||
|         [FromBody] AuthController.OauthRegisterRequest req | ||||
|     ) | ||||
|     [ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req) | ||||
|     { | ||||
|         RemoteAuthService.RemoteUser? remoteUser = | ||||
|             await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>( | ||||
|  | @ -109,14 +108,12 @@ public class DiscordAuthController( | |||
|             + $"&prompt=none&state={state}" | ||||
|             + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; | ||||
| 
 | ||||
|         return Ok(new AuthController.SingleUrlResponse(url)); | ||||
|         return Ok(new SingleUrlResponse(url)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("add-account/callback")] | ||||
|     [Authorize("*")] | ||||
|     public async Task<IActionResult> AddAccountCallbackAsync( | ||||
|         [FromBody] AuthController.CallbackRequest req | ||||
|     ) | ||||
|     public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req) | ||||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|  | @ -144,7 +141,7 @@ public class DiscordAuthController( | |||
|             ); | ||||
| 
 | ||||
|             return Ok( | ||||
|                 new AuthController.AddOauthAccountResponse( | ||||
|                 new AddOauthAccountResponse( | ||||
|                     authMethod.Id, | ||||
|                     AuthType.Discord, | ||||
|                     authMethod.RemoteId, | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ using System.Net; | |||
| using EntityFramework.Exceptions.Common; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Extensions; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
|  | @ -30,7 +31,7 @@ public class EmailAuthController( | |||
| 
 | ||||
|     [HttpPost("register/init")] | ||||
|     public async Task<IActionResult> RegisterInitAsync( | ||||
|         [FromBody] RegisterRequest req, | ||||
|         [FromBody] EmailRegisterRequest req, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|  | @ -73,7 +74,7 @@ public class EmailAuthController( | |||
| 
 | ||||
|     [HttpPost("register")] | ||||
|     public async Task<IActionResult> CompleteRegistrationAsync( | ||||
|         [FromBody] CompleteRegistrationRequest req | ||||
|         [FromBody] EmailCompleteRegistrationRequest req | ||||
|     ) | ||||
|     { | ||||
|         CheckRequirements(); | ||||
|  | @ -102,7 +103,7 @@ public class EmailAuthController( | |||
|         await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}"); | ||||
| 
 | ||||
|         return Ok( | ||||
|             new AuthController.AuthResponse( | ||||
|             new AuthResponse( | ||||
|                 await userRenderer.RenderUserAsync(user, user, renderMembers: false), | ||||
|                 tokenStr, | ||||
|                 token.ExpiresAt | ||||
|  | @ -111,9 +112,9 @@ public class EmailAuthController( | |||
|     } | ||||
| 
 | ||||
|     [HttpPost("login")] | ||||
|     [ProducesResponseType<AuthController.AuthResponse>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> LoginAsync( | ||||
|         [FromBody] LoginRequest req, | ||||
|         [FromBody] EmailLoginRequest req, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|  | @ -141,7 +142,7 @@ public class EmailAuthController( | |||
|         await db.SaveChangesAsync(ct); | ||||
| 
 | ||||
|         return Ok( | ||||
|             new AuthController.AuthResponse( | ||||
|             new AuthResponse( | ||||
|                 await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), | ||||
|                 tokenStr, | ||||
|                 token.ExpiresAt | ||||
|  | @ -151,7 +152,7 @@ public class EmailAuthController( | |||
| 
 | ||||
|     [HttpPost("change-password")] | ||||
|     [Authorize("*")] | ||||
|     public async Task<IActionResult> UpdatePasswordAsync([FromBody] ChangePasswordRequest req) | ||||
|     public async Task<IActionResult> UpdatePasswordAsync([FromBody] EmailChangePasswordRequest req) | ||||
|     { | ||||
|         if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Current)) | ||||
|             throw new ApiError.Forbidden("Invalid password"); | ||||
|  | @ -211,7 +212,7 @@ public class EmailAuthController( | |||
| 
 | ||||
|     [HttpPost("add-email/callback")] | ||||
|     [Authorize("*")] | ||||
|     public async Task<IActionResult> AddEmailCallbackAsync([FromBody] CallbackRequest req) | ||||
|     public async Task<IActionResult> AddEmailCallbackAsync([FromBody] EmailCallbackRequest req) | ||||
|     { | ||||
|         CheckRequirements(); | ||||
| 
 | ||||
|  | @ -233,7 +234,7 @@ public class EmailAuthController( | |||
|             ); | ||||
| 
 | ||||
|             return Ok( | ||||
|                 new AuthController.AddOauthAccountResponse( | ||||
|                 new AddOauthAccountResponse( | ||||
|                     authMethod.Id, | ||||
|                     AuthType.Email, | ||||
|                     authMethod.RemoteId, | ||||
|  | @ -258,14 +259,4 @@ public class EmailAuthController( | |||
|         if (!config.EmailAuth.Enabled) | ||||
|             throw new ApiError.BadRequest("Email authentication is not enabled on this instance."); | ||||
|     } | ||||
| 
 | ||||
|     public record LoginRequest(string Email, string Password); | ||||
| 
 | ||||
|     public record RegisterRequest(string Email); | ||||
| 
 | ||||
|     public record CompleteRegistrationRequest(string Ticket, string Username, string Password); | ||||
| 
 | ||||
|     public record CallbackRequest(string State); | ||||
| 
 | ||||
|     public record ChangePasswordRequest(string Current, string New); | ||||
| } | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ using System.Net; | |||
| using EntityFramework.Exceptions.Common; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Foxnouns.Backend.Services.Auth; | ||||
|  | @ -25,7 +26,7 @@ public class FediverseAuthController( | |||
|     private readonly ILogger _logger = logger.ForContext<FediverseAuthController>(); | ||||
| 
 | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType<AuthController.SingleUrlResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<SingleUrlResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetFediverseUrlAsync( | ||||
|         [FromQuery] string instance, | ||||
|         [FromQuery] bool forceRefresh = false | ||||
|  | @ -35,12 +36,12 @@ public class FediverseAuthController( | |||
|             throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); | ||||
| 
 | ||||
|         string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); | ||||
|         return Ok(new AuthController.SingleUrlResponse(url)); | ||||
|         return Ok(new SingleUrlResponse(url)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("callback")] | ||||
|     [ProducesResponseType<CallbackResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> FediverseCallbackAsync([FromBody] CallbackRequest req) | ||||
|     public async Task<IActionResult> FediverseCallbackAsync([FromBody] FediverseCallbackRequest req) | ||||
|     { | ||||
|         FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); | ||||
|         FediverseAuthService.FediverseUser remoteUser = | ||||
|  | @ -74,10 +75,8 @@ public class FediverseAuthController( | |||
|     } | ||||
| 
 | ||||
|     [HttpPost("register")] | ||||
|     [ProducesResponseType<AuthController.AuthResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RegisterAsync( | ||||
|         [FromBody] AuthController.OauthRegisterRequest req | ||||
|     ) | ||||
|     [ProducesResponseType<AuthResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req) | ||||
|     { | ||||
|         FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>( | ||||
|             $"fediverse:{req.Ticket}", | ||||
|  | @ -138,12 +137,14 @@ public class FediverseAuthController( | |||
|         ); | ||||
| 
 | ||||
|         string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); | ||||
|         return Ok(new AuthController.SingleUrlResponse(url)); | ||||
|         return Ok(new SingleUrlResponse(url)); | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("add-account/callback")] | ||||
|     [Authorize("*")] | ||||
|     public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req) | ||||
|     public async Task<IActionResult> AddAccountCallbackAsync( | ||||
|         [FromBody] FediverseCallbackRequest req | ||||
|     ) | ||||
|     { | ||||
|         await remoteAuthService.ValidateAddAccountStateAsync( | ||||
|             req.State, | ||||
|  | @ -171,7 +172,7 @@ public class FediverseAuthController( | |||
|             ); | ||||
| 
 | ||||
|             return Ok( | ||||
|                 new AuthController.AddOauthAccountResponse( | ||||
|                 new AddOauthAccountResponse( | ||||
|                     authMethod.Id, | ||||
|                     AuthType.Fediverse, | ||||
|                     authMethod.RemoteId, | ||||
|  | @ -189,8 +190,6 @@ public class FediverseAuthController( | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public record CallbackRequest(string Instance, string Code, string State); | ||||
| 
 | ||||
|     private record FediverseTicketData( | ||||
|         Snowflake ApplicationId, | ||||
|         FediverseAuthService.FediverseUser User | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| using Coravel.Queuing.Interfaces; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Jobs; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
|  | @ -43,8 +44,6 @@ public class ExportsController( | |||
|     private string ExportUrl(Snowflake userId, string filename) => | ||||
|         $"{config.MediaBaseUrl}/data-exports/{userId}/{filename}.zip"; | ||||
| 
 | ||||
|     private record DataExportResponse(string? Url, Instant? ExpiresAt); | ||||
| 
 | ||||
|     [HttpPost] | ||||
|     public async Task<IActionResult> QueueDataExportAsync() | ||||
|     { | ||||
|  |  | |||
|  | @ -1,50 +1,49 @@ | |||
| using Coravel.Queuing.Interfaces; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Extensions; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Jobs; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.EntityFrameworkCore.Storage; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Controllers; | ||||
| 
 | ||||
| [Route("/api/v2/users/@me/flags")] | ||||
| public class FlagsController( | ||||
|     ILogger logger, | ||||
|     DatabaseContext db, | ||||
|     UserRendererService userRenderer, | ||||
|     ObjectStorageService objectStorageService, | ||||
|     ISnowflakeGenerator snowflakeGenerator, | ||||
|     IQueue queue | ||||
| ) : ApiControllerBase | ||||
| { | ||||
|     private readonly ILogger _logger = logger.ForContext<FlagsController>(); | ||||
| 
 | ||||
|     [HttpGet] | ||||
|     [Authorize("identify")] | ||||
|     [ProducesResponseType<IEnumerable<UserRendererService.PrideFlagResponse>>( | ||||
|         statusCode: StatusCodes.Status200OK | ||||
|     )] | ||||
|     [ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default) | ||||
|     { | ||||
|         List<PrideFlag> flags = await db | ||||
|             .PrideFlags.Where(f => f.UserId == CurrentUser!.Id) | ||||
|             .OrderBy(f => f.Name) | ||||
|             .ThenBy(f => f.Id) | ||||
|             .ToListAsync(ct); | ||||
| 
 | ||||
|         return Ok(flags.Select(userRenderer.RenderPrideFlag)); | ||||
|     } | ||||
| 
 | ||||
|     public const int MaxFlagCount = 500; | ||||
| 
 | ||||
|     [HttpPost] | ||||
|     [Authorize("user.update")] | ||||
|     [ProducesResponseType<UserRendererService.PrideFlagResponse>( | ||||
|         statusCode: StatusCodes.Status202Accepted | ||||
|     )] | ||||
|     public IActionResult CreateFlag([FromBody] CreateFlagRequest req) | ||||
|     [ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status202Accepted)] | ||||
|     public async Task<IActionResult> CreateFlagAsync([FromBody] CreateFlagRequest req) | ||||
|     { | ||||
|         int flagCount = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).CountAsync(); | ||||
|         if (flagCount >= MaxFlagCount) | ||||
|             throw new ApiError.BadRequest("Maximum number of flags reached"); | ||||
| 
 | ||||
|         ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); | ||||
| 
 | ||||
|         Snowflake id = snowflakeGenerator.GenerateSnowflake(); | ||||
|  | @ -56,10 +55,6 @@ public class FlagsController( | |||
|         return Accepted(new CreateFlagResponse(id, req.Name, req.Description)); | ||||
|     } | ||||
| 
 | ||||
|     public record CreateFlagRequest(string Name, string Image, string? Description); | ||||
| 
 | ||||
|     private record CreateFlagResponse(Snowflake Id, string Name, string? Description); | ||||
| 
 | ||||
|     [HttpPatch("{id}")] | ||||
|     [Authorize("user.update")] | ||||
|     public async Task<IActionResult> UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) | ||||
|  | @ -84,52 +79,19 @@ public class FlagsController( | |||
|         return Ok(userRenderer.RenderPrideFlag(flag)); | ||||
|     } | ||||
| 
 | ||||
|     public class UpdateFlagRequest : PatchRequest | ||||
|     { | ||||
|         public string? Name { get; init; } | ||||
|         public string? Description { get; init; } | ||||
|     } | ||||
| 
 | ||||
|     [HttpDelete("{id}")] | ||||
|     [Authorize("user.update")] | ||||
|     public async Task<IActionResult> DeleteFlagAsync(Snowflake id) | ||||
|     { | ||||
|         await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(); | ||||
| 
 | ||||
|         PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => | ||||
|             f.Id == id && f.UserId == CurrentUser!.Id | ||||
|         ); | ||||
|         if (flag == null) | ||||
|             throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); | ||||
| 
 | ||||
|         string hash = flag.Hash; | ||||
| 
 | ||||
|         db.PrideFlags.Remove(flag); | ||||
|         await db.SaveChangesAsync(); | ||||
| 
 | ||||
|         int flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); | ||||
|         if (flagCount == 0) | ||||
|         { | ||||
|             try | ||||
|             { | ||||
|                 _logger.Information( | ||||
|                     "Deleting flag file {Hash} as it is no longer used by any flags", | ||||
|                     hash | ||||
|                 ); | ||||
|                 await objectStorageService.DeleteFlagAsync(hash); | ||||
|             } | ||||
|             catch (Exception e) | ||||
|             { | ||||
|                 _logger.Error(e, "Error deleting flag file {Hash}", hash); | ||||
|             } | ||||
|         } | ||||
|         else | ||||
|         { | ||||
|             _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); | ||||
|         } | ||||
| 
 | ||||
|         await tx.CommitAsync(); | ||||
| 
 | ||||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| using System.Text.RegularExpressions; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| using Microsoft.AspNetCore.Mvc.Controllers; | ||||
|  | @ -63,10 +64,6 @@ public partial class InternalController(DatabaseContext db) : ControllerBase | |||
|         return Ok(new RequestDataResponse(userId, template)); | ||||
|     } | ||||
| 
 | ||||
|     public record RequestDataRequest(string? Token, string Method, string Path); | ||||
| 
 | ||||
|     public record RequestDataResponse(Snowflake? UserId, string Template); | ||||
| 
 | ||||
|     private static RouteEndpoint? GetEndpoint( | ||||
|         HttpContext httpContext, | ||||
|         string url, | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ using Coravel.Queuing.Interfaces; | |||
| using EntityFramework.Exceptions.Common; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Extensions; | ||||
| using Foxnouns.Backend.Jobs; | ||||
| using Foxnouns.Backend.Middleware; | ||||
|  | @ -28,9 +29,7 @@ public class MembersController( | |||
|     private readonly ILogger _logger = logger.ForContext<MembersController>(); | ||||
| 
 | ||||
|     [HttpGet] | ||||
|     [ProducesResponseType<IEnumerable<MemberRendererService.PartialMember>>( | ||||
|         StatusCodes.Status200OK | ||||
|     )] | ||||
|     [ProducesResponseType<IEnumerable<PartialMember>>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default) | ||||
|     { | ||||
|         User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); | ||||
|  | @ -38,7 +37,7 @@ public class MembersController( | |||
|     } | ||||
| 
 | ||||
|     [HttpGet("{memberRef}")] | ||||
|     [ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetMemberAsync( | ||||
|         string userRef, | ||||
|         string memberRef, | ||||
|  | @ -52,7 +51,7 @@ public class MembersController( | |||
|     public const int MaxMemberCount = 500; | ||||
| 
 | ||||
|     [HttpPost("/api/v2/users/@me/members")] | ||||
|     [ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)] | ||||
|     [Authorize("member.create")] | ||||
|     public async Task<IActionResult> CreateMemberAsync( | ||||
|         [FromBody] CreateMemberRequest req, | ||||
|  | @ -246,20 +245,6 @@ public class MembersController( | |||
|         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; } | ||||
|         public bool? Unlisted { get; init; } | ||||
|     } | ||||
| 
 | ||||
|     [HttpDelete("/api/v2/users/@me/members/{memberRef}")] | ||||
|     [Authorize("member.update")] | ||||
|     public async Task<IActionResult> DeleteMemberAsync(string memberRef) | ||||
|  | @ -282,21 +267,9 @@ public class MembersController( | |||
|         return NoContent(); | ||||
|     } | ||||
| 
 | ||||
|     public record CreateMemberRequest( | ||||
|         string Name, | ||||
|         string? DisplayName, | ||||
|         string? Bio, | ||||
|         string? Avatar, | ||||
|         bool? Unlisted, | ||||
|         string[]? Links, | ||||
|         List<FieldEntry>? Names, | ||||
|         List<Pronoun>? Pronouns, | ||||
|         List<Field>? Fields | ||||
|     ); | ||||
| 
 | ||||
|     [HttpPost("/api/v2/users/@me/members/{memberRef}/reroll-sid")] | ||||
|     [Authorize("member.update")] | ||||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<MemberResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RerollSidAsync(string memberRef) | ||||
|     { | ||||
|         Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Mvc; | ||||
| 
 | ||||
|  | @ -17,17 +18,18 @@ public class MetaController : ApiControllerBase | |||
|                 BuildInfo.Version, | ||||
|                 BuildInfo.Hash, | ||||
|                 (int)FoxnounsMetrics.MemberCount.Value, | ||||
|                 new UserInfo( | ||||
|                 new UserInfoResponse( | ||||
|                     (int)FoxnounsMetrics.UsersCount.Value, | ||||
|                     (int)FoxnounsMetrics.UsersActiveMonthCount.Value, | ||||
|                     (int)FoxnounsMetrics.UsersActiveWeekCount.Value, | ||||
|                     (int)FoxnounsMetrics.UsersActiveDayCount.Value | ||||
|                 ), | ||||
|                 new Limits( | ||||
|                 new LimitsResponse( | ||||
|                     MembersController.MaxMemberCount, | ||||
|                     ValidationUtils.MaxBioLength, | ||||
|                     ValidationUtils.MaxCustomPreferences, | ||||
|                     AuthUtils.MaxAuthMethodsPerType | ||||
|                     AuthUtils.MaxAuthMethodsPerType, | ||||
|                     FlagsController.MaxFlagCount | ||||
|                 ) | ||||
|             ) | ||||
|         ); | ||||
|  | @ -35,23 +37,4 @@ public class MetaController : ApiControllerBase | |||
|     [HttpGet("/api/v2/coffee")] | ||||
|     public IActionResult BrewCoffee() => | ||||
|         Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); | ||||
| 
 | ||||
|     private record MetaResponse( | ||||
|         string Repository, | ||||
|         string Version, | ||||
|         string Hash, | ||||
|         int Members, | ||||
|         UserInfo Users, | ||||
|         Limits Limits | ||||
|     ); | ||||
| 
 | ||||
|     private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); | ||||
| 
 | ||||
|     // All limits that the frontend should know about (for UI purposes) | ||||
|     private record Limits( | ||||
|         int MemberCount, | ||||
|         int BioLength, | ||||
|         int CustomPreferences, | ||||
|         int MaxAuthMethods | ||||
|     ); | ||||
| } | ||||
|  |  | |||
|  | @ -1,8 +1,8 @@ | |||
| using System.Diagnostics.CodeAnalysis; | ||||
| using Coravel.Queuing.Interfaces; | ||||
| using EntityFramework.Exceptions.Common; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Jobs; | ||||
| using Foxnouns.Backend.Middleware; | ||||
| using Foxnouns.Backend.Services; | ||||
|  | @ -27,7 +27,7 @@ public class UsersController( | |||
|     private readonly ILogger _logger = logger.ForContext<UsersController>(); | ||||
| 
 | ||||
|     [HttpGet("{userRef}")] | ||||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default) | ||||
|     { | ||||
|         User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); | ||||
|  | @ -38,7 +38,7 @@ public class UsersController( | |||
| 
 | ||||
|     [HttpPatch("@me")] | ||||
|     [Authorize("user.update")] | ||||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> UpdateUserAsync( | ||||
|         [FromBody] UpdateUserRequest req, | ||||
|         CancellationToken ct = default | ||||
|  | @ -196,7 +196,7 @@ public class UsersController( | |||
|     [Authorize("user.update")] | ||||
|     [ProducesResponseType<Dictionary<Snowflake, User.CustomPreference>>(StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> UpdateCustomPreferencesAsync( | ||||
|         [FromBody] List<CustomPreferenceUpdate> req, | ||||
|         [FromBody] List<CustomPreferenceUpdateRequest> req, | ||||
|         CancellationToken ct = default | ||||
|     ) | ||||
|     { | ||||
|  | @ -207,7 +207,7 @@ public class UsersController( | |||
|             .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) | ||||
|             .ToDictionary(); | ||||
| 
 | ||||
|         foreach (CustomPreferenceUpdate? r in req) | ||||
|         foreach (CustomPreferenceUpdateRequest? r in req) | ||||
|         { | ||||
|             if (r.Id != null && preferences.ContainsKey(r.Id.Value)) | ||||
|             { | ||||
|  | @ -239,33 +239,6 @@ public class UsersController( | |||
|         return Ok(user.CustomPreferences); | ||||
|     } | ||||
| 
 | ||||
|     [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] | ||||
|     public class CustomPreferenceUpdate | ||||
|     { | ||||
|         public Snowflake? Id { get; init; } | ||||
|         public required string Icon { get; set; } | ||||
|         public required string Tooltip { get; set; } | ||||
|         public PreferenceSize Size { get; set; } | ||||
|         public bool Muted { get; set; } | ||||
|         public bool Favourite { get; set; } | ||||
|     } | ||||
| 
 | ||||
|     public class UpdateUserRequest : PatchRequest | ||||
|     { | ||||
|         public string? Username { 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; } | ||||
|         public string? MemberTitle { get; init; } | ||||
|         public bool? MemberListHidden { get; init; } | ||||
|         public string? Timezone { get; init; } | ||||
|     } | ||||
| 
 | ||||
|     [HttpGet("@me/settings")] | ||||
|     [Authorize("user.read_hidden")] | ||||
|     [ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)] | ||||
|  | @ -294,14 +267,9 @@ public class UsersController( | |||
|         return Ok(user.Settings); | ||||
|     } | ||||
| 
 | ||||
|     public class UpdateUserSettingsRequest : PatchRequest | ||||
|     { | ||||
|         public bool? DarkMode { get; init; } | ||||
|     } | ||||
| 
 | ||||
|     [HttpPost("@me/reroll-sid")] | ||||
|     [Authorize("user.update")] | ||||
|     [ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     [ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)] | ||||
|     public async Task<IActionResult> RerollSidAsync() | ||||
|     { | ||||
|         Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); | ||||
|  |  | |||
							
								
								
									
										47
									
								
								Foxnouns.Backend/Dto/Auth.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								Foxnouns.Backend/Dto/Auth.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| // ReSharper disable ClassNeverInstantiated.Global | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record CallbackResponse( | ||||
|     bool HasAccount, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] UserResponse? User, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt | ||||
| ); | ||||
| 
 | ||||
| public record UrlsResponse(bool EmailEnabled, string? Discord, string? Google, string? Tumblr); | ||||
| 
 | ||||
| public record AuthResponse(UserResponse User, string Token, Instant ExpiresAt); | ||||
| 
 | ||||
| public record SingleUrlResponse(string Url); | ||||
| 
 | ||||
| public record AddOauthAccountResponse( | ||||
|     Snowflake Id, | ||||
|     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, | ||||
|     string RemoteId, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername | ||||
| ); | ||||
| 
 | ||||
| public record OauthRegisterRequest(string Ticket, string Username); | ||||
| 
 | ||||
| public record CallbackRequest(string Code, string State); | ||||
| 
 | ||||
| public record EmailLoginRequest(string Email, string Password); | ||||
| 
 | ||||
| public record EmailRegisterRequest(string Email); | ||||
| 
 | ||||
| public record EmailCompleteRegistrationRequest(string Ticket, string Username, string Password); | ||||
| 
 | ||||
| public record EmailCallbackRequest(string State); | ||||
| 
 | ||||
| public record EmailChangePasswordRequest(string Current, string New); | ||||
| 
 | ||||
| public record FediverseCallbackRequest(string Instance, string Code, string State); | ||||
							
								
								
									
										6
									
								
								Foxnouns.Backend/Dto/DataExport.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								Foxnouns.Backend/Dto/DataExport.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | |||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record DataExportResponse(string? Url, Instant? ExpiresAt); | ||||
							
								
								
									
										17
									
								
								Foxnouns.Backend/Dto/Flag.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								Foxnouns.Backend/Dto/Flag.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | |||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Utils; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record PrideFlagResponse(Snowflake Id, string ImageUrl, string Name, string? Description); | ||||
| 
 | ||||
| public record CreateFlagRequest(string Name, string Image, string? Description); | ||||
| 
 | ||||
| public record CreateFlagResponse(Snowflake Id, string Name, string? Description); | ||||
| 
 | ||||
| public class UpdateFlagRequest : PatchRequest | ||||
| { | ||||
|     public string? Name { get; init; } | ||||
|     public string? Description { get; init; } | ||||
| } | ||||
							
								
								
									
										8
									
								
								Foxnouns.Backend/Dto/Internal.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Foxnouns.Backend/Dto/Internal.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| using Foxnouns.Backend.Database; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record RequestDataRequest(string? Token, string Method, string Path); | ||||
| 
 | ||||
| public record RequestDataResponse(Snowflake? UserId, string Template); | ||||
							
								
								
									
										61
									
								
								Foxnouns.Backend/Dto/Member.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								Foxnouns.Backend/Dto/Member.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | |||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Newtonsoft.Json; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record CreateMemberRequest( | ||||
|     string Name, | ||||
|     string? DisplayName, | ||||
|     string? Bio, | ||||
|     string? Avatar, | ||||
|     bool? Unlisted, | ||||
|     string[]? Links, | ||||
|     List<FieldEntry>? Names, | ||||
|     List<Pronoun>? Pronouns, | ||||
|     List<Field>? Fields | ||||
| ); | ||||
| 
 | ||||
| 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; } | ||||
|     public bool? Unlisted { get; init; } | ||||
| } | ||||
| 
 | ||||
| public record PartialMember( | ||||
|     Snowflake Id, | ||||
|     string Sid, | ||||
|     string Name, | ||||
|     string DisplayName, | ||||
|     string? Bio, | ||||
|     string? AvatarUrl, | ||||
|     IEnumerable<FieldEntry> Names, | ||||
|     IEnumerable<Pronoun> Pronouns, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted | ||||
| ); | ||||
| 
 | ||||
| public record MemberResponse( | ||||
|     Snowflake Id, | ||||
|     string Sid, | ||||
|     string Name, | ||||
|     string DisplayName, | ||||
|     string? Bio, | ||||
|     string? AvatarUrl, | ||||
|     string[] Links, | ||||
|     IEnumerable<FieldEntry> Names, | ||||
|     IEnumerable<Pronoun> Pronouns, | ||||
|     IEnumerable<Field> Fields, | ||||
|     IEnumerable<PrideFlagResponse> Flags, | ||||
|     PartialUser User, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted | ||||
| ); | ||||
							
								
								
									
										21
									
								
								Foxnouns.Backend/Dto/Meta.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								Foxnouns.Backend/Dto/Meta.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record MetaResponse( | ||||
|     string Repository, | ||||
|     string Version, | ||||
|     string Hash, | ||||
|     int Members, | ||||
|     UserInfoResponse Users, | ||||
|     LimitsResponse Limits | ||||
| ); | ||||
| 
 | ||||
| public record UserInfoResponse(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); | ||||
| 
 | ||||
| public record LimitsResponse( | ||||
|     int MemberCount, | ||||
|     int BioLength, | ||||
|     int CustomPreferences, | ||||
|     int MaxAuthMethods, | ||||
|     int MaxFlags | ||||
| ); | ||||
							
								
								
									
										82
									
								
								Foxnouns.Backend/Dto/User.cs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								Foxnouns.Backend/Dto/User.cs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,82 @@ | |||
| // ReSharper disable NotAccessedPositionalProperty.Global | ||||
| // ReSharper disable ClassNeverInstantiated.Global | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Newtonsoft.Json; | ||||
| using NodaTime; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| public record UserResponse( | ||||
|     Snowflake Id, | ||||
|     string Sid, | ||||
|     string Username, | ||||
|     string? DisplayName, | ||||
|     string? Bio, | ||||
|     string? MemberTitle, | ||||
|     string? AvatarUrl, | ||||
|     string[] Links, | ||||
|     IEnumerable<FieldEntry> Names, | ||||
|     IEnumerable<Pronoun> Pronouns, | ||||
|     IEnumerable<Field> Fields, | ||||
|     Dictionary<Snowflake, User.CustomPreference> CustomPreferences, | ||||
|     IEnumerable<PrideFlagResponse> Flags, | ||||
|     int? UtcOffset, | ||||
|     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|         IEnumerable<PartialMember>? Members, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|         IEnumerable<AuthMethodResponse>? AuthMethods, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? MemberListHidden, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastSidReroll, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Timezone | ||||
| ); | ||||
| 
 | ||||
| public record AuthMethodResponse( | ||||
|     Snowflake Id, | ||||
|     [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, | ||||
|     string RemoteId, | ||||
|     [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername | ||||
| ); | ||||
| 
 | ||||
| public record PartialUser( | ||||
|     Snowflake Id, | ||||
|     string Sid, | ||||
|     string Username, | ||||
|     string? DisplayName, | ||||
|     string? AvatarUrl, | ||||
|     Dictionary<Snowflake, User.CustomPreference> CustomPreferences | ||||
| ); | ||||
| 
 | ||||
| public class UpdateUserSettingsRequest : PatchRequest | ||||
| { | ||||
|     public bool? DarkMode { get; init; } | ||||
| } | ||||
| 
 | ||||
| public class CustomPreferenceUpdateRequest | ||||
| { | ||||
|     public Snowflake? Id { get; init; } | ||||
|     public required string Icon { get; set; } | ||||
|     public required string Tooltip { get; set; } | ||||
|     public PreferenceSize Size { get; set; } | ||||
|     public bool Muted { get; set; } | ||||
|     public bool Favourite { get; set; } | ||||
| } | ||||
| 
 | ||||
| public class UpdateUserRequest : PatchRequest | ||||
| { | ||||
|     public string? Username { 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; } | ||||
|     public string? MemberTitle { get; init; } | ||||
|     public bool? MemberListHidden { get; init; } | ||||
|     public string? Timezone { get; init; } | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| using System.Security.Cryptography; | ||||
| using Foxnouns.Backend.Controllers.Authentication; | ||||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.AspNetCore.Identity; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Newtonsoft.Json; | ||||
|  | @ -49,7 +50,7 @@ public class MemberRendererService(DatabaseContext db, Config config) | |||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private UserRendererService.PartialUser RenderPartialUser(User user) => | ||||
|     private PartialUser RenderPartialUser(User user) => | ||||
|         new( | ||||
|             user.Id, | ||||
|             user.Sid, | ||||
|  | @ -84,34 +85,6 @@ public class MemberRendererService(DatabaseContext db, Config config) | |||
| 
 | ||||
|     private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; | ||||
| 
 | ||||
|     private UserRendererService.PrideFlagResponse RenderPrideFlag(PrideFlag flag) => | ||||
|     private PrideFlagResponse RenderPrideFlag(PrideFlag flag) => | ||||
|         new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); | ||||
| 
 | ||||
|     public record PartialMember( | ||||
|         Snowflake Id, | ||||
|         string Sid, | ||||
|         string Name, | ||||
|         string DisplayName, | ||||
|         string? Bio, | ||||
|         string? AvatarUrl, | ||||
|         IEnumerable<FieldEntry> Names, | ||||
|         IEnumerable<Pronoun> Pronouns, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted | ||||
|     ); | ||||
| 
 | ||||
|     public record MemberResponse( | ||||
|         Snowflake Id, | ||||
|         string Sid, | ||||
|         string Name, | ||||
|         string DisplayName, | ||||
|         string? Bio, | ||||
|         string? AvatarUrl, | ||||
|         string[] Links, | ||||
|         IEnumerable<FieldEntry> Names, | ||||
|         IEnumerable<Pronoun> Pronouns, | ||||
|         IEnumerable<Field> Fields, | ||||
|         IEnumerable<UserRendererService.PrideFlagResponse> Flags, | ||||
|         UserRendererService.PartialUser User, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted | ||||
|     ); | ||||
| } | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| using Foxnouns.Backend.Database; | ||||
| using Foxnouns.Backend.Database.Models; | ||||
| using Foxnouns.Backend.Dto; | ||||
| using Foxnouns.Backend.Utils; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Newtonsoft.Json; | ||||
|  | @ -132,58 +133,6 @@ public class UserRendererService( | |||
| 
 | ||||
|     public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; | ||||
| 
 | ||||
|     public record UserResponse( | ||||
|         Snowflake Id, | ||||
|         string Sid, | ||||
|         string Username, | ||||
|         string? DisplayName, | ||||
|         string? Bio, | ||||
|         string? MemberTitle, | ||||
|         string? AvatarUrl, | ||||
|         string[] Links, | ||||
|         IEnumerable<FieldEntry> Names, | ||||
|         IEnumerable<Pronoun> Pronouns, | ||||
|         IEnumerable<Field> Fields, | ||||
|         Dictionary<Snowflake, User.CustomPreference> CustomPreferences, | ||||
|         IEnumerable<PrideFlagResponse> Flags, | ||||
|         int? UtcOffset, | ||||
|         [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|             IEnumerable<MemberRendererService.PartialMember>? Members, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|             IEnumerable<AuthMethodResponse>? AuthMethods, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|             bool? MemberListHidden, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|             Instant? LastSidReroll, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Timezone | ||||
|     ); | ||||
| 
 | ||||
|     public record AuthMethodResponse( | ||||
|         Snowflake Id, | ||||
|         [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, | ||||
|         string RemoteId, | ||||
|         [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] | ||||
|             string? RemoteUsername | ||||
|     ); | ||||
| 
 | ||||
|     public record PartialUser( | ||||
|         Snowflake Id, | ||||
|         string Sid, | ||||
|         string Username, | ||||
|         string? DisplayName, | ||||
|         string? AvatarUrl, | ||||
|         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 | ||||
|     ); | ||||
| } | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ | |||
| // along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
| 
 | ||||
| using Foxnouns.Backend.Controllers; | ||||
| using Foxnouns.Backend.Dto; | ||||
| 
 | ||||
| namespace Foxnouns.Backend.Utils; | ||||
| 
 | ||||
|  | @ -23,7 +24,7 @@ public static partial class ValidationUtils | |||
|     public const int MaxPreferenceTooltipLength = 128; | ||||
| 
 | ||||
|     public static List<(string, ValidationError?)> ValidateCustomPreferences( | ||||
|         List<UsersController.CustomPreferenceUpdate> preferences | ||||
|         List<CustomPreferenceUpdateRequest> preferences | ||||
|     ) | ||||
|     { | ||||
|         var errors = new List<(string, ValidationError?)>(); | ||||
|  | @ -46,11 +47,7 @@ public static partial class ValidationUtils | |||
|         if (preferences.Count > 50) | ||||
|             return errors; | ||||
| 
 | ||||
|         foreach ( | ||||
|             (UsersController.CustomPreferenceUpdate? p, int i) in preferences.Select( | ||||
|                 (p, i) => (p, i) | ||||
|             ) | ||||
|         ) | ||||
|         foreach ((CustomPreferenceUpdateRequest? p, int i) in preferences.Select((p, i) => (p, i))) | ||||
|         { | ||||
|             if (!BootstrapIcons.IsValid(p.Icon)) | ||||
|             { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue