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 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
|
|
||||||
);
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
{
|
{
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
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 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;
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue