refactor(backend): move all request/response types to a new Dto namespace

This commit is contained in:
sam 2024-12-08 20:17:30 +01:00
parent f8e6032449
commit 8bd4449804
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
21 changed files with 310 additions and 316 deletions

View file

@ -2,14 +2,12 @@ using System.Net;
using System.Web; using System.Web;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Extensions;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
using NodaTime;
namespace Foxnouns.Backend.Controllers.Authentication; namespace Foxnouns.Backend.Controllers.Authentication;
@ -47,28 +45,6 @@ public class AuthController(
return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); 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")] [HttpPost("force-log-out")]
[Authorize("identify")] [Authorize("identify")]
public async Task<IActionResult> ForceLogoutAsync() public async Task<IActionResult> ForceLogoutAsync()
@ -83,9 +59,7 @@ public class AuthController(
[HttpGet("methods/{id}")] [HttpGet("methods/{id}")]
[Authorize("*")] [Authorize("*")]
[ProducesResponseType<UserRendererService.AuthMethodResponse>( [ProducesResponseType<AuthMethodResponse>(statusCode: StatusCodes.Status200OK)]
statusCode: StatusCodes.Status200OK
)]
public async Task<IActionResult> GetAuthMethodAsync(Snowflake id) public async Task<IActionResult> GetAuthMethodAsync(Snowflake id)
{ {
AuthMethod? authMethod = await db AuthMethod? authMethod = await db
@ -143,13 +117,3 @@ public class AuthController(
return NoContent(); 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
);

View file

@ -3,6 +3,7 @@ using System.Web;
using EntityFramework.Exceptions.Common; using EntityFramework.Exceptions.Common;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Extensions;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
@ -29,7 +30,7 @@ public class DiscordAuthController(
[HttpPost("callback")] [HttpPost("callback")]
[ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)] [ProducesResponseType<CallbackResponse>(StatusCodes.Status200OK)]
public async Task<IActionResult> CallbackAsync([FromBody] AuthController.CallbackRequest req) public async Task<IActionResult> CallbackAsync([FromBody] CallbackRequest req)
{ {
CheckRequirements(); CheckRequirements();
await keyCacheService.ValidateAuthStateAsync(req.State); await keyCacheService.ValidateAuthStateAsync(req.State);
@ -58,10 +59,8 @@ public class DiscordAuthController(
} }
[HttpPost("register")] [HttpPost("register")]
[ProducesResponseType<AuthController.AuthResponse>(StatusCodes.Status200OK)] [ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)]
public async Task<IActionResult> RegisterAsync( public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req)
[FromBody] AuthController.OauthRegisterRequest req
)
{ {
RemoteAuthService.RemoteUser? remoteUser = RemoteAuthService.RemoteUser? remoteUser =
await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>( await keyCacheService.GetKeyAsync<RemoteAuthService.RemoteUser>(
@ -109,14 +108,12 @@ public class DiscordAuthController(
+ $"&prompt=none&state={state}" + $"&prompt=none&state={state}"
+ $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}";
return Ok(new AuthController.SingleUrlResponse(url)); return Ok(new SingleUrlResponse(url));
} }
[HttpPost("add-account/callback")] [HttpPost("add-account/callback")]
[Authorize("*")] [Authorize("*")]
public async Task<IActionResult> AddAccountCallbackAsync( public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req)
[FromBody] AuthController.CallbackRequest req
)
{ {
CheckRequirements(); CheckRequirements();
@ -144,7 +141,7 @@ public class DiscordAuthController(
); );
return Ok( return Ok(
new AuthController.AddOauthAccountResponse( new AddOauthAccountResponse(
authMethod.Id, authMethod.Id,
AuthType.Discord, AuthType.Discord,
authMethod.RemoteId, authMethod.RemoteId,

View file

@ -2,6 +2,7 @@ using System.Net;
using EntityFramework.Exceptions.Common; using EntityFramework.Exceptions.Common;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Extensions;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
@ -30,7 +31,7 @@ public class EmailAuthController(
[HttpPost("register/init")] [HttpPost("register/init")]
public async Task<IActionResult> RegisterInitAsync( public async Task<IActionResult> RegisterInitAsync(
[FromBody] RegisterRequest req, [FromBody] EmailRegisterRequest req,
CancellationToken ct = default CancellationToken ct = default
) )
{ {
@ -73,7 +74,7 @@ public class EmailAuthController(
[HttpPost("register")] [HttpPost("register")]
public async Task<IActionResult> CompleteRegistrationAsync( public async Task<IActionResult> CompleteRegistrationAsync(
[FromBody] CompleteRegistrationRequest req [FromBody] EmailCompleteRegistrationRequest req
) )
{ {
CheckRequirements(); CheckRequirements();
@ -102,7 +103,7 @@ public class EmailAuthController(
await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}"); await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}");
return Ok( return Ok(
new AuthController.AuthResponse( new AuthResponse(
await userRenderer.RenderUserAsync(user, user, renderMembers: false), await userRenderer.RenderUserAsync(user, user, renderMembers: false),
tokenStr, tokenStr,
token.ExpiresAt token.ExpiresAt
@ -111,9 +112,9 @@ public class EmailAuthController(
} }
[HttpPost("login")] [HttpPost("login")]
[ProducesResponseType<AuthController.AuthResponse>(StatusCodes.Status200OK)] [ProducesResponseType<AuthResponse>(StatusCodes.Status200OK)]
public async Task<IActionResult> LoginAsync( public async Task<IActionResult> LoginAsync(
[FromBody] LoginRequest req, [FromBody] EmailLoginRequest req,
CancellationToken ct = default CancellationToken ct = default
) )
{ {
@ -141,7 +142,7 @@ public class EmailAuthController(
await db.SaveChangesAsync(ct); await db.SaveChangesAsync(ct);
return Ok( return Ok(
new AuthController.AuthResponse( new AuthResponse(
await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct),
tokenStr, tokenStr,
token.ExpiresAt token.ExpiresAt
@ -151,7 +152,7 @@ public class EmailAuthController(
[HttpPost("change-password")] [HttpPost("change-password")]
[Authorize("*")] [Authorize("*")]
public async Task<IActionResult> UpdatePasswordAsync([FromBody] ChangePasswordRequest req) public async Task<IActionResult> UpdatePasswordAsync([FromBody] EmailChangePasswordRequest req)
{ {
if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Current)) if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Current))
throw new ApiError.Forbidden("Invalid password"); throw new ApiError.Forbidden("Invalid password");
@ -211,7 +212,7 @@ public class EmailAuthController(
[HttpPost("add-email/callback")] [HttpPost("add-email/callback")]
[Authorize("*")] [Authorize("*")]
public async Task<IActionResult> AddEmailCallbackAsync([FromBody] CallbackRequest req) public async Task<IActionResult> AddEmailCallbackAsync([FromBody] EmailCallbackRequest req)
{ {
CheckRequirements(); CheckRequirements();
@ -233,7 +234,7 @@ public class EmailAuthController(
); );
return Ok( return Ok(
new AuthController.AddOauthAccountResponse( new AddOauthAccountResponse(
authMethod.Id, authMethod.Id,
AuthType.Email, AuthType.Email,
authMethod.RemoteId, authMethod.RemoteId,
@ -258,14 +259,4 @@ public class EmailAuthController(
if (!config.EmailAuth.Enabled) if (!config.EmailAuth.Enabled)
throw new ApiError.BadRequest("Email authentication is not enabled on this instance."); 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);
} }

View file

@ -2,6 +2,7 @@ using System.Net;
using EntityFramework.Exceptions.Common; using EntityFramework.Exceptions.Common;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
using Foxnouns.Backend.Services.Auth; using Foxnouns.Backend.Services.Auth;
@ -25,7 +26,7 @@ public class FediverseAuthController(
private readonly ILogger _logger = logger.ForContext<FediverseAuthController>(); private readonly ILogger _logger = logger.ForContext<FediverseAuthController>();
[HttpGet] [HttpGet]
[ProducesResponseType<AuthController.SingleUrlResponse>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<SingleUrlResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> GetFediverseUrlAsync( public async Task<IActionResult> GetFediverseUrlAsync(
[FromQuery] string instance, [FromQuery] string instance,
[FromQuery] bool forceRefresh = false [FromQuery] bool forceRefresh = false
@ -35,12 +36,12 @@ public class FediverseAuthController(
throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); throw new ApiError.BadRequest("Not a valid domain.", "instance", instance);
string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh);
return Ok(new AuthController.SingleUrlResponse(url)); return Ok(new SingleUrlResponse(url));
} }
[HttpPost("callback")] [HttpPost("callback")]
[ProducesResponseType<CallbackResponse>(statusCode: StatusCodes.Status200OK)] [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); FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance);
FediverseAuthService.FediverseUser remoteUser = FediverseAuthService.FediverseUser remoteUser =
@ -74,10 +75,8 @@ public class FediverseAuthController(
} }
[HttpPost("register")] [HttpPost("register")]
[ProducesResponseType<AuthController.AuthResponse>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<AuthResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> RegisterAsync( public async Task<IActionResult> RegisterAsync([FromBody] OauthRegisterRequest req)
[FromBody] AuthController.OauthRegisterRequest req
)
{ {
FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>( FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync<FediverseTicketData>(
$"fediverse:{req.Ticket}", $"fediverse:{req.Ticket}",
@ -138,12 +137,14 @@ public class FediverseAuthController(
); );
string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state);
return Ok(new AuthController.SingleUrlResponse(url)); return Ok(new SingleUrlResponse(url));
} }
[HttpPost("add-account/callback")] [HttpPost("add-account/callback")]
[Authorize("*")] [Authorize("*")]
public async Task<IActionResult> AddAccountCallbackAsync([FromBody] CallbackRequest req) public async Task<IActionResult> AddAccountCallbackAsync(
[FromBody] FediverseCallbackRequest req
)
{ {
await remoteAuthService.ValidateAddAccountStateAsync( await remoteAuthService.ValidateAddAccountStateAsync(
req.State, req.State,
@ -171,7 +172,7 @@ public class FediverseAuthController(
); );
return Ok( return Ok(
new AuthController.AddOauthAccountResponse( new AddOauthAccountResponse(
authMethod.Id, authMethod.Id,
AuthType.Fediverse, AuthType.Fediverse,
authMethod.RemoteId, authMethod.RemoteId,
@ -189,8 +190,6 @@ public class FediverseAuthController(
} }
} }
public record CallbackRequest(string Instance, string Code, string State);
private record FediverseTicketData( private record FediverseTicketData(
Snowflake ApplicationId, Snowflake ApplicationId,
FediverseAuthService.FediverseUser User FediverseAuthService.FediverseUser User

View file

@ -1,6 +1,7 @@
using Coravel.Queuing.Interfaces; using Coravel.Queuing.Interfaces;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Jobs;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -43,8 +44,6 @@ public class ExportsController(
private string ExportUrl(Snowflake userId, string filename) => private string ExportUrl(Snowflake userId, string filename) =>
$"{config.MediaBaseUrl}/data-exports/{userId}/{filename}.zip"; $"{config.MediaBaseUrl}/data-exports/{userId}/{filename}.zip";
private record DataExportResponse(string? Url, Instant? ExpiresAt);
[HttpPost] [HttpPost]
public async Task<IActionResult> QueueDataExportAsync() public async Task<IActionResult> QueueDataExportAsync()
{ {

View file

@ -1,50 +1,49 @@
using Coravel.Queuing.Interfaces; using Coravel.Queuing.Interfaces;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Jobs;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
namespace Foxnouns.Backend.Controllers; namespace Foxnouns.Backend.Controllers;
[Route("/api/v2/users/@me/flags")] [Route("/api/v2/users/@me/flags")]
public class FlagsController( public class FlagsController(
ILogger logger,
DatabaseContext db, DatabaseContext db,
UserRendererService userRenderer, UserRendererService userRenderer,
ObjectStorageService objectStorageService,
ISnowflakeGenerator snowflakeGenerator, ISnowflakeGenerator snowflakeGenerator,
IQueue queue IQueue queue
) : ApiControllerBase ) : ApiControllerBase
{ {
private readonly ILogger _logger = logger.ForContext<FlagsController>();
[HttpGet] [HttpGet]
[Authorize("identify")] [Authorize("identify")]
[ProducesResponseType<IEnumerable<UserRendererService.PrideFlagResponse>>( [ProducesResponseType<IEnumerable<PrideFlagResponse>>(statusCode: StatusCodes.Status200OK)]
statusCode: StatusCodes.Status200OK
)]
public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default) public async Task<IActionResult> GetFlagsAsync(CancellationToken ct = default)
{ {
List<PrideFlag> flags = await db List<PrideFlag> flags = await db
.PrideFlags.Where(f => f.UserId == CurrentUser!.Id) .PrideFlags.Where(f => f.UserId == CurrentUser!.Id)
.OrderBy(f => f.Name)
.ThenBy(f => f.Id)
.ToListAsync(ct); .ToListAsync(ct);
return Ok(flags.Select(userRenderer.RenderPrideFlag)); return Ok(flags.Select(userRenderer.RenderPrideFlag));
} }
public const int MaxFlagCount = 500;
[HttpPost] [HttpPost]
[Authorize("user.update")] [Authorize("user.update")]
[ProducesResponseType<UserRendererService.PrideFlagResponse>( [ProducesResponseType<PrideFlagResponse>(statusCode: StatusCodes.Status202Accepted)]
statusCode: StatusCodes.Status202Accepted public async Task<IActionResult> CreateFlagAsync([FromBody] CreateFlagRequest req)
)]
public IActionResult CreateFlag([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)); ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image));
Snowflake id = snowflakeGenerator.GenerateSnowflake(); Snowflake id = snowflakeGenerator.GenerateSnowflake();
@ -56,10 +55,6 @@ public class FlagsController(
return Accepted(new CreateFlagResponse(id, req.Name, req.Description)); 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}")] [HttpPatch("{id}")]
[Authorize("user.update")] [Authorize("user.update")]
public async Task<IActionResult> UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) public async Task<IActionResult> UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req)
@ -84,52 +79,19 @@ public class FlagsController(
return Ok(userRenderer.RenderPrideFlag(flag)); return Ok(userRenderer.RenderPrideFlag(flag));
} }
public class UpdateFlagRequest : PatchRequest
{
public string? Name { get; init; }
public string? Description { get; init; }
}
[HttpDelete("{id}")] [HttpDelete("{id}")]
[Authorize("user.update")] [Authorize("user.update")]
public async Task<IActionResult> DeleteFlagAsync(Snowflake id) public async Task<IActionResult> DeleteFlagAsync(Snowflake id)
{ {
await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync();
PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f =>
f.Id == id && f.UserId == CurrentUser!.Id f.Id == id && f.UserId == CurrentUser!.Id
); );
if (flag == null) if (flag == null)
throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); throw new ApiError.NotFound("Unknown flag ID, or it's not your flag.");
string hash = flag.Hash;
db.PrideFlags.Remove(flag); db.PrideFlags.Remove(flag);
await db.SaveChangesAsync(); 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(); return NoContent();
} }

View file

@ -1,5 +1,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Controllers;
@ -63,10 +64,6 @@ public partial class InternalController(DatabaseContext db) : ControllerBase
return Ok(new RequestDataResponse(userId, template)); 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( private static RouteEndpoint? GetEndpoint(
HttpContext httpContext, HttpContext httpContext,
string url, string url,

View file

@ -2,6 +2,7 @@ using Coravel.Queuing.Interfaces;
using EntityFramework.Exceptions.Common; using EntityFramework.Exceptions.Common;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Extensions;
using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Jobs;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
@ -28,9 +29,7 @@ public class MembersController(
private readonly ILogger _logger = logger.ForContext<MembersController>(); private readonly ILogger _logger = logger.ForContext<MembersController>();
[HttpGet] [HttpGet]
[ProducesResponseType<IEnumerable<MemberRendererService.PartialMember>>( [ProducesResponseType<IEnumerable<PartialMember>>(StatusCodes.Status200OK)]
StatusCodes.Status200OK
)]
public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default) public async Task<IActionResult> GetMembersAsync(string userRef, CancellationToken ct = default)
{ {
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
@ -38,7 +37,7 @@ public class MembersController(
} }
[HttpGet("{memberRef}")] [HttpGet("{memberRef}")]
[ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)] [ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
public async Task<IActionResult> GetMemberAsync( public async Task<IActionResult> GetMemberAsync(
string userRef, string userRef,
string memberRef, string memberRef,
@ -52,7 +51,7 @@ public class MembersController(
public const int MaxMemberCount = 500; public const int MaxMemberCount = 500;
[HttpPost("/api/v2/users/@me/members")] [HttpPost("/api/v2/users/@me/members")]
[ProducesResponseType<MemberRendererService.MemberResponse>(StatusCodes.Status200OK)] [ProducesResponseType<MemberResponse>(StatusCodes.Status200OK)]
[Authorize("member.create")] [Authorize("member.create")]
public async Task<IActionResult> CreateMemberAsync( public async Task<IActionResult> CreateMemberAsync(
[FromBody] CreateMemberRequest req, [FromBody] CreateMemberRequest req,
@ -246,20 +245,6 @@ public class MembersController(
return Ok(memberRenderer.RenderMember(member, CurrentToken)); 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}")] [HttpDelete("/api/v2/users/@me/members/{memberRef}")]
[Authorize("member.update")] [Authorize("member.update")]
public async Task<IActionResult> DeleteMemberAsync(string memberRef) public async Task<IActionResult> DeleteMemberAsync(string memberRef)
@ -282,21 +267,9 @@ public class MembersController(
return NoContent(); 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")] [HttpPost("/api/v2/users/@me/members/{memberRef}/reroll-sid")]
[Authorize("member.update")] [Authorize("member.update")]
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<MemberResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> RerollSidAsync(string memberRef) public async Task<IActionResult> RerollSidAsync(string memberRef)
{ {
Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef);

View file

@ -1,3 +1,4 @@
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -17,17 +18,18 @@ public class MetaController : ApiControllerBase
BuildInfo.Version, BuildInfo.Version,
BuildInfo.Hash, BuildInfo.Hash,
(int)FoxnounsMetrics.MemberCount.Value, (int)FoxnounsMetrics.MemberCount.Value,
new UserInfo( new UserInfoResponse(
(int)FoxnounsMetrics.UsersCount.Value, (int)FoxnounsMetrics.UsersCount.Value,
(int)FoxnounsMetrics.UsersActiveMonthCount.Value, (int)FoxnounsMetrics.UsersActiveMonthCount.Value,
(int)FoxnounsMetrics.UsersActiveWeekCount.Value, (int)FoxnounsMetrics.UsersActiveWeekCount.Value,
(int)FoxnounsMetrics.UsersActiveDayCount.Value (int)FoxnounsMetrics.UsersActiveDayCount.Value
), ),
new Limits( new LimitsResponse(
MembersController.MaxMemberCount, MembersController.MaxMemberCount,
ValidationUtils.MaxBioLength, ValidationUtils.MaxBioLength,
ValidationUtils.MaxCustomPreferences, ValidationUtils.MaxCustomPreferences,
AuthUtils.MaxAuthMethodsPerType AuthUtils.MaxAuthMethodsPerType,
FlagsController.MaxFlagCount
) )
) )
); );
@ -35,23 +37,4 @@ public class MetaController : ApiControllerBase
[HttpGet("/api/v2/coffee")] [HttpGet("/api/v2/coffee")]
public IActionResult BrewCoffee() => public IActionResult BrewCoffee() =>
Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); 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
);
} }

View file

@ -1,8 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Coravel.Queuing.Interfaces; using Coravel.Queuing.Interfaces;
using EntityFramework.Exceptions.Common; using EntityFramework.Exceptions.Common;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Jobs;
using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Middleware;
using Foxnouns.Backend.Services; using Foxnouns.Backend.Services;
@ -27,7 +27,7 @@ public class UsersController(
private readonly ILogger _logger = logger.ForContext<UsersController>(); private readonly ILogger _logger = logger.ForContext<UsersController>();
[HttpGet("{userRef}")] [HttpGet("{userRef}")]
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default) public async Task<IActionResult> GetUserAsync(string userRef, CancellationToken ct = default)
{ {
User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); User user = await db.ResolveUserAsync(userRef, CurrentToken, ct);
@ -38,7 +38,7 @@ public class UsersController(
[HttpPatch("@me")] [HttpPatch("@me")]
[Authorize("user.update")] [Authorize("user.update")]
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> UpdateUserAsync( public async Task<IActionResult> UpdateUserAsync(
[FromBody] UpdateUserRequest req, [FromBody] UpdateUserRequest req,
CancellationToken ct = default CancellationToken ct = default
@ -196,7 +196,7 @@ public class UsersController(
[Authorize("user.update")] [Authorize("user.update")]
[ProducesResponseType<Dictionary<Snowflake, User.CustomPreference>>(StatusCodes.Status200OK)] [ProducesResponseType<Dictionary<Snowflake, User.CustomPreference>>(StatusCodes.Status200OK)]
public async Task<IActionResult> UpdateCustomPreferencesAsync( public async Task<IActionResult> UpdateCustomPreferencesAsync(
[FromBody] List<CustomPreferenceUpdate> req, [FromBody] List<CustomPreferenceUpdateRequest> req,
CancellationToken ct = default CancellationToken ct = default
) )
{ {
@ -207,7 +207,7 @@ public class UsersController(
.CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key))
.ToDictionary(); .ToDictionary();
foreach (CustomPreferenceUpdate? r in req) foreach (CustomPreferenceUpdateRequest? r in req)
{ {
if (r.Id != null && preferences.ContainsKey(r.Id.Value)) if (r.Id != null && preferences.ContainsKey(r.Id.Value))
{ {
@ -239,33 +239,6 @@ public class UsersController(
return Ok(user.CustomPreferences); 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")] [HttpGet("@me/settings")]
[Authorize("user.read_hidden")] [Authorize("user.read_hidden")]
[ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<UserSettings>(statusCode: StatusCodes.Status200OK)]
@ -294,14 +267,9 @@ public class UsersController(
return Ok(user.Settings); return Ok(user.Settings);
} }
public class UpdateUserSettingsRequest : PatchRequest
{
public bool? DarkMode { get; init; }
}
[HttpPost("@me/reroll-sid")] [HttpPost("@me/reroll-sid")]
[Authorize("user.update")] [Authorize("user.update")]
[ProducesResponseType<UserRendererService.UserResponse>(statusCode: StatusCodes.Status200OK)] [ProducesResponseType<UserResponse>(statusCode: StatusCodes.Status200OK)]
public async Task<IActionResult> RerollSidAsync() public async Task<IActionResult> RerollSidAsync()
{ {
Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1);

View 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);

View file

@ -0,0 +1,6 @@
// ReSharper disable NotAccessedPositionalProperty.Global
using NodaTime;
namespace Foxnouns.Backend.Dto;
public record DataExportResponse(string? Url, Instant? ExpiresAt);

View 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; }
}

View 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);

View 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
);

View 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
);

View 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; }
}

View file

@ -1,7 +1,7 @@
using System.Security.Cryptography; using System.Security.Cryptography;
using Foxnouns.Backend.Controllers.Authentication;
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;

View file

@ -1,5 +1,6 @@
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; 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( new(
user.Id, user.Id,
user.Sid, 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 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); 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
);
} }

View file

@ -1,5 +1,6 @@
using Foxnouns.Backend.Database; using Foxnouns.Backend.Database;
using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Database.Models;
using Foxnouns.Backend.Dto;
using Foxnouns.Backend.Utils; using Foxnouns.Backend.Utils;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -132,58 +133,6 @@ public class UserRendererService(
public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; 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) => public PrideFlagResponse RenderPrideFlag(PrideFlag flag) =>
new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description);
public record PrideFlagResponse(
Snowflake Id,
string ImageUrl,
string Name,
string? Description
);
} }

View file

@ -14,6 +14,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
using Foxnouns.Backend.Controllers; using Foxnouns.Backend.Controllers;
using Foxnouns.Backend.Dto;
namespace Foxnouns.Backend.Utils; namespace Foxnouns.Backend.Utils;
@ -23,7 +24,7 @@ public static partial class ValidationUtils
public const int MaxPreferenceTooltipLength = 128; public const int MaxPreferenceTooltipLength = 128;
public static List<(string, ValidationError?)> ValidateCustomPreferences( public static List<(string, ValidationError?)> ValidateCustomPreferences(
List<UsersController.CustomPreferenceUpdate> preferences List<CustomPreferenceUpdateRequest> preferences
) )
{ {
var errors = new List<(string, ValidationError?)>(); var errors = new List<(string, ValidationError?)>();
@ -46,11 +47,7 @@ public static partial class ValidationUtils
if (preferences.Count > 50) if (preferences.Count > 50)
return errors; return errors;
foreach ( foreach ((CustomPreferenceUpdateRequest? p, int i) in preferences.Select((p, i) => (p, i)))
(UsersController.CustomPreferenceUpdate? p, int i) in preferences.Select(
(p, i) => (p, i)
)
)
{ {
if (!BootstrapIcons.IsValid(p.Icon)) if (!BootstrapIcons.IsValid(p.Icon))
{ {