diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 8016a1f..ced8c3d 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -2,12 +2,14 @@ using System.Net; using System.Web; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; +using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; @@ -45,6 +47,28 @@ public class AuthController( return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); } + private record UrlsResponse(bool EmailEnabled, string? Discord, string? Google, string? Tumblr); + + public record AuthResponse( + UserRendererService.UserResponse User, + string Token, + Instant ExpiresAt + ); + + public record SingleUrlResponse(string Url); + + public record AddOauthAccountResponse( + Snowflake Id, + [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, + string RemoteId, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? RemoteUsername + ); + + public record OauthRegisterRequest(string Ticket, string Username); + + public record CallbackRequest(string Code, string State); + [HttpPost("force-log-out")] [Authorize("identify")] public async Task ForceLogoutAsync() @@ -59,7 +83,9 @@ public class AuthController( [HttpGet("methods/{id}")] [Authorize("*")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType( + statusCode: StatusCodes.Status200OK + )] public async Task GetAuthMethodAsync(Snowflake id) { AuthMethod? authMethod = await db @@ -117,3 +143,13 @@ public class AuthController( return NoContent(); } } + +public record CallbackResponse( + bool HasAccount, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + UserRendererService.UserResponse? User, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt +); diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 2d66ede..f7f9c3d 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -3,7 +3,6 @@ using System.Web; using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; @@ -30,7 +29,7 @@ public class DiscordAuthController( [HttpPost("callback")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] CallbackRequest req) + public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req) { CheckRequirements(); await keyCacheService.ValidateAuthStateAsync(req.State); @@ -59,8 +58,10 @@ public class DiscordAuthController( } [HttpPost("register")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] OauthRegisterRequest req) + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task RegisterAsync( + [FromBody] AuthController.OauthRegisterRequest req + ) { RemoteAuthService.RemoteUser? remoteUser = await keyCacheService.GetKeyAsync( @@ -108,12 +109,14 @@ public class DiscordAuthController( + $"&prompt=none&state={state}" + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; - return Ok(new SingleUrlResponse(url)); + return Ok(new AuthController.SingleUrlResponse(url)); } [HttpPost("add-account/callback")] [Authorize("*")] - public async Task AddAccountCallbackAsync([FromBody] CallbackRequest req) + public async Task AddAccountCallbackAsync( + [FromBody] AuthController.CallbackRequest req + ) { CheckRequirements(); @@ -141,7 +144,7 @@ public class DiscordAuthController( ); return Ok( - new AddOauthAccountResponse( + new AuthController.AddOauthAccountResponse( authMethod.Id, AuthType.Discord, authMethod.RemoteId, diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index f8d78b0..4162f4c 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -2,7 +2,6 @@ using System.Net; using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; @@ -31,7 +30,7 @@ public class EmailAuthController( [HttpPost("register/init")] public async Task RegisterInitAsync( - [FromBody] EmailRegisterRequest req, + [FromBody] RegisterRequest req, CancellationToken ct = default ) { @@ -74,7 +73,7 @@ public class EmailAuthController( [HttpPost("register")] public async Task CompleteRegistrationAsync( - [FromBody] EmailCompleteRegistrationRequest req + [FromBody] CompleteRegistrationRequest req ) { CheckRequirements(); @@ -103,7 +102,7 @@ public class EmailAuthController( await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}"); return Ok( - new AuthResponse( + new AuthController.AuthResponse( await userRenderer.RenderUserAsync(user, user, renderMembers: false), tokenStr, token.ExpiresAt @@ -112,9 +111,9 @@ public class EmailAuthController( } [HttpPost("login")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task LoginAsync( - [FromBody] EmailLoginRequest req, + [FromBody] LoginRequest req, CancellationToken ct = default ) { @@ -142,7 +141,7 @@ public class EmailAuthController( await db.SaveChangesAsync(ct); return Ok( - new AuthResponse( + new AuthController.AuthResponse( await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), tokenStr, token.ExpiresAt @@ -152,7 +151,7 @@ public class EmailAuthController( [HttpPost("change-password")] [Authorize("*")] - public async Task UpdatePasswordAsync([FromBody] EmailChangePasswordRequest req) + public async Task UpdatePasswordAsync([FromBody] ChangePasswordRequest req) { if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Current)) throw new ApiError.Forbidden("Invalid password"); @@ -212,7 +211,7 @@ public class EmailAuthController( [HttpPost("add-email/callback")] [Authorize("*")] - public async Task AddEmailCallbackAsync([FromBody] EmailCallbackRequest req) + public async Task AddEmailCallbackAsync([FromBody] CallbackRequest req) { CheckRequirements(); @@ -234,7 +233,7 @@ public class EmailAuthController( ); return Ok( - new AddOauthAccountResponse( + new AuthController.AddOauthAccountResponse( authMethod.Id, AuthType.Email, authMethod.RemoteId, @@ -259,4 +258,14 @@ public class EmailAuthController( if (!config.EmailAuth.Enabled) throw new ApiError.BadRequest("Email authentication is not enabled on this instance."); } + + public record LoginRequest(string Email, string Password); + + public record RegisterRequest(string Email); + + public record CompleteRegistrationRequest(string Ticket, string Username, string Password); + + public record CallbackRequest(string State); + + public record ChangePasswordRequest(string Current, string New); } diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 2587621..f47eb43 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -2,7 +2,6 @@ using System.Net; using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Foxnouns.Backend.Services.Auth; @@ -26,7 +25,7 @@ public class FediverseAuthController( private readonly ILogger _logger = logger.ForContext(); [HttpGet] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetFediverseUrlAsync( [FromQuery] string instance, [FromQuery] bool forceRefresh = false @@ -36,12 +35,12 @@ public class FediverseAuthController( throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); - return Ok(new SingleUrlResponse(url)); + return Ok(new AuthController.SingleUrlResponse(url)); } [HttpPost("callback")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task FediverseCallbackAsync([FromBody] FediverseCallbackRequest req) + public async Task FediverseCallbackAsync([FromBody] CallbackRequest req) { FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); FediverseAuthService.FediverseUser remoteUser = @@ -75,8 +74,10 @@ public class FediverseAuthController( } [HttpPost("register")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] OauthRegisterRequest req) + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task RegisterAsync( + [FromBody] AuthController.OauthRegisterRequest req + ) { FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync( $"fediverse:{req.Ticket}", @@ -137,14 +138,12 @@ public class FediverseAuthController( ); string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); - return Ok(new SingleUrlResponse(url)); + return Ok(new AuthController.SingleUrlResponse(url)); } [HttpPost("add-account/callback")] [Authorize("*")] - public async Task AddAccountCallbackAsync( - [FromBody] FediverseCallbackRequest req - ) + public async Task AddAccountCallbackAsync([FromBody] CallbackRequest req) { await remoteAuthService.ValidateAddAccountStateAsync( req.State, @@ -172,7 +171,7 @@ public class FediverseAuthController( ); return Ok( - new AddOauthAccountResponse( + new AuthController.AddOauthAccountResponse( authMethod.Id, AuthType.Fediverse, authMethod.RemoteId, @@ -190,6 +189,8 @@ public class FediverseAuthController( } } + public record CallbackRequest(string Instance, string Code, string State); + private record FediverseTicketData( Snowflake ApplicationId, FediverseAuthService.FediverseUser User diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs index 06844a1..feb8d91 100644 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ b/Foxnouns.Backend/Controllers/ExportsController.cs @@ -1,7 +1,6 @@ using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc; @@ -44,6 +43,8 @@ public class ExportsController( private string ExportUrl(Snowflake userId, string filename) => $"{config.MediaBaseUrl}/data-exports/{userId}/{filename}.zip"; + private record DataExportResponse(string? Url, Instant? ExpiresAt); + [HttpPost] public async Task QueueDataExportAsync() { diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 4bc947b..9bb9f91 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -1,69 +1,65 @@ using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; +using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users/@me/flags")] public class FlagsController( + ILogger logger, DatabaseContext db, UserRendererService userRenderer, + ObjectStorageService objectStorageService, ISnowflakeGenerator snowflakeGenerator, IQueue queue ) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpGet] [Authorize("identify")] - [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType>( + statusCode: StatusCodes.Status200OK + )] public async Task GetFlagsAsync(CancellationToken ct = default) { List flags = await db .PrideFlags.Where(f => f.UserId == CurrentUser!.Id) - .OrderBy(f => f.Name) - .ThenBy(f => f.Id) .ToListAsync(ct); return Ok(flags.Select(userRenderer.RenderPrideFlag)); } - public const int MaxFlagCount = 500; - [HttpPost] [Authorize("user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] - public async Task CreateFlagAsync([FromBody] CreateFlagRequest req) + [ProducesResponseType( + statusCode: StatusCodes.Status202Accepted + )] + 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)); - var flag = new PrideFlag - { - Id = snowflakeGenerator.GenerateSnowflake(), - UserId = CurrentUser!.Id, - Name = req.Name, - Description = req.Description, - }; - - db.Add(flag); - await db.SaveChangesAsync(); + Snowflake id = snowflakeGenerator.GenerateSnowflake(); queue.QueueInvocableWithPayload( - new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Name, req.Image, req.Description) + new CreateFlagPayload(id, CurrentUser!.Id, req.Name, req.Image, req.Description) ); - return Accepted(userRenderer.RenderPrideFlag(flag)); + return Accepted(new CreateFlagResponse(id, req.Name, req.Description)); } + public record CreateFlagRequest(string Name, string Image, string? Description); + + private record CreateFlagResponse(Snowflake Id, string Name, string? Description); + [HttpPatch("{id}")] [Authorize("user.update")] public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) @@ -88,19 +84,52 @@ public class FlagsController( return Ok(userRenderer.RenderPrideFlag(flag)); } + public class UpdateFlagRequest : PatchRequest + { + public string? Name { get; init; } + public string? Description { get; init; } + } + [HttpDelete("{id}")] [Authorize("user.update")] public async Task DeleteFlagAsync(Snowflake id) { + await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(); + PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => f.Id == id && f.UserId == CurrentUser!.Id ); if (flag == null) throw new ApiError.NotFound("Unknown flag ID, or it's not your flag."); + string hash = flag.Hash; + db.PrideFlags.Remove(flag); await db.SaveChangesAsync(); + int flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); + if (flagCount == 0) + { + try + { + _logger.Information( + "Deleting flag file {Hash} as it is no longer used by any flags", + hash + ); + await objectStorageService.DeleteFlagAsync(hash); + } + catch (Exception e) + { + _logger.Error(e, "Error deleting flag file {Hash}", hash); + } + } + else + { + _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); + } + + await tx.CommitAsync(); + return NoContent(); } diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 2f4baf7..9e94022 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -1,6 +1,5 @@ using System.Text.RegularExpressions; using Foxnouns.Backend.Database; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -64,6 +63,10 @@ public partial class InternalController(DatabaseContext db) : ControllerBase return Ok(new RequestDataResponse(userId, template)); } + public record RequestDataRequest(string? Token, string Method, string Path); + + public record RequestDataResponse(Snowflake? UserId, string Template); + private static RouteEndpoint? GetEndpoint( HttpContext httpContext, string url, diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 269817e..a9021b9 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -2,7 +2,6 @@ using Coravel.Queuing.Interfaces; using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; @@ -29,7 +28,9 @@ public class MembersController( private readonly ILogger _logger = logger.ForContext(); [HttpGet] - [ProducesResponseType>(StatusCodes.Status200OK)] + [ProducesResponseType>( + StatusCodes.Status200OK + )] public async Task GetMembersAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); @@ -37,7 +38,7 @@ public class MembersController( } [HttpGet("{memberRef}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMemberAsync( string userRef, string memberRef, @@ -51,7 +52,7 @@ public class MembersController( public const int MaxMemberCount = 500; [HttpPost("/api/v2/users/@me/members")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [Authorize("member.create")] public async Task CreateMemberAsync( [FromBody] CreateMemberRequest req, @@ -245,6 +246,20 @@ public class MembersController( return Ok(memberRenderer.RenderMember(member, CurrentToken)); } + public class UpdateMemberRequest : PatchRequest + { + public string? Name { get; init; } + public string? DisplayName { get; init; } + public string? Bio { get; init; } + public string? Avatar { get; init; } + public string[]? Links { get; init; } + public FieldEntry[]? Names { get; init; } + public Pronoun[]? Pronouns { get; init; } + public Field[]? Fields { get; init; } + public Snowflake[]? Flags { get; init; } + public bool? Unlisted { get; init; } + } + [HttpDelete("/api/v2/users/@me/members/{memberRef}")] [Authorize("member.update")] public async Task DeleteMemberAsync(string memberRef) @@ -267,9 +282,21 @@ public class MembersController( return NoContent(); } + public record CreateMemberRequest( + string Name, + string? DisplayName, + string? Bio, + string? Avatar, + bool? Unlisted, + string[]? Links, + List? Names, + List? Pronouns, + List? Fields + ); + [HttpPost("/api/v2/users/@me/members/{memberRef}/reroll-sid")] [Authorize("member.update")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task RerollSidAsync(string memberRef) { Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 2c5bc45..2a9466e 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -1,4 +1,3 @@ -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; @@ -18,18 +17,17 @@ public class MetaController : ApiControllerBase BuildInfo.Version, BuildInfo.Hash, (int)FoxnounsMetrics.MemberCount.Value, - new UserInfoResponse( + new UserInfo( (int)FoxnounsMetrics.UsersCount.Value, (int)FoxnounsMetrics.UsersActiveMonthCount.Value, (int)FoxnounsMetrics.UsersActiveWeekCount.Value, (int)FoxnounsMetrics.UsersActiveDayCount.Value ), - new LimitsResponse( + new Limits( MembersController.MaxMemberCount, ValidationUtils.MaxBioLength, ValidationUtils.MaxCustomPreferences, - AuthUtils.MaxAuthMethodsPerType, - FlagsController.MaxFlagCount + AuthUtils.MaxAuthMethodsPerType ) ) ); @@ -37,4 +35,23 @@ public class MetaController : ApiControllerBase [HttpGet("/api/v2/coffee")] public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); + + private record MetaResponse( + string Repository, + string Version, + string Hash, + int Members, + UserInfo Users, + Limits Limits + ); + + private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); + + // All limits that the frontend should know about (for UI purposes) + private record Limits( + int MemberCount, + int BioLength, + int CustomPreferences, + int MaxAuthMethods + ); } diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 74149b7..98d5645 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,8 +1,8 @@ +using System.Diagnostics.CodeAnalysis; using Coravel.Queuing.Interfaces; using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; @@ -27,7 +27,7 @@ public class UsersController( private readonly ILogger _logger = logger.ForContext(); [HttpGet("{userRef}")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetUserAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); @@ -38,7 +38,7 @@ public class UsersController( [HttpPatch("@me")] [Authorize("user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task UpdateUserAsync( [FromBody] UpdateUserRequest req, CancellationToken ct = default @@ -196,7 +196,7 @@ public class UsersController( [Authorize("user.update")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task UpdateCustomPreferencesAsync( - [FromBody] List req, + [FromBody] List req, CancellationToken ct = default ) { @@ -207,7 +207,7 @@ public class UsersController( .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) .ToDictionary(); - foreach (CustomPreferenceUpdateRequest? r in req) + foreach (CustomPreferenceUpdate? r in req) { if (r.Id != null && preferences.ContainsKey(r.Id.Value)) { @@ -239,6 +239,33 @@ public class UsersController( return Ok(user.CustomPreferences); } + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + public class CustomPreferenceUpdate + { + public Snowflake? Id { get; init; } + public required string Icon { get; set; } + public required string Tooltip { get; set; } + public PreferenceSize Size { get; set; } + public bool Muted { get; set; } + public bool Favourite { get; set; } + } + + public class UpdateUserRequest : PatchRequest + { + public string? Username { get; init; } + public string? DisplayName { get; init; } + public string? Bio { get; init; } + public string? Avatar { get; init; } + public string[]? Links { get; init; } + public FieldEntry[]? Names { get; init; } + public Pronoun[]? Pronouns { get; init; } + public Field[]? Fields { get; init; } + public Snowflake[]? Flags { get; init; } + public string? MemberTitle { get; init; } + public bool? MemberListHidden { get; init; } + public string? Timezone { get; init; } + } + [HttpGet("@me/settings")] [Authorize("user.read_hidden")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] @@ -267,9 +294,14 @@ public class UsersController( return Ok(user.Settings); } + public class UpdateUserSettingsRequest : PatchRequest + { + public bool? DarkMode { get; init; } + } + [HttpPost("@me/reroll-sid")] [Authorize("user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task RerollSidAsync() { Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); diff --git a/Foxnouns.Backend/Database/Migrations/20241209134148_NullableFlagHash.cs b/Foxnouns.Backend/Database/Migrations/20241209134148_NullableFlagHash.cs deleted file mode 100644 index 12d84ff..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241209134148_NullableFlagHash.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241209134148_NullableFlagHash")] - public partial class NullableFlagHash : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "hash", - table: "pride_flags", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "hash", - table: "pride_flags", - type: "text", - nullable: false, - defaultValue: "", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 4bd1ede..79a9855 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -282,6 +282,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnName("description"); b.Property("Hash") + .IsRequired() .HasColumnType("text") .HasColumnName("hash"); @@ -545,7 +546,7 @@ namespace Foxnouns.Backend.Database.Migrations modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => { b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("DataExports") + .WithMany() .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired() @@ -644,8 +645,6 @@ namespace Foxnouns.Backend.Database.Migrations { b.Navigation("AuthMethods"); - b.Navigation("DataExports"); - b.Navigation("Flags"); b.Navigation("Members"); diff --git a/Foxnouns.Backend/Database/Models/PrideFlag.cs b/Foxnouns.Backend/Database/Models/PrideFlag.cs index d5437f8..45a3a7c 100644 --- a/Foxnouns.Backend/Database/Models/PrideFlag.cs +++ b/Foxnouns.Backend/Database/Models/PrideFlag.cs @@ -3,9 +3,7 @@ namespace Foxnouns.Backend.Database.Models; public class PrideFlag : BaseModel { public required Snowflake UserId { get; init; } - - // A null hash means the flag hasn't been processed yet. - public string? Hash { get; set; } + public required string Hash { get; init; } public required string Name { get; set; } public string? Description { get; set; } } diff --git a/Foxnouns.Backend/Dto/Auth.cs b/Foxnouns.Backend/Dto/Auth.cs deleted file mode 100644 index 71ebd91..0000000 --- a/Foxnouns.Backend/Dto/Auth.cs +++ /dev/null @@ -1,47 +0,0 @@ -// 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); diff --git a/Foxnouns.Backend/Dto/DataExport.cs b/Foxnouns.Backend/Dto/DataExport.cs deleted file mode 100644 index 91413ab..0000000 --- a/Foxnouns.Backend/Dto/DataExport.cs +++ /dev/null @@ -1,6 +0,0 @@ -// ReSharper disable NotAccessedPositionalProperty.Global -using NodaTime; - -namespace Foxnouns.Backend.Dto; - -public record DataExportResponse(string? Url, Instant? ExpiresAt); diff --git a/Foxnouns.Backend/Dto/Flag.cs b/Foxnouns.Backend/Dto/Flag.cs deleted file mode 100644 index 882613a..0000000 --- a/Foxnouns.Backend/Dto/Flag.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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; } -} diff --git a/Foxnouns.Backend/Dto/Internal.cs b/Foxnouns.Backend/Dto/Internal.cs deleted file mode 100644 index aed0411..0000000 --- a/Foxnouns.Backend/Dto/Internal.cs +++ /dev/null @@ -1,8 +0,0 @@ -// 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); diff --git a/Foxnouns.Backend/Dto/Member.cs b/Foxnouns.Backend/Dto/Member.cs deleted file mode 100644 index 2925efe..0000000 --- a/Foxnouns.Backend/Dto/Member.cs +++ /dev/null @@ -1,61 +0,0 @@ -// 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? Names, - List? Pronouns, - List? 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 Names, - IEnumerable 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 Names, - IEnumerable Pronouns, - IEnumerable Fields, - IEnumerable Flags, - PartialUser User, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted -); diff --git a/Foxnouns.Backend/Dto/Meta.cs b/Foxnouns.Backend/Dto/Meta.cs deleted file mode 100644 index 80826ca..0000000 --- a/Foxnouns.Backend/Dto/Meta.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 -); diff --git a/Foxnouns.Backend/Dto/User.cs b/Foxnouns.Backend/Dto/User.cs deleted file mode 100644 index c0604fb..0000000 --- a/Foxnouns.Backend/Dto/User.cs +++ /dev/null @@ -1,82 +0,0 @@ -// 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 Names, - IEnumerable Pronouns, - IEnumerable Fields, - Dictionary CustomPreferences, - IEnumerable Flags, - int? UtcOffset, - [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - IEnumerable? Members, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - IEnumerable? 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 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; } -} diff --git a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs index c07ea7f..8e34d01 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs @@ -108,12 +108,6 @@ public class CreateDataExportInvocable( private async Task WritePrideFlag(ZipArchive zip, PrideFlag flag) { - if (flag.Hash == null) - { - _logger.Debug("Flag {FlagId} has a null hash, ignoring it", flag.Id); - return; - } - _logger.Debug("Writing flag {FlagId}", flag.Id); var flagData = $""" diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 93a4e0c..5c5df2d 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -3,7 +3,6 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; -using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Jobs; @@ -27,18 +26,6 @@ public class CreateFlagInvocable( try { - PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => - f.Id == Payload.Id && f.UserId == Payload.UserId - ); - if (flag == null) - { - _logger.Warning( - "Got a flag create job for {FlagId} but it doesn't exist, aborting", - Payload.Id - ); - return; - } - (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( Payload.ImageData, 256, @@ -46,8 +33,16 @@ public class CreateFlagInvocable( ); await objectStorageService.PutObjectAsync(Path(hash), image, "image/webp"); - flag.Hash = hash; - db.Update(flag); + var flag = new PrideFlag + { + Id = Payload.Id, + UserId = Payload.UserId, + Hash = hash, + Name = Payload.Name, + Description = Payload.Description, + }; + db.Add(flag); + await db.SaveChangesAsync(); _logger.Information("Uploaded flag {FlagId} with hash {Hash}", flag.Id, flag.Hash); diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs index 0317c4a..221bb58 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -1,7 +1,7 @@ using System.Security.Cryptography; +using Foxnouns.Backend.Controllers.Authentication; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 06de060..ea93e4d 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -1,6 +1,5 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; @@ -50,7 +49,7 @@ public class MemberRendererService(DatabaseContext db, Config config) ); } - private PartialUser RenderPartialUser(User user) => + private UserRendererService.PartialUser RenderPartialUser(User user) => new( user.Id, user.Sid, @@ -83,9 +82,36 @@ public class MemberRendererService(DatabaseContext db, Config config) ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - private string? ImageUrlFor(PrideFlag flag) => - flag.Hash != null ? $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp" : null; + private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; - private PrideFlagResponse RenderPrideFlag(PrideFlag flag) => + private UserRendererService.PrideFlagResponse RenderPrideFlag(PrideFlag flag) => new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); + + public record PartialMember( + Snowflake Id, + string Sid, + string Name, + string DisplayName, + string? Bio, + string? AvatarUrl, + IEnumerable Names, + IEnumerable 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 Names, + IEnumerable Pronouns, + IEnumerable Fields, + IEnumerable Flags, + UserRendererService.PartialUser User, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted + ); } diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 6f33583..9f6da8b 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -1,6 +1,5 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; @@ -131,9 +130,60 @@ public class UserRendererService( ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - public string? ImageUrlFor(PrideFlag flag) => - flag.Hash != null ? $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp" : null; + 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 Names, + IEnumerable Pronouns, + IEnumerable Fields, + Dictionary CustomPreferences, + IEnumerable Flags, + int? UtcOffset, + [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + IEnumerable? Members, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + IEnumerable? 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 CustomPreferences + ); public PrideFlagResponse RenderPrideFlag(PrideFlag flag) => new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); + + public record PrideFlagResponse( + Snowflake Id, + string ImageUrl, + string Name, + string? Description + ); } diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs index a6ec90e..a4109f2 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs @@ -14,7 +14,6 @@ // along with this program. If not, see . using Foxnouns.Backend.Controllers; -using Foxnouns.Backend.Dto; namespace Foxnouns.Backend.Utils; @@ -24,7 +23,7 @@ public static partial class ValidationUtils public const int MaxPreferenceTooltipLength = 128; public static List<(string, ValidationError?)> ValidateCustomPreferences( - List preferences + List preferences ) { var errors = new List<(string, ValidationError?)>(); @@ -47,7 +46,11 @@ public static partial class ValidationUtils if (preferences.Count > 50) return errors; - foreach ((CustomPreferenceUpdateRequest? p, int i) in preferences.Select((p, i) => (p, i))) + foreach ( + (UsersController.CustomPreferenceUpdate? p, int i) in preferences.Select( + (p, i) => (p, i) + ) + ) { if (!BootstrapIcons.IsValid(p.Icon)) { diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index bd5ed64..0e74736 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -29,7 +29,6 @@ "prettier-plugin-svelte": "^3.2.6", "sass": "^1.81.0", "svelte": "^5.0.0", - "svelte-bootstrap-icons": "^3.1.1", "svelte-check": "^4.0.0", "sveltekit-i18n": "^2.4.2", "typescript": "^5.0.0", diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index d2289ec..bb1a839 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -93,9 +93,6 @@ importers: svelte: specifier: ^5.0.0 version: 5.2.2 - svelte-bootstrap-icons: - specifier: ^3.1.1 - version: 3.1.1 svelte-check: specifier: ^4.0.0 version: 4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3) @@ -1324,9 +1321,6 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} - svelte-bootstrap-icons@3.1.1: - resolution: {integrity: sha512-ghJlt6TX3IX35M7wSvGyrmVgXeT5GMRF+7+q6L4OUT2RJWF09mQIvZTZ04Ii3FBfg10KdzFdvVuoB8M0cVHfzw==} - svelte-check@4.0.9: resolution: {integrity: sha512-SVNCz2L+9ZELGli7G0n3B3QE5kdf0u27RtKr2ZivWQhcWIXatZxwM4VrQ6AiA2k9zKp2mk5AxkEhdjbpjv7rEw==} engines: {node: '>= 18.0.0'} @@ -2570,8 +2564,6 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-bootstrap-icons@3.1.1: {} - svelte-check@4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 diff --git a/Foxnouns.Frontend/src/lib/api/models/meta.ts b/Foxnouns.Frontend/src/lib/api/models/meta.ts index 56f31c9..eb77a31 100644 --- a/Foxnouns.Frontend/src/lib/api/models/meta.ts +++ b/Foxnouns.Frontend/src/lib/api/models/meta.ts @@ -17,5 +17,4 @@ export type Limits = { bio_length: number; custom_preferences: number; max_auth_methods: number; - max_flags: number; }; diff --git a/Foxnouns.Frontend/src/lib/api/models/user.ts b/Foxnouns.Frontend/src/lib/api/models/user.ts index f830983..d2deb8f 100644 --- a/Foxnouns.Frontend/src/lib/api/models/user.ts +++ b/Foxnouns.Frontend/src/lib/api/models/user.ts @@ -66,7 +66,7 @@ export type Field = { export type PrideFlag = { id: string; - image_url: string | null; + image_url: string; name: string; description: string | null; }; diff --git a/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte b/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte deleted file mode 100644 index 7f72ca2..0000000 --- a/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - -{#if pageCount > 1} -
- - - (currentPage = 0)} /> - - - (currentPage = prevPage)} /> - - {#each new Array(pageCount) as _, page} - - (currentPage = page)}>{page + 1} - - {/each} - - (currentPage = nextPage)} /> - - - (currentPage = pageCount - 1)} /> - - -
-{/if} diff --git a/Foxnouns.Frontend/src/lib/components/IconButton.svelte b/Foxnouns.Frontend/src/lib/components/IconButton.svelte index 1feedd9..4633dd1 100644 --- a/Foxnouns.Frontend/src/lib/components/IconButton.svelte +++ b/Foxnouns.Frontend/src/lib/components/IconButton.svelte @@ -10,18 +10,11 @@ type?: "submit" | "reset" | "button"; id?: string; onclick?: MouseEventHandler; - outline?: boolean; }; - let { icon, tooltip, color = "primary", type, id, onclick, outline }: Props = $props(); + let { icon, tooltip, color = "primary", type, id, onclick }: Props = $props(); - diff --git a/Foxnouns.Frontend/src/lib/components/editor/EditorFlagImage.svelte b/Foxnouns.Frontend/src/lib/components/editor/EditorFlagImage.svelte deleted file mode 100644 index 45875b8..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/EditorFlagImage.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -{flag.description - - diff --git a/Foxnouns.Frontend/src/lib/components/editor/FlagButton.svelte b/Foxnouns.Frontend/src/lib/components/editor/FlagButton.svelte deleted file mode 100644 index 51b87c4..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FlagButton.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - - diff --git a/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte deleted file mode 100644 index 8cb994c..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte +++ /dev/null @@ -1,57 +0,0 @@ - - -
- - {flag.description - -
- - -
- - -
-
-
- - diff --git a/Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte b/Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte deleted file mode 100644 index 78a6c0f..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - - - -
- {#each filteredFlags as flag (flag.id)} - select(flag)} padding /> - {:else} -
-

- -

-

- {#if query} - {$t("editor.flag-search-no-flags")} - {:else} - {$t("editor.flag-search-no-account-flags")} - {/if} -

-
- {/each} - {#if flags.length > 0} -

- - {$t("editor.flag-search-hint")} - {$t("editor.flag-manage-your-flags")} -

- {:else} -

{$t("editor.flag-manage-your-flags")}

- {/if} -
diff --git a/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte deleted file mode 100644 index 5bd62fd..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte +++ /dev/null @@ -1,95 +0,0 @@ - - -
-
-

- {$t("settings.flag-title")} - -

- - {#each flags as flag, i} -
-
- moveFlag(i, true)} - /> - moveFlag(i, false)} - /> - removeFlag(flag)} - tooltip={$t("editor.remove-this-flag")} - /> -
-
- {:else} -

- {$t("editor.no-flags-hint")} -

- {/each} -
-
-

{$t("editor.add-flags-header")}

- -
-
- - diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte index b3d1611..bf171cd 100644 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte @@ -1,6 +1,5 @@ - -

{$t("settings.flag-title")}

- - - -
- -

{$t("settings.flag-upload-title")}

- - - - - - -

- {$t("settings.flag-current-flags-title", { - count: data.flags.length, - max: data.meta.limits.max_flags, - })} -

- - - - - {#each arr as flag (flag.id)} - - - - {flag.name} - - - {#if lastEditedFlag === flag.id}{/if} - - - {/each} - - - diff --git a/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts index 715610b..220865b 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts @@ -1,15 +1,18 @@ -import paginate from "$lib/paginate"; - const MEMBERS_PER_PAGE = 15; export const load = async ({ url, parent }) => { const { user } = await parent(); - const { data, currentPage, pageCount } = paginate( - user.members, - url.searchParams.get("page"), - MEMBERS_PER_PAGE, + let currentPage = Number(url.searchParams.get("page") || "0"); + let pageCount = Math.ceil(user.members.length / MEMBERS_PER_PAGE); + let members = user.members.slice( + currentPage * MEMBERS_PER_PAGE, + (currentPage + 1) * MEMBERS_PER_PAGE, ); + if (members.length === 0) { + members = user.members.slice(0, MEMBERS_PER_PAGE); + currentPage = 0; + } - return { members: data, currentPage, pageCount }; + return { members, currentPage, pageCount }; }; diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte index c286c38..b4b38e2 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte @@ -6,7 +6,6 @@ import ApiError from "$api/error"; import log from "$lib/log"; import { Icon, InputGroup } from "@sveltestrap/sveltestrap"; - import InfoCircleFill from "svelte-bootstrap-icons/lib/InfoCircleFill.svelte"; import { t } from "$lib/i18n"; import AvatarEditor from "$components/editor/AvatarEditor.svelte"; import ErrorAlert from "$components/ErrorAlert.svelte"; @@ -134,7 +133,7 @@

- + {$t("edit-profile.unlisted-note")} {PUBLIC_BASE_URL.substring("https://".length)}/@{data.member.user.username}/{data.member diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte deleted file mode 100644 index 491a45f..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.server.ts deleted file mode 100644 index 0b3a452..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { apiRequest } from "$api"; -import type { PrideFlag } from "$api/models/user"; - -export const load = async ({ fetch, cookies }) => { - const flags = await apiRequest("GET", "/users/@me/flags", { fetch, cookies }); - return { flags }; -}; diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte deleted file mode 100644 index db35e18..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte index 4c61a58..8bab783 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte @@ -29,4 +29,4 @@ }; - + diff --git a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.server.ts deleted file mode 100644 index 0b3a452..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { apiRequest } from "$api"; -import type { PrideFlag } from "$api/models/user"; - -export const load = async ({ fetch, cookies }) => { - const flags = await apiRequest("GET", "/users/@me/flags", { fetch, cookies }); - return { flags }; -}; diff --git a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte deleted file mode 100644 index 0273e6b..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/static/unknown_flag.svg b/Foxnouns.Frontend/static/unknown_flag.svg deleted file mode 100644 index bbf82ce..0000000 --- a/Foxnouns.Frontend/static/unknown_flag.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/README.md b/README.md index 146591f..ea28fc8 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,3 @@ Code taken entirely or almost entirely from external sources: taken from [PluralKit](https://github.com/PluralKit/PluralKit/blob/32a6e97342acc3b35e6f9e7b4dd169e21d888770/PluralKit.Core/Database/Functions/functions.sql) - `Foxnouns.Backend/Database/prune-designer-cs-files.sh`, taken from [Iceshrimp.NET](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/commit/7c93dcf79dda54fc1a4ea9772e3f80874e6bcefb/Iceshrimp.Backend/Core/Database/prune-designer-cs-files.sh) - -Files under a different license: - -- `Foxnouns.Frontend/static/unknown_flag.svg` is https://commons.wikimedia.org/wiki/File:Unknown_flag.svg, - by 8938e on Wikimedia Commons, licensed as CC BY-SA 4.0. \ No newline at end of file