diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index 3652ec5..0000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "husky": { - "version": "0.7.2", - "commands": [ - "husky" - ], - "rollForward": false - }, - "csharpier": { - "version": "0.30.6", - "commands": [ - "dotnet-csharpier" - ], - "rollForward": false - } - } -} diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index d755b6c..0000000 --- a/.dockerignore +++ /dev/null @@ -1,24 +0,0 @@ -**/.dockerignore -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/.idea -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md -static-pages/* diff --git a/.editorconfig b/.editorconfig index 22061dc..2a1f655 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,52 +1,2 @@ -[*] -# We use PostgresSQL which doesn't recommend more specific string types +[*.cs] resharper_entity_framework_model_validation_unlimited_string_length_highlighting = none -# This is raised for every single property of records returned by endpoints -resharper_not_accessed_positional_property_local_highlighting = none - - -# Microsoft .NET properties -csharp_new_line_before_members_in_object_initializers = false -csharp_preferred_modifier_order = public, internal, protected, private, file, new, virtual, override, required, abstract, sealed, static, extern, unsafe, volatile, async, readonly:suggestion - -# ReSharper properties -resharper_align_multiline_binary_expressions_chain = false -resharper_arguments_skip_single = true -resharper_blank_lines_after_start_comment = 0 -resharper_blank_lines_around_single_line_invocable = 0 -resharper_blank_lines_before_block_statements = 0 -resharper_braces_for_foreach = required_for_multiline -resharper_braces_for_ifelse = required_for_multiline -resharper_braces_redundant = false -resharper_csharp_blank_lines_around_field = 0 -resharper_csharp_empty_block_style = together_same_line -resharper_csharp_max_line_length = 166 -resharper_csharp_wrap_after_declaration_lpar = true -resharper_csharp_wrap_before_binary_opsign = true -resharper_csharp_wrap_before_declaration_rpar = true -resharper_csharp_wrap_parameters_style = chop_if_long -resharper_indent_preprocessor_other = do_not_change -resharper_instance_members_qualify_declared_in = -resharper_keep_existing_attribute_arrangement = true -resharper_max_attribute_length_for_same_line = 70 -resharper_place_accessorholder_attribute_on_same_line = false -resharper_place_expr_method_on_single_line = if_owner_is_single_line -resharper_place_method_attribute_on_same_line = if_owner_is_single_line -resharper_place_record_field_attribute_on_same_line = true -resharper_place_simple_embedded_statement_on_same_line = false -resharper_place_simple_initializer_on_single_line = false -resharper_place_simple_list_pattern_on_single_line = false -resharper_space_within_empty_braces = false -resharper_trailing_comma_in_multiline_lists = true -resharper_wrap_after_invocation_lpar = false -resharper_wrap_before_invocation_rpar = false -resharper_wrap_before_primary_constructor_declaration_rpar = true -resharper_wrap_chained_binary_patterns = chop_if_long -resharper_wrap_list_pattern = chop_always -resharper_wrap_object_and_collection_initializer_style = chop_always - -# Roslynator properties -dotnet_diagnostic.RCS1194.severity = none - -[*generated.cs] -generated_code = true \ No newline at end of file diff --git a/.gitignore b/.gitignore index b1e845f..cd1b080 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,3 @@ bin/ obj/ -node_modules/ .version -config.ini -*.DotSettings.user -proxy-config.json -.DS_Store -.idea/.idea.Foxnouns.NET/.idea/dataSources.xml -.idea/.idea.Foxnouns.NET/.idea/sqldialects.xml - -docker/config.ini -docker/proxy-config.json -docker/frontend.env - -Foxnouns.DataMigrator/apps.json -migration-tools/avatar-proxy/config.json -migration-tools/avatar-migrator/.env - -out/ -build/ diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100755 index fd85d23..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -## husky task runner examples ------------------- -## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' - -## run all tasks -#husky run - -### run all tasks with group: 'group-name' -#husky run --group group-name - -## run task with name: 'task-name' -#husky run --name task-name - -## pass hook arguments to task -#husky run --args "$1" "$2" - -## or put your custom commands ------------------- -#echo 'Husky.Net is awesome!' - -dotnet husky run diff --git a/.husky/task-runner.json b/.husky/task-runner.json deleted file mode 100644 index 72e6fea..0000000 --- a/.husky/task-runner.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", - "tasks": [ - { - "name": "run-prettier", - "command": "npx", - "args": ["prettier", "-w", "${staged}"], - "include": [ - "Foxnouns.Frontend/**/*.ts", - "Foxnouns.Frontend/**/*.json", - "Foxnouns.Frontend/**/*.scss", - "Foxnouns.Frontend/**/*.js", - "Foxnouns.Frontend/**/*.svelte" - ], - "cwd": "Foxnouns.Frontend/", - "pathMode": "absolute" - }, - { - "name": "run-csharpier", - "command": "dotnet", - "args": ["csharpier", "${staged}"], - "include": ["**/*.cs"] - } - ] -} diff --git a/.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml b/.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml deleted file mode 100644 index 5e24061..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/CSharpierPlugin.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml b/.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml deleted file mode 100644 index c80f77e..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml b/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml index ea4646c..7b08163 100644 --- a/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml +++ b/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml @@ -1,10 +1,7 @@ - - Foxnouns.Frontend - migrators/go-exporter - + diff --git a/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index c9504c5..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml b/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml deleted file mode 100644 index 5f8621e..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/prettier.xml b/.idea/.idea.Foxnouns.NET/.idea/prettier.xml deleted file mode 100644 index ffcf89b..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/prettier.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml b/.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml deleted file mode 100644 index fb0d65a..0000000 --- a/.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.noai b/.noai deleted file mode 100644 index e69de29..0000000 diff --git a/DOCKER.md b/DOCKER.md deleted file mode 100644 index b670743..0000000 --- a/DOCKER.md +++ /dev/null @@ -1,32 +0,0 @@ -# Running with Docker (pre-built backend and rate limiter) *(linux/arm64 only)* - -Because SvelteKit is a pain in the ass to build in a container, and processes secrets at build time, -there is no pre-built frontend image available. -If you don't want to build images on your server, I recommend running the frontend outside of Docker. -This is preconfigured in `docker-compose.prebuilt.yml`: the backend, database, and rate limiter will run in Docker, -while the frontend is run as a normal, non-containerized service. - -1. Copy `docker/config.example.ini` to `docker/config.ini`, and change the settings to your liking. -2. Copy `docker/proxy-config.example.json` to `docker/proxy-config.json`, and do the same. -3. Run with `docker compose up -f docker-compose.prebuilt.yml` - -The backend will listen on port 5001 and metrics will be available on port 5002. -The rate limiter (which is what should be exposed to the outside) will listen on port 5003. -You can use `docker/Caddyfile` as an example for your reverse proxy. If you use nginx, good luck. - -# Running with Docker (local builds) - -In order to run *everything* in Docker, you'll have to build every container yourself. -The advantage of this is that it's an all-in-one solution, where you only have to point your reverse proxy at a single container. -The disadvantage is that you'll likely have to build the images on the server you'll be running them on. - -1. Configure the backend and rate limiter as in the section above. -2. Copy `docker/frontend.example.env` to `docker/frontend.env`, and configure it. -3. Build with `docker compose build -f docker-compose.local.yml` -4. Run with `docker compose up -f docker-compose.local.yml` - -The Caddy server will listen on `localhost:5004` for the frontend and API, -and on `localhost:5005` for the profile URL shortener. - -The backend server listens on `localhost:5006` for unproxied API access, -and `localhost:5007` for metrics. \ No newline at end of file diff --git a/Dockerfile.backend b/Dockerfile.backend deleted file mode 100644 index 69f90b0..0000000 --- a/Dockerfile.backend +++ /dev/null @@ -1,22 +0,0 @@ -FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base -USER $APP_UID -WORKDIR /app -EXPOSE 5000 - -FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build -ARG BUILD_CONFIGURATION=Release -WORKDIR /src -COPY ["Foxnouns.Backend/Foxnouns.Backend.csproj", "Foxnouns.Backend/"] -RUN dotnet restore "Foxnouns.Backend/Foxnouns.Backend.csproj" -COPY . . -WORKDIR "/src/Foxnouns.Backend" -RUN dotnet build "Foxnouns.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/build - -FROM build AS publish -ARG BUILD_CONFIGURATION=Release -RUN dotnet publish "Foxnouns.Backend.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false - -FROM base AS final -WORKDIR /app -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "Foxnouns.Backend.dll", "--migrate-and-start"] diff --git a/ENDPOINTS.md b/ENDPOINTS.md deleted file mode 100644 index ddadf1e..0000000 --- a/ENDPOINTS.md +++ /dev/null @@ -1,48 +0,0 @@ -# List of API endpoints and scopes - -## Scopes - -- `identify`: `@me` will refer to token user (always granted) -- `user.read_hidden`: can read non-privileged hidden information such as timezone, - whether the member list is hidden, and whether a member is unlisted. -- `user.read_privileged`: can read privileged information such as authentication methods -- `user.update`: can update the user's profile. - **cannot** update anything locked behind `user.read_privileged` -- `member.read`: can view member list if it's hidden and enumerate unlisted members -- `member.create`: can create new members -- `member.update`: can edit and delete members - -## Meta - -- [x] GET `/meta`: gets stats and server information - -## Users - -- [x] GET `/users/{userRef}`: views current user. - `identify` required to use `@me` as user reference. - `user.read_hidden` required to view timezone and other hidden non-privileged data. - `user.read_privileged` required to view authentication methods. - `member.read` required to view unlisted members. -- [x] PATCH `/users/@me`: updates current user. `user.update` required -- [x] PATCH `/users/@me/custom-preferences`: updates user's custom preferences. `user.update` required -- [ ] DELETE `/users/@me`: deletes current user. `*` required -- [ ] POST `/users/@me/export`: queues new data export. `*` required -- [ ] GET `/users/@me/export`: gets latest data export. `*` required -- [ ] GET `/users/@me/flags`: get all the user's flags. `identify` required -- [ ] POST `/users/@me/flags`: creates a new flag. `user.update` required -- [ ] PATCH `/users/@me/flags/{id}`: updates an existing flag. `user.update` required -- [ ] DELETE `/users/@me/flags/{id}`: deletes a user flag. `user.update` required -- [ ] POST `/users/@me/reroll`: rerolls a user's short ID. `user.update` required - -## Members - -- [x] GET `/users/{userRef}/members`: gets list of a user's members. - if the user's member list is hidden, - and it is not the authenticated user (or the token doesn't have the `member.read` scope) - returns an empty array. -- [x] GET `/users/{userRef}/members/{memberRef}`: gets a single member. - will always return a member if it exists, even if the member is unlisted. -- [x] POST `/users/@me/members`: creates a new member. `member.create` required -- [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required -- [x] DELETE `/users/@me/members/{memberRef}`: deletes a member. `member.update` required -- [ ] POST `/users/@me/members/{memberRef}/reroll`: rerolls a member's short ID. `member.update` required. diff --git a/Foxnouns.Backend/BuildInfo.cs b/Foxnouns.Backend/BuildInfo.cs index 1e27874..fe39640 100644 --- a/Foxnouns.Backend/BuildInfo.cs +++ b/Foxnouns.Backend/BuildInfo.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . namespace Foxnouns.Backend; public static class BuildInfo @@ -21,27 +7,20 @@ public static class BuildInfo public static async Task ReadBuildInfo() { - await using Stream? stream = typeof(BuildInfo).Assembly.GetManifestResourceStream( - "version" - ); - if (stream == null) - return; + await using var stream = typeof(BuildInfo).Assembly.GetManifestResourceStream("version"); + if (stream == null) return; using var reader = new StreamReader(stream); - string[] data = (await reader.ReadToEndAsync()).Trim().Split("\n"); - if (data.Length < 3) - return; + var data = (await reader.ReadToEndAsync()).Trim().Split("\n"); + if (data.Length < 3) return; Hash = data[0]; - bool dirty = data[2] == "dirty"; + var dirty = data[2] == "dirty"; - string[] versionData = data[1].Split("-"); - if (versionData.Length < 3) - return; + var versionData = data[1].Split("-"); + if (versionData.Length < 3) return; Version = versionData[0]; - if (versionData[1] != "0" || dirty) - Version += $"+{versionData[2]}"; - if (dirty) - Version += ".dirty"; + if (versionData[1] != "0" || dirty) Version += $"+{versionData[2]}"; + if (dirty) Version += ".dirty"; } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 461e55e..39a417f 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -1,115 +1,24 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable UnusedAutoPropertyAccessor.Global using Serilog.Events; namespace Foxnouns.Backend; public class Config { - public string Host { get; init; } = "localhost"; - public int Port { get; init; } = 3000; - public string BaseUrl { get; init; } = null!; - public string MediaBaseUrl { get; init; } = null!; + public string Host { get; set; } = "localhost"; + public int Port { get; set; } = 3000; + public string BaseUrl { get; set; } = null!; public string Address => $"http://{Host}:{Port}"; - public LoggingConfig Logging { get; init; } = new(); - public DatabaseConfig Database { get; init; } = new(); - public StorageConfig Storage { get; init; } = new(); - public LimitsConfig Limits { get; init; } = new(); - public EmailAuthConfig EmailAuth { get; init; } = new(); - public DiscordAuthConfig DiscordAuth { get; init; } = new(); - public GoogleAuthConfig GoogleAuth { get; init; } = new(); - public TumblrAuthConfig TumblrAuth { get; init; } = new(); + public string? SeqLogUrl { get; set; } + public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug; - public class LoggingConfig - { - public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug; - public string? SeqLogUrl { get; init; } - public string? SentryUrl { get; init; } - public bool SentryTracing { get; init; } = false; - public double SentryTracesSampleRate { get; init; } = 0.0; - public bool LogQueries { get; init; } = false; - public bool EnableMetrics { get; init; } = false; - public ushort MetricsPort { get; init; } = 5001; - } + public DatabaseConfig Database { get; set; } = new(); public class DatabaseConfig { - public string Url { get; init; } = string.Empty; - public bool? EnablePooling { get; init; } - public int? Timeout { get; init; } - public int? MaxPoolSize { get; init; } - public string Redis { get; init; } = string.Empty; + public string Url { get; set; } = string.Empty; + public int? Timeout { get; set; } + public int? MaxPoolSize { get; set; } } - - public class StorageConfig - { - public string Endpoint { get; init; } = string.Empty; - public string AccessKey { get; init; } = string.Empty; - public string SecretKey { get; init; } = string.Empty; - public string Bucket { get; init; } = string.Empty; - } - - public class EmailAuthConfig - { - public bool Enabled => From != null; - public string? From { get; init; } - } - - public class DiscordAuthConfig - { - public bool Enabled => ClientId != null && ClientSecret != null; - - public string? ClientId { get; init; } - public string? ClientSecret { get; init; } - } - - public class GoogleAuthConfig - { - public bool Enabled => ClientId != null && ClientSecret != null; - - public string? ClientId { get; init; } - public string? ClientSecret { get; init; } - } - - public class TumblrAuthConfig - { - public bool Enabled => ClientId != null && ClientSecret != null; - - public string? ClientId { get; init; } - public string? ClientSecret { get; init; } - } - - public class LimitsConfig - { - public int MaxMemberCount { get; init; } = 1000; - - public int MaxFields { get; init; } = 25; - public int MaxFieldNameLength { get; init; } = 100; - public int MaxFieldEntryTextLength { get; init; } = 100; - public int MaxFieldEntries { get; init; } = 100; - - public int MaxUsernameLength { get; init; } = 40; - public int MaxMemberNameLength { get; init; } = 100; - public int MaxDisplayNameLength { get; init; } = 100; - public int MaxLinks { get; init; } = 25; - public int MaxLinkLength { get; init; } = 256; - public int MaxBioLength { get; init; } = 1024; - public int MaxAvatarLength { get; init; } = 1_500_000; - } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/ApiControllerBase.cs b/Foxnouns.Backend/Controllers/ApiControllerBase.cs index 75a256e..d30803e 100644 --- a/Foxnouns.Backend/Controllers/ApiControllerBase.cs +++ b/Foxnouns.Backend/Controllers/ApiControllerBase.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc; @@ -24,4 +10,4 @@ public class ApiControllerBase : ControllerBase { internal Token? CurrentToken => HttpContext.GetToken(); internal User? CurrentUser => HttpContext.GetUser(); -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs deleted file mode 100644 index 39d3b11..0000000 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -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 Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Controllers.Authentication; - -[Route("/api/internal/auth")] -[ApiExplorerSettings(IgnoreApi = true)] -public class AuthController( - Config config, - DatabaseContext db, - KeyCacheService keyCacheService, - ILogger logger -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpPost("urls")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task UrlsAsync(CancellationToken ct = default) - { - _logger.Debug( - "Generating auth URLs for Discord: {Discord}, Google: {Google}, Tumblr: {Tumblr}", - config.DiscordAuth.Enabled, - config.GoogleAuth.Enabled, - config.TumblrAuth.Enabled - ); - string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync()); - string? discord = null; - string? google = null; - string? tumblr = null; - if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) - { - discord = - "https://discord.com/oauth2/authorize?response_type=code" - + $"&client_id={config.DiscordAuth.ClientId}&scope=identify" - + $"&prompt=none&state={state}" - + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; - } - - if (config.GoogleAuth is { ClientId: not null, ClientSecret: not null }) - { - google = - "https://accounts.google.com/o/oauth2/auth?response_type=code" - + $"&client_id={config.GoogleAuth.ClientId}" - + $"&scope=openid+{HttpUtility.UrlEncode("https://www.googleapis.com/auth/userinfo.email")}" - + $"&prompt=select_account&state={state}" - + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/google")}"; - } - - if (config.TumblrAuth is { ClientId: not null, ClientSecret: not null }) - { - tumblr = - "https://www.tumblr.com/oauth2/authorize?response_type=code" - + $"&client_id={config.TumblrAuth.ClientId}" - + $"&scope=basic&state={state}" - + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/tumblr")}"; - } - - return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, google, tumblr)); - } - - [HttpPost("force-log-out")] - [Authorize("identify")] - public async Task ForceLogoutAsync() - { - _logger.Information("Invalidating all tokens for user {UserId}", CurrentUser!.Id); - await db - .Tokens.Where(t => t.UserId == CurrentUser.Id) - .ExecuteUpdateAsync(s => s.SetProperty(t => t.ManuallyExpired, true)); - - return NoContent(); - } - - [HttpGet("methods/{id}")] - [Authorize("*")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetAuthMethodAsync(Snowflake id) - { - AuthMethod? authMethod = await db - .AuthMethods.Include(a => a.FediverseApplication) - .FirstOrDefaultAsync(a => a.UserId == CurrentUser!.Id && a.Id == id); - if (authMethod == null) - throw new ApiError.NotFound("No authentication method with that ID found."); - - return Ok(UserRendererService.RenderAuthMethod(authMethod)); - } - - [HttpDelete("methods/{id}")] - [Authorize("*")] - public async Task DeleteAuthMethodAsync(Snowflake id) - { - List authMethods = await db - .AuthMethods.Where(a => a.UserId == CurrentUser!.Id) - .ToListAsync(); - if (authMethods.Count < 2) - { - throw new ApiError( - "You cannot remove your last authentication method.", - HttpStatusCode.BadRequest, - ErrorCode.LastAuthMethod - ); - } - - AuthMethod? authMethod = authMethods.FirstOrDefault(a => a.Id == id); - if (authMethod == null) - throw new ApiError.NotFound("No authentication method with that ID found."); - - _logger.Debug( - "Deleting auth method {AuthMethodId} for user {UserId}", - authMethod.Id, - CurrentUser!.Id - ); - - // If this is the user's last email, we should also clear the user's password. - if ( - authMethod.AuthType == AuthType.Email - && authMethods.Count(a => a.AuthType == AuthType.Email) == 1 - ) - { - _logger.Debug( - "Deleted last email address for user {UserId}, resetting their password", - CurrentUser.Id - ); - CurrentUser.Password = null; - db.Update(CurrentUser); - } - - db.Remove(authMethod); - await db.SaveChangesAsync(); - - return NoContent(); - } -} diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs deleted file mode 100644 index a82956a..0000000 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Net; -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; -using Foxnouns.Backend.Services.Auth; -using Foxnouns.Backend.Utils; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers.Authentication; - -[Route("/api/internal/auth/discord")] -[ApiExplorerSettings(IgnoreApi = true)] -public class DiscordAuthController( - [UsedImplicitly] Config config, - ILogger logger, - DatabaseContext db, - KeyCacheService keyCacheService, - AuthService authService, - RemoteAuthService remoteAuthService -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpPost("callback")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] CallbackRequest req) - { - CheckRequirements(); - await keyCacheService.ValidateAuthStateAsync(req.State); - - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync( - req.Code - ); - User? user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); - if (user != null) - return Ok(await authService.GenerateUserTokenAsync(user)); - - _logger.Debug( - "Discord user {Username} ({Id}) authenticated with no local account", - remoteUser.Username, - remoteUser.Id - ); - - string ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync( - $"discord:{ticket}", - remoteUser, - Duration.FromMinutes(20) - ); - - return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null)); - } - - [HttpPost("register")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] OauthRegisterRequest req) - { - RemoteAuthService.RemoteUser? remoteUser = - await keyCacheService.GetKeyAsync( - $"discord:{req.Ticket}" - ); - if (remoteUser == null) - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - if ( - await db.AuthMethods.AnyAsync(a => - a.AuthType == AuthType.Discord && a.RemoteId == remoteUser.Id - ) - ) - { - _logger.Error( - "Discord user {Id} has valid ticket but is already linked to an existing account", - remoteUser.Id - ); - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - } - - User user = await authService.CreateUserWithRemoteAuthAsync( - req.Username, - AuthType.Discord, - remoteUser.Id, - remoteUser.Username - ); - - return Ok(await authService.GenerateUserTokenAsync(user)); - } - - [HttpGet("add-account")] - [Authorize("*")] - public async Task AddDiscordAccountAsync() - { - CheckRequirements(); - - string state = await remoteAuthService.ValidateAddAccountRequestAsync( - CurrentUser!.Id, - AuthType.Discord - ); - - string url = - "https://discord.com/oauth2/authorize?response_type=code" - + $"&client_id={config.DiscordAuth.ClientId}&scope=identify" - + $"&prompt=none&state={state}" - + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; - - return Ok(new SingleUrlResponse(url)); - } - - [HttpPost("add-account/callback")] - [Authorize("*")] - public async Task AddAccountCallbackAsync([FromBody] CallbackRequest req) - { - CheckRequirements(); - - await remoteAuthService.ValidateAddAccountStateAsync( - req.State, - CurrentUser!.Id, - AuthType.Discord - ); - - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestDiscordTokenAsync( - req.Code - ); - try - { - AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser.Id, - AuthType.Discord, - remoteUser.Id, - remoteUser.Username - ); - _logger.Debug( - "Added new Discord auth method {AuthMethodId} to user {UserId}", - authMethod.Id, - CurrentUser.Id - ); - - return Ok( - new AddOauthAccountResponse( - authMethod.Id, - AuthType.Discord, - authMethod.RemoteId, - authMethod.RemoteUsername - ) - ); - } - catch (UniqueConstraintException) - { - throw new ApiError( - "That account is already linked.", - HttpStatusCode.BadRequest, - ErrorCode.AccountAlreadyLinked - ); - } - } - - private void CheckRequirements() - { - if (!config.DiscordAuth.Enabled) - { - throw new ApiError.BadRequest( - "Discord authentication is not enabled on this instance." - ); - } - } -} diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs deleted file mode 100644 index 8024ee6..0000000 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ /dev/null @@ -1,374 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -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; -using Foxnouns.Backend.Services.Auth; -using Foxnouns.Backend.Utils; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers.Authentication; - -[Route("/api/internal/auth/email")] -[ApiExplorerSettings(IgnoreApi = true)] -public class EmailAuthController( - [UsedImplicitly] Config config, - DatabaseContext db, - AuthService authService, - MailService mailService, - EmailRateLimiter rateLimiter, - KeyCacheService keyCacheService, - UserRendererService userRenderer, - IClock clock, - ILogger logger -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpPost("register/init")] - public async Task RegisterInitAsync( - [FromBody] EmailRegisterRequest req, - CancellationToken ct = default - ) - { - CheckRequirements(); - - if (!req.Email.Contains('@')) - throw new ApiError.BadRequest("Email is invalid", "email", req.Email); - - string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null); - - // If there's already a user with that email address, pretend we sent an email but actually ignore it - if ( - await db.AuthMethods.AnyAsync( - a => a.AuthType == AuthType.Email && a.RemoteId == req.Email, - ct - ) - ) - { - return NoContent(); - } - - if (IsRateLimited()) - return NoContent(); - - mailService.QueueAccountCreationEmail(req.Email, state); - return NoContent(); - } - - [HttpPost("callback")] - public async Task CallbackAsync([FromBody] EmailCallbackRequest req) - { - CheckRequirements(); - - RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State); - if (state is not { ExistingUserId: null }) - throw new ApiError.BadRequest("Invalid state", "state", req.State); - - string ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); - - return Ok(new CallbackResponse(false, ticket, state.Email, null, null, null)); - } - - [HttpPost("register")] - public async Task CompleteRegistrationAsync( - [FromBody] EmailCompleteRegistrationRequest req - ) - { - CheckRequirements(); - - string? email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); - if (email == null) - throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket); - - User user = await authService.CreateUserWithPasswordAsync( - req.Username, - email, - req.Password - ); - Application frontendApp = await db.GetFrontendApplicationAsync(); - - (string? tokenStr, Token? token) = authService.GenerateToken( - user, - frontendApp, - ["*"], - clock.GetCurrentInstant() + Duration.FromDays(365) - ); - db.Add(token); - - await db.SaveChangesAsync(); - - await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}"); - - return Ok( - new AuthResponse( - await userRenderer.RenderUserAsync(user, user, renderMembers: false), - tokenStr, - token.ExpiresAt - ) - ); - } - - [HttpPost("login")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task LoginAsync( - [FromBody] EmailLoginRequest req, - CancellationToken ct = default - ) - { - CheckRequirements(); - - (User? user, AuthService.EmailAuthenticationResult authenticationResult) = - await authService.AuthenticateUserAsync(req.Email, req.Password, ct); - if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired) - throw new NotImplementedException("MFA is not implemented yet"); - - Application frontendApp = await db.GetFrontendApplicationAsync(ct); - - _logger.Debug("Logging user {Id} in with email and password", user.Id); - - (string? tokenStr, Token? token) = authService.GenerateToken( - user, - frontendApp, - ["*"], - clock.GetCurrentInstant() + Duration.FromDays(365) - ); - db.Add(token); - - _logger.Debug("Generated token {TokenId} for {UserId}", token.Id, user.Id); - - await db.SaveChangesAsync(ct); - - return Ok( - new AuthResponse( - await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), - tokenStr, - token.ExpiresAt - ) - ); - } - - [HttpPost("change-password")] - [Authorize("*")] - public async Task UpdatePasswordAsync([FromBody] EmailChangePasswordRequest req) - { - if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Current)) - throw new ApiError.Forbidden("Invalid password"); - - ValidationUtils.Validate([("new", ValidationUtils.ValidatePassword(req.New))]); - - await authService.SetUserPasswordAsync(CurrentUser!, req.New); - await db.SaveChangesAsync(); - return NoContent(); - } - - [HttpPost("forgot-password")] - public async Task ForgotPasswordAsync([FromBody] EmailForgotPasswordRequest req) - { - CheckRequirements(); - - if (!req.Email.Contains('@')) - throw new ApiError.BadRequest("Email is invalid", "email", req.Email); - - AuthMethod? authMethod = await db - .AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email) - .FirstOrDefaultAsync(); - if (authMethod == null) - return NoContent(); - - string state = await keyCacheService.GenerateForgotPasswordStateAsync( - req.Email, - authMethod.UserId - ); - - if (IsRateLimited()) - return NoContent(); - - mailService.QueueResetPasswordEmail(req.Email, state); - return NoContent(); - } - - [HttpPost("reset-password")] - public async Task ResetPasswordAsync([FromBody] EmailResetPasswordRequest req) - { - ForgotPasswordState? state = await keyCacheService.GetForgotPasswordStateAsync(req.State); - if (state == null) - throw new ApiError.BadRequest("Unknown state", "state", req.State); - - if ( - !await db - .AuthMethods.Where(m => - m.AuthType == AuthType.Email - && m.RemoteId == state.Email - && m.UserId == state.UserId - ) - .AnyAsync() - ) - { - throw new ApiError.BadRequest("Invalid state"); - } - - ValidationUtils.Validate([("password", ValidationUtils.ValidatePassword(req.Password))]); - - User user = await db.Users.FirstAsync(u => u.Id == state.UserId); - await authService.SetUserPasswordAsync(user, req.Password); - await db.SaveChangesAsync(); - - mailService.QueuePasswordChangedEmail(state.Email); - - return NoContent(); - } - - [HttpPost("add-account")] - [Authorize("*")] - public async Task AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) - { - CheckRequirements(); - - List emails = await db - .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email) - .ToListAsync(); - if (emails.Count > AuthUtils.MaxAuthMethodsPerType) - { - throw new ApiError.BadRequest( - "Too many email addresses, maximum of 3 per account.", - "email", - null - ); - } - - if (emails.Count != 0) - { - if (!await authService.ValidatePasswordAsync(CurrentUser!, req.Password)) - throw new ApiError.Forbidden("Invalid password"); - } - else - { - ValidationUtils.Validate( - [("password", ValidationUtils.ValidatePassword(req.Password))] - ); - await authService.SetUserPasswordAsync(CurrentUser!, req.Password); - await db.SaveChangesAsync(); - } - - string state = await keyCacheService.GenerateRegisterEmailStateAsync( - req.Email, - CurrentUser!.Id - ); - - bool emailExists = await db - .AuthMethods.Where(m => m.AuthType == AuthType.Email && m.RemoteId == req.Email) - .AnyAsync(); - if (emailExists) - { - return NoContent(); - } - - if (IsRateLimited()) - return NoContent(); - - mailService.QueueAddEmailAddressEmail(req.Email, state, CurrentUser.Username); - return NoContent(); - } - - [HttpPost("add-account/callback")] - [Authorize("*")] - public async Task AddEmailCallbackAsync([FromBody] EmailCallbackRequest req) - { - CheckRequirements(); - - RegisterEmailState? state = await keyCacheService.GetRegisterEmailStateAsync(req.State); - if (state?.ExistingUserId != CurrentUser!.Id) - throw new ApiError.BadRequest("Invalid state", "state", req.State); - - try - { - AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser.Id, - AuthType.Email, - state.Email - ); - _logger.Debug( - "Added email auth {AuthId} for user {UserId}", - authMethod.Id, - CurrentUser.Id - ); - - return Ok( - new AddOauthAccountResponse( - authMethod.Id, - AuthType.Email, - authMethod.RemoteId, - null - ) - ); - } - catch (UniqueConstraintException) - { - throw new ApiError( - "That email address is already linked.", - HttpStatusCode.BadRequest, - ErrorCode.AccountAlreadyLinked - ); - } - } - - public record AddEmailAddressRequest(string Email, string Password); - - private void CheckRequirements() - { - if (!config.EmailAuth.Enabled) - throw new ApiError.BadRequest("Email authentication is not enabled on this instance."); - } - - /// - /// Checks whether the context's IP address is rate limited from dispatching emails. - /// - private bool IsRateLimited() - { - if (HttpContext.Connection.RemoteIpAddress == null) - { - _logger.Information( - "No remote IP address in HTTP context for email-related request, ignoring as we can't rate limit it" - ); - return true; - } - - if ( - !rateLimiter.IsLimited( - HttpContext.Connection.RemoteIpAddress.ToString(), - out Duration retryAfter - ) - ) - { - return false; - } - - _logger.Information( - "IP address cannot send email until {RetryAfter}, ignoring", - retryAfter - ); - return true; - } -} diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs deleted file mode 100644 index 1ae2ef9..0000000 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -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; -using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers.Authentication; - -[Route("/api/internal/auth/fediverse")] -[ApiExplorerSettings(IgnoreApi = true)] -public class FediverseAuthController( - ILogger logger, - DatabaseContext db, - FediverseAuthService fediverseAuthService, - AuthService authService, - RemoteAuthService remoteAuthService, - KeyCacheService keyCacheService -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpGet] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetFediverseUrlAsync( - [FromQuery] string instance, - [FromQuery(Name = "force-refresh")] bool forceRefresh = false - ) - { - if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) - throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); - - string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); - return Ok(new SingleUrlResponse(url)); - } - - [HttpPost("callback")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task FediverseCallbackAsync([FromBody] FediverseCallbackRequest req) - { - FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); - FediverseAuthService.FediverseUser remoteUser = - await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code, req.State); - - User? user = await authService.AuthenticateUserAsync( - AuthType.Fediverse, - remoteUser.Id, - app - ); - if (user != null) - return Ok(await authService.GenerateUserTokenAsync(user)); - - string ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync( - $"fediverse:{ticket}", - new FediverseTicketData(app.Id, remoteUser), - Duration.FromMinutes(20) - ); - - return Ok( - new CallbackResponse( - false, - ticket, - $"@{remoteUser.Username}@{app.Domain}", - null, - null, - null - ) - ); - } - - [HttpPost("register")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] OauthRegisterRequest req) - { - FediverseTicketData? ticketData = await keyCacheService.GetKeyAsync( - $"fediverse:{req.Ticket}" - ); - if (ticketData == null) - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - - FediverseApplication? app = await db.FediverseApplications.FindAsync( - ticketData.ApplicationId - ); - if (app == null) - throw new FoxnounsError("Null application found for ticket"); - - if ( - await db.AuthMethods.AnyAsync(a => - a.AuthType == AuthType.Fediverse - && a.RemoteId == ticketData.User.Id - && a.FediverseApplicationId == app.Id - ) - ) - { - _logger.Error( - "Fediverse user {Id}/{ApplicationId} ({Username} on {Domain}) has valid ticket but is already linked to an existing account", - ticketData.User.Id, - ticketData.ApplicationId, - ticketData.User.Username, - app.Domain - ); - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - } - - User user = await authService.CreateUserWithRemoteAuthAsync( - req.Username, - AuthType.Fediverse, - ticketData.User.Id, - ticketData.User.Username, - app - ); - - return Ok(await authService.GenerateUserTokenAsync(user)); - } - - [HttpGet("add-account")] - [Authorize("*")] - public async Task AddFediverseAccountAsync( - [FromQuery] string instance, - [FromQuery(Name = "force-refresh")] bool forceRefresh = false - ) - { - if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) - throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); - - string state = await remoteAuthService.ValidateAddAccountRequestAsync( - CurrentUser!.Id, - AuthType.Fediverse, - instance - ); - - string url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh, state); - return Ok(new SingleUrlResponse(url)); - } - - [HttpPost("add-account/callback")] - [Authorize("*")] - public async Task AddAccountCallbackAsync( - [FromBody] FediverseCallbackRequest req - ) - { - FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); - FediverseAuthService.FediverseUser remoteUser = - await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); - try - { - AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser!.Id, - AuthType.Fediverse, - remoteUser.Id, - remoteUser.Username, - app - ); - _logger.Debug( - "Added new Fediverse auth method {AuthMethodId} to user {UserId}", - authMethod.Id, - CurrentUser.Id - ); - - return Ok( - new AddOauthAccountResponse( - authMethod.Id, - AuthType.Fediverse, - authMethod.RemoteId, - $"{authMethod.RemoteUsername}@{app.Domain}" - ) - ); - } - catch (UniqueConstraintException) - { - throw new ApiError( - "That account is already linked.", - HttpStatusCode.BadRequest, - ErrorCode.AccountAlreadyLinked - ); - } - } - - private record FediverseTicketData( - Snowflake ApplicationId, - FediverseAuthService.FediverseUser User - ); -} diff --git a/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs deleted file mode 100644 index 0f386d7..0000000 --- a/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Net; -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; -using Foxnouns.Backend.Services.Auth; -using Foxnouns.Backend.Utils; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers.Authentication; - -[Route("/api/internal/auth/google")] -[ApiExplorerSettings(IgnoreApi = true)] -public class GoogleAuthController( - [UsedImplicitly] Config config, - ILogger logger, - DatabaseContext db, - KeyCacheService keyCacheService, - AuthService authService, - RemoteAuthService remoteAuthService -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpPost("callback")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] CallbackRequest req) - { - CheckRequirements(); - await keyCacheService.ValidateAuthStateAsync(req.State); - - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestGoogleTokenAsync( - req.Code - ); - User? user = await authService.AuthenticateUserAsync(AuthType.Google, remoteUser.Id); - if (user != null) - return Ok(await authService.GenerateUserTokenAsync(user)); - - _logger.Debug( - "Google user {Username} ({Id}) authenticated with no local account", - remoteUser.Username, - remoteUser.Id - ); - - string ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync($"google:{ticket}", remoteUser, Duration.FromMinutes(20)); - - return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null)); - } - - [HttpPost("register")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] OauthRegisterRequest req) - { - RemoteAuthService.RemoteUser? remoteUser = - await keyCacheService.GetKeyAsync($"google:{req.Ticket}"); - if (remoteUser == null) - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - if ( - await db.AuthMethods.AnyAsync(a => - a.AuthType == AuthType.Google && a.RemoteId == remoteUser.Id - ) - ) - { - _logger.Error( - "Google user {Id} has valid ticket but is already linked to an existing account", - remoteUser.Id - ); - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - } - - User user = await authService.CreateUserWithRemoteAuthAsync( - req.Username, - AuthType.Google, - remoteUser.Id, - remoteUser.Username - ); - - return Ok(await authService.GenerateUserTokenAsync(user)); - } - - [HttpGet("add-account")] - [Authorize("*")] - public async Task AddGoogleAccountAsync() - { - CheckRequirements(); - - string state = await remoteAuthService.ValidateAddAccountRequestAsync( - CurrentUser!.Id, - AuthType.Google - ); - - string url = - "https://accounts.google.com/o/oauth2/auth?response_type=code" - + $"&client_id={config.GoogleAuth.ClientId}" - + $"&scope=openid+{HttpUtility.UrlEncode("https://www.googleapis.com/auth/userinfo.email")}" - + $"&prompt=select_account&state={state}" - + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/google")}"; - - return Ok(new SingleUrlResponse(url)); - } - - [HttpPost("add-account/callback")] - [Authorize("*")] - public async Task AddAccountCallbackAsync([FromBody] CallbackRequest req) - { - CheckRequirements(); - - await remoteAuthService.ValidateAddAccountStateAsync( - req.State, - CurrentUser!.Id, - AuthType.Google - ); - - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestGoogleTokenAsync( - req.Code - ); - try - { - AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser.Id, - AuthType.Google, - remoteUser.Id, - remoteUser.Username - ); - _logger.Debug( - "Added new Google auth method {AuthMethodId} to user {UserId}", - authMethod.Id, - CurrentUser.Id - ); - - return Ok( - new AddOauthAccountResponse( - authMethod.Id, - AuthType.Google, - authMethod.RemoteId, - authMethod.RemoteUsername - ) - ); - } - catch (UniqueConstraintException) - { - throw new ApiError( - "That account is already linked.", - HttpStatusCode.BadRequest, - ErrorCode.AccountAlreadyLinked - ); - } - } - - private void CheckRequirements() - { - if (!config.GoogleAuth.Enabled) - { - throw new ApiError.BadRequest("Google authentication is not enabled on this instance."); - } - } -} diff --git a/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs deleted file mode 100644 index 9860957..0000000 --- a/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Net; -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; -using Foxnouns.Backend.Services.Auth; -using Foxnouns.Backend.Utils; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers.Authentication; - -[Route("/api/internal/auth/tumblr")] -[ApiExplorerSettings(IgnoreApi = true)] -public class TumblrAuthController( - [UsedImplicitly] Config config, - ILogger logger, - DatabaseContext db, - KeyCacheService keyCacheService, - AuthService authService, - RemoteAuthService remoteAuthService -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpPost("callback")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] CallbackRequest req) - { - CheckRequirements(); - await keyCacheService.ValidateAuthStateAsync(req.State); - - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestTumblrTokenAsync( - req.Code - ); - User? user = await authService.AuthenticateUserAsync(AuthType.Tumblr, remoteUser.Id); - if (user != null) - return Ok(await authService.GenerateUserTokenAsync(user)); - - _logger.Debug( - "Tumblr user {Username} ({Id}) authenticated with no local account", - remoteUser.Username, - remoteUser.Id - ); - - string ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync($"tumblr:{ticket}", remoteUser, Duration.FromMinutes(20)); - - return Ok(new CallbackResponse(false, ticket, remoteUser.Username, null, null, null)); - } - - [HttpPost("register")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] OauthRegisterRequest req) - { - RemoteAuthService.RemoteUser? remoteUser = - await keyCacheService.GetKeyAsync($"tumblr:{req.Ticket}"); - if (remoteUser == null) - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - if ( - await db.AuthMethods.AnyAsync(a => - a.AuthType == AuthType.Tumblr && a.RemoteId == remoteUser.Id - ) - ) - { - _logger.Error( - "Tumblr user {Id} has valid ticket but is already linked to an existing account", - remoteUser.Id - ); - throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); - } - - User user = await authService.CreateUserWithRemoteAuthAsync( - req.Username, - AuthType.Tumblr, - remoteUser.Id, - remoteUser.Username - ); - - return Ok(await authService.GenerateUserTokenAsync(user)); - } - - [HttpGet("add-account")] - [Authorize("*")] - public async Task AddTumblrAccountAsync() - { - CheckRequirements(); - - string state = await remoteAuthService.ValidateAddAccountRequestAsync( - CurrentUser!.Id, - AuthType.Tumblr - ); - - string url = - "https://www.tumblr.com/oauth2/authorize?response_type=code" - + $"&client_id={config.TumblrAuth.ClientId}" - + $"&scope=basic&state={state}" - + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/tumblr")}"; - - return Ok(new SingleUrlResponse(url)); - } - - [HttpPost("add-account/callback")] - [Authorize("*")] - public async Task AddAccountCallbackAsync([FromBody] CallbackRequest req) - { - CheckRequirements(); - - await remoteAuthService.ValidateAddAccountStateAsync( - req.State, - CurrentUser!.Id, - AuthType.Tumblr - ); - - RemoteAuthService.RemoteUser remoteUser = await remoteAuthService.RequestTumblrTokenAsync( - req.Code - ); - try - { - AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser.Id, - AuthType.Tumblr, - remoteUser.Id, - remoteUser.Username - ); - _logger.Debug( - "Added new Tumblr auth method {AuthMethodId} to user {UserId}", - authMethod.Id, - CurrentUser.Id - ); - - return Ok( - new AddOauthAccountResponse( - authMethod.Id, - AuthType.Tumblr, - authMethod.RemoteId, - authMethod.RemoteUsername - ) - ); - } - catch (UniqueConstraintException) - { - throw new ApiError( - "That account is already linked.", - HttpStatusCode.BadRequest, - ErrorCode.AccountAlreadyLinked - ); - } - } - - private void CheckRequirements() - { - if (!config.TumblrAuth.Enabled) - { - throw new ApiError.BadRequest("Tumblr authentication is not enabled on this instance."); - } - } -} diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs new file mode 100644 index 0000000..328bf3d --- /dev/null +++ b/Foxnouns.Backend/Controllers/DebugController.cs @@ -0,0 +1,32 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Services; +using Microsoft.AspNetCore.Mvc; +using NodaTime; + +namespace Foxnouns.Backend.Controllers; + +[Route("/api/v2/debug")] +public class DebugController(DatabaseContext db, AuthService authSvc, IClock clock, ILogger logger) : ApiControllerBase +{ + [HttpPost("users")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))] + public async Task CreateUser([FromBody] CreateUserRequest req) + { + logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email); + + var user = await authSvc.CreateUserWithPasswordAsync(req.Username, req.Email, req.Password); + var frontendApp = await db.GetFrontendApplicationAsync(); + + var (tokenStr, token) = + authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); + db.Add(token); + + await db.SaveChangesAsync(); + + return Ok(new AuthResponse(user.Id, user.Username, tokenStr)); + } + + public record CreateUserRequest(string Username, string Password, string Email); + + public record AuthResponse(Snowflake Id, string Username, string Token); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/DeleteUserController.cs b/Foxnouns.Backend/Controllers/DeleteUserController.cs deleted file mode 100644 index d1c8e62..0000000 --- a/Foxnouns.Backend/Controllers/DeleteUserController.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Middleware; -using Microsoft.AspNetCore.Mvc; -using NodaTime; - -namespace Foxnouns.Backend.Controllers; - -[Route("/api/internal/self-delete")] -[Authorize("*")] -[ApiExplorerSettings(IgnoreApi = true)] -public class DeleteUserController(DatabaseContext db, IClock clock, ILogger logger) - : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpPost("delete")] - public async Task DeleteSelfAsync() - { - _logger.Information( - "User {UserId} has requested their account to be deleted", - CurrentUser!.Id - ); - - CurrentUser.Deleted = true; - CurrentUser.DeletedAt = clock.GetCurrentInstant(); - - db.Update(CurrentUser); - await db.SaveChangesAsync(); - return NoContent(); - } - - [HttpPost("force")] - [Limit(UsableByDeletedUsers = true)] - public async Task ForceDeleteAsync() - { - if (!CurrentUser!.Deleted) - throw new ApiError.BadRequest("Your account isn't deleted."); - - _logger.Information( - "User {UserId} has requested an early full delete of their account", - CurrentUser.Id - ); - - // This is the easiest way to force delete a user, don't judge me - CurrentUser.DeletedAt = clock.GetCurrentInstant() - Duration.FromDays(365); - db.Update(CurrentUser); - await db.SaveChangesAsync(); - return NoContent(); - } - - [HttpPost("undelete")] - [Limit(UsableByDeletedUsers = true)] - public async Task UndeleteSelfAsync() - { - if (!CurrentUser!.Deleted) - throw new ApiError.BadRequest("Your account isn't deleted."); - if (CurrentUser!.DeletedBy != null) - { - throw new ApiError.BadRequest( - "Your account has been suspended and can't be reactivated by yourself." - ); - } - - _logger.Information( - "User {UserId} has requested to undelete their account", - CurrentUser.Id - ); - - CurrentUser.Deleted = false; - CurrentUser.DeletedAt = null; - db.Update(CurrentUser); - await db.SaveChangesAsync(); - return NoContent(); - } -} diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs deleted file mode 100644 index 0442386..0000000 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -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; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers; - -[Route("/api/internal/data-exports")] -[Authorize("identify")] -[Limit(UsableByDeletedUsers = true)] -[ApiExplorerSettings(IgnoreApi = true)] -public class ExportsController(ILogger logger, Config config, IClock clock, DatabaseContext db) - : ApiControllerBase -{ - private static readonly Duration MinimumTimeBetween = Duration.FromDays(1); - private readonly ILogger _logger = logger.ForContext(); - - [HttpGet] - public async Task GetDataExportsAsync() - { - DataExport? export = await db - .DataExports.Where(d => d.UserId == CurrentUser!.Id) - .OrderByDescending(d => d.Id) - .FirstOrDefaultAsync(); - if (export == null) - return Ok(new DataExportResponse(null, null)); - - return Ok( - new DataExportResponse( - ExportUrl(CurrentUser!.Id, export.Filename), - export.Id.Time + DataExport.Expiration - ) - ); - } - - private string ExportUrl(Snowflake userId, string filename) => - $"{config.MediaBaseUrl}/data-exports/{userId}/{filename}/data-export.zip"; - - [HttpPost] - public async Task QueueDataExportAsync() - { - var snowflakeToCheck = Snowflake.FromInstant( - clock.GetCurrentInstant() - MinimumTimeBetween - ); - _logger.Debug( - "Checking if user {UserId} has data exports newer than {Snowflake}", - CurrentUser!.Id, - snowflakeToCheck - ); - if ( - await db.DataExports.AnyAsync(d => - d.UserId == CurrentUser.Id && d.Id > snowflakeToCheck - ) - ) - { - throw new ApiError.BadRequest("You can't request a new data export so soon."); - } - - CreateDataExportJob.Enqueue(CurrentUser.Id); - return NoContent(); - } -} diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs deleted file mode 100644 index bed022a..0000000 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ /dev/null @@ -1,209 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -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; -using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using XidNet; - -namespace Foxnouns.Backend.Controllers; - -[Route("/api/v2/users/@me/flags")] -public class FlagsController( - DatabaseContext db, - UserRendererService userRenderer, - ISnowflakeGenerator snowflakeGenerator -) : ApiControllerBase -{ - [HttpGet] - [Limit(UsableByDeletedUsers = true)] - [Authorize("user.read_flags")] - [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_flags")] - [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] - public async Task CreateFlagAsync([FromBody] CreateFlagRequest req) - { - int flagCount = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).CountAsync(); - if (flagCount >= MaxFlagCount) - throw new ApiError.BadRequest("Maximum number of flags reached"); - - ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); - - var flag = new PrideFlag - { - Id = snowflakeGenerator.GenerateSnowflake(), - LegacyId = Xid.NewXid().ToString(), - UserId = CurrentUser!.Id, - Name = req.Name, - Description = req.Description, - }; - - db.Add(flag); - await db.SaveChangesAsync(); - - CreateFlagJob.Enqueue(new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image)); - return Accepted(userRenderer.RenderPrideFlag(flag)); - } - - [HttpPatch("{id}")] - [Authorize("user.create_flags")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) - { - ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); - - 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."); - - if (req.Name != null) - flag.Name = req.Name; - - if (req.HasProperty(nameof(req.Description))) - flag.Description = req.Description; - - db.Update(flag); - await db.SaveChangesAsync(); - - return Ok(userRenderer.RenderPrideFlag(flag)); - } - - [HttpDelete("{id}")] - [Authorize("user.update_flags")] - public async Task DeleteFlagAsync(Snowflake id) - { - 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."); - - db.PrideFlags.Remove(flag); - await db.SaveChangesAsync(); - - return NoContent(); - } - - private static List<(string, ValidationError?)> ValidateFlag( - string? name, - string? description, - string? imageData - ) - { - var errors = new List<(string, ValidationError?)>(); - - if (name != null) - { - switch (name.Length) - { - case < 1: - errors.Add( - ( - "name", - ValidationError.LengthError("Name is too short", 1, 100, name.Length) - ) - ); - break; - case > 100: - errors.Add( - ( - "name", - ValidationError.LengthError("Name is too long", 1, 100, name.Length) - ) - ); - break; - } - } - - if (description != null) - { - switch (description.Length) - { - case < 1: - errors.Add( - ( - "description", - ValidationError.LengthError( - "Description is too short", - 1, - 100, - description.Length - ) - ) - ); - break; - case > 500: - errors.Add( - ( - "description", - ValidationError.LengthError( - "Description is too long", - 1, - 100, - description.Length - ) - ) - ); - break; - } - } - - if (imageData != null) - { - switch (imageData.Length) - { - case 0: - errors.Add( - ( - "image", - ValidationError.GenericValidationError("Image cannot be empty", null) - ) - ); - break; - case > 1_500_000: - errors.Add( - ( - "image", - ValidationError.GenericValidationError("Image is too large", null) - ) - ); - break; - } - } - - return errors; - } -} diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs deleted file mode 100644 index 3954547..0000000 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -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; -using Microsoft.AspNetCore.Mvc.Routing; -using Microsoft.AspNetCore.Routing.Template; - -namespace Foxnouns.Backend.Controllers; - -[ApiController] -[Route("/api/internal")] -[ApiExplorerSettings(IgnoreApi = true)] -public partial class InternalController(DatabaseContext db) : ControllerBase -{ - [GeneratedRegex(@"(\{\w+\})")] - private static partial Regex PathVarRegex(); - - [GeneratedRegex(@"\{id\}")] - private static partial Regex IdCountRegex(); - - private static string GetCleanedTemplate(string template) - { - if (template.StartsWith("api/v2")) - template = template["api/v2".Length..]; - else if (template.StartsWith("api/v1")) - template = template["api/v1".Length..]; - template = PathVarRegex() - .Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}` - .Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}` - - // If there's at least one path parameter, we only return the *first* part of the path. - if (template.Contains("{id}")) - { - // However, if the path starts with /users/{id} *and* there's another path parameter (such as a member ID) - // we ignore the leading /users/{id}. This is because a lot of routes are scoped by user, but should have - // separate rate limits from other user-scoped routes. - if (template.StartsWith("/users/{id}/") && IdCountRegex().Count(template) >= 2) - template = template["/users/{id}".Length..]; - - return template.Split("{id}")[0] + "{id}"; - } - - return template; - } - - [HttpPost("request-data")] - public async Task GetRequestDataAsync([FromBody] RequestDataRequest req) - { - RouteEndpoint? endpoint = GetEndpoint(HttpContext, req.Path, req.Method); - if (endpoint == null) - throw new ApiError.BadRequest("Path/method combination is invalid"); - - ControllerActionDescriptor? actionDescriptor = - endpoint.Metadata.GetMetadata(); - string? template = actionDescriptor?.AttributeRouteInfo?.Template; - if (template == null) - throw new FoxnounsError("Template value was null on valid endpoint"); - template = GetCleanedTemplate(template); - - // If no token was supplied, or it isn't valid base 64, return a null user ID (limiting by IP) - if (!AuthUtils.TryParseToken(req.Token, out byte[]? rawToken)) - return Ok(new RequestDataResponse(null, template)); - - Snowflake? userId = await db.GetTokenUserId(rawToken); - return Ok(new RequestDataResponse(userId, template)); - } - - private static RouteEndpoint? GetEndpoint( - HttpContext httpContext, - string url, - string requestMethod - ) - { - EndpointDataSource? endpointDataSource = - httpContext.RequestServices.GetService(); - if (endpointDataSource == null) - return null; - IEnumerable endpoints = endpointDataSource.Endpoints.OfType(); - - foreach (RouteEndpoint? endpoint in endpoints) - { - if (endpoint.RoutePattern.RawText == null) - continue; - - var templateMatcher = new TemplateMatcher( - TemplateParser.Parse(endpoint.RoutePattern.RawText), - new RouteValueDictionary() - ); - if (!templateMatcher.TryMatch(url, new RouteValueDictionary())) - continue; - HttpMethodAttribute? httpMethodAttribute = - endpoint.Metadata.GetMetadata(); - if ( - httpMethodAttribute?.HttpMethods.Any(x => - x.Equals(requestMethod, StringComparison.OrdinalIgnoreCase) - ) == false - ) - { - continue; - } - - return endpoint; - } - - return null; - } -} diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs deleted file mode 100644 index 635dab9..0000000 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ /dev/null @@ -1,322 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -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; -using Foxnouns.Backend.Services; -using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using NodaTime; -using XidNet; - -namespace Foxnouns.Backend.Controllers; - -[Route("/api/v2/users/{userRef}/members")] -public class MembersController( - ILogger logger, - DatabaseContext db, - MemberRendererService memberRenderer, - ISnowflakeGenerator snowflakeGenerator, - ObjectStorageService objectStorageService, - IClock clock, - ValidationService validationService, - Config config -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpGet] - [ProducesResponseType>(StatusCodes.Status200OK)] - [Limit(UsableByDeletedUsers = true)] - public async Task GetMembersAsync(string userRef, CancellationToken ct = default) - { - User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); - return Ok(await memberRenderer.RenderUserMembersAsync(user, CurrentToken)); - } - - [HttpGet("{memberRef}")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Limit(UsableByDeletedUsers = true)] - public async Task GetMemberAsync( - string userRef, - string memberRef, - CancellationToken ct = default - ) - { - Member member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct); - return Ok(memberRenderer.RenderMember(member, CurrentToken)); - } - - [HttpPost("/api/v2/users/@me/members")] - [ProducesResponseType(StatusCodes.Status200OK)] - [Authorize("member.create")] - public async Task CreateMemberAsync( - [FromBody] CreateMemberRequest req, - CancellationToken ct = default - ) - { - ValidationUtils.Validate( - [ - ("name", validationService.ValidateMemberName(req.Name)), - ("display_name", validationService.ValidateDisplayName(req.DisplayName)), - ("bio", validationService.ValidateBio(req.Bio)), - ("avatar", validationService.ValidateAvatar(req.Avatar)), - .. validationService.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), - .. validationService.ValidateFieldEntries( - req.Names?.ToArray(), - CurrentUser!.CustomPreferences, - "names" - ), - .. validationService.ValidatePronouns( - req.Pronouns?.ToArray(), - CurrentUser!.CustomPreferences - ), - .. validationService.ValidateLinks(req.Links), - ] - ); - - int memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct); - if (memberCount >= config.Limits.MaxMemberCount) - throw new ApiError.BadRequest("Maximum number of members reached"); - - var member = new Member - { - Id = snowflakeGenerator.GenerateSnowflake(), - LegacyId = Xid.NewXid().ToString(), - User = CurrentUser!, - Name = req.Name, - DisplayName = req.DisplayName, - Bio = req.Bio, - Links = req.Links ?? [], - Fields = req.Fields ?? [], - Names = req.Names ?? [], - Pronouns = req.Pronouns ?? [], - Unlisted = req.Unlisted ?? false, - Sid = null!, - }; - db.Add(member); - - _logger.Debug( - "Creating member {MemberName} ({Id}) for {UserId}", - member.Name, - member.Id, - CurrentUser!.Id - ); - - CurrentUser.LastActive = clock.GetCurrentInstant(); - db.Update(CurrentUser); - - try - { - await db.SaveChangesAsync(ct); - } - catch (UniqueConstraintException) - { - _logger.Debug("Could not create member {Id} due to name conflict", member.Id); - throw new ApiError.BadRequest( - "A member with that name already exists", - "name", - req.Name - ); - } - - if (req.Avatar != null) - { - MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar)); - } - - return Ok(memberRenderer.RenderMember(member, CurrentToken)); - } - - [HttpPatch("/api/v2/users/@me/members/{memberRef}")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - [Authorize("member.update")] - public async Task UpdateMemberAsync( - string memberRef, - [FromBody] UpdateMemberRequest req - ) - { - await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(); - Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); - var errors = new List<(string, ValidationError?)>(); - - // We might add extra validations for names later down the line. - // These should only take effect when a member's name is changed, not on other changes. - if (req.Name != null && req.Name != member.Name) - { - errors.Add(("name", validationService.ValidateMemberName(req.Name))); - member.Name = req.Name; - } - - if (req.HasProperty(nameof(req.DisplayName))) - { - errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName))); - member.DisplayName = req.DisplayName; - } - - if (req.HasProperty(nameof(req.Bio))) - { - errors.Add(("bio", validationService.ValidateBio(req.Bio))); - member.Bio = req.Bio; - } - - if (req.HasProperty(nameof(req.Links))) - { - errors.AddRange(validationService.ValidateLinks(req.Links)); - member.Links = req.Links ?? []; - } - - if (req.HasProperty(nameof(req.Unlisted))) - member.Unlisted = req.Unlisted ?? false; - - if (req.Names != null) - { - errors.AddRange( - validationService.ValidateFieldEntries( - req.Names, - CurrentUser!.CustomPreferences, - "names" - ) - ); - member.Names = req.Names.ToList(); - } - - if (req.Pronouns != null) - { - errors.AddRange( - validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences) - ); - member.Pronouns = req.Pronouns.ToList(); - } - - if (req.Fields != null) - { - errors.AddRange( - validationService.ValidateFields( - req.Fields.ToList(), - CurrentUser!.CustomPreferences - ) - ); - member.Fields = req.Fields.ToList(); - } - - if (req.Flags != null) - { - ValidationError? flagError = await db.SetMemberFlagsAsync( - CurrentUser!.Id, - member.Id, - req.Flags - ); - if (flagError != null) - errors.Add(("flags", flagError)); - } - - if (req.HasProperty(nameof(req.Avatar))) - errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar))); - - ValidationUtils.Validate(errors); - // This is fired off regardless of whether the transaction is committed - // (atomic operations are hard when combined with background jobs) - // so it's in a separate block to the validation above. - if (req.HasProperty(nameof(req.Avatar))) - { - MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar)); - } - - CurrentUser.LastActive = clock.GetCurrentInstant(); - db.Update(CurrentUser); - - try - { - await db.SaveChangesAsync(); - } - catch (UniqueConstraintException) - { - _logger.Debug( - "Could not update member {Id} due to name conflict ({CurrentName} / {NewName})", - member.Id, - member.Name, - req.Name - ); - throw new ApiError.BadRequest( - "A member with that name already exists", - "name", - req.Name - ); - } - - await tx.CommitAsync(); - return Ok(memberRenderer.RenderMember(member, CurrentToken)); - } - - [HttpDelete("/api/v2/users/@me/members/{memberRef}")] - [Authorize("member.update")] - public async Task DeleteMemberAsync(string memberRef) - { - Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); - int deleteCount = await db - .Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id) - .ExecuteDeleteAsync(); - if (deleteCount == 0) - { - _logger.Warning( - "Successfully resolved member {Id} but could not delete them", - member.Id - ); - return NoContent(); - } - - if (member.Avatar != null) - await objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar); - return NoContent(); - } - - [HttpPost("/api/v2/users/@me/members/{memberRef}/reroll-sid")] - [Authorize("member.update")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task RerollSidAsync(string memberRef) - { - Member member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); - - Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); - if (CurrentUser!.LastSidReroll > minTimeAgo) - throw new ApiError.BadRequest("Cannot reroll short ID yet"); - - // Using ExecuteUpdateAsync here as the new short ID is generated by the database - await db - .Members.Where(m => m.Id == member.Id) - .ExecuteUpdateAsync(s => s.SetProperty(m => m.Sid, _ => db.FindFreeMemberSid())); - - await db - .Users.Where(u => u.Id == CurrentUser.Id) - .ExecuteUpdateAsync(s => - s.SetProperty(u => u.LastSidReroll, clock.GetCurrentInstant()) - .SetProperty(u => u.LastActive, clock.GetCurrentInstant()) - ); - - // Fetch the new sid then pass that to RenderMember - string newSid = await db - .Members.Where(m => m.Id == member.Id) - .Select(m => m.Sid) - .FirstAsync(); - return Ok(memberRenderer.RenderMember(member, CurrentToken, newSid)); - } -} diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs deleted file mode 100644 index 0166e86..0000000 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Text.RegularExpressions; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Services.Caching; -using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc; - -namespace Foxnouns.Backend.Controllers; - -[Route("/api/v2/meta")] -public partial class MetaController(Config config, NoticeCacheService noticeCache) - : ApiControllerBase -{ - private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; - - [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task GetMeta(CancellationToken ct = default) => - Ok( - new MetaResponse( - Repository, - BuildInfo.Version, - BuildInfo.Hash, - (int)FoxnounsMetrics.MemberCount.Value, - new UserInfoResponse( - (int)FoxnounsMetrics.UsersCount.Value, - (int)FoxnounsMetrics.UsersActiveMonthCount.Value, - (int)FoxnounsMetrics.UsersActiveWeekCount.Value, - (int)FoxnounsMetrics.UsersActiveDayCount.Value - ), - new LimitsResponse( - config.Limits.MaxMemberCount, - config.Limits.MaxBioLength, - ValidationUtils.MaxCustomPreferences, - AuthUtils.MaxAuthMethodsPerType, - FlagsController.MaxFlagCount - ), - Notice: NoticeResponse(await noticeCache.GetAsync(ct)) - ) - ); - - private static MetaNoticeResponse? NoticeResponse(Notice? notice) => - notice == null ? null : new MetaNoticeResponse(notice.Id, notice.Message); - - [HttpGet("page/{page}")] - public async Task GetStaticPageAsync(string page, CancellationToken ct = default) - { - if (!PageRegex().IsMatch(page)) - { - throw new ApiError.BadRequest("Invalid page name"); - } - - string path = Path.Join(Directory.GetCurrentDirectory(), "static-pages", $"{page}.md"); - try - { - string text = await System.IO.File.ReadAllTextAsync(path, ct); - return Ok(text); - } - catch (FileNotFoundException) - { - throw new ApiError.NotFound("Page not found", code: ErrorCode.PageNotFound); - } - } - - [HttpGet("/api/v2/coffee")] - public IActionResult BrewCoffee() => - StatusCode(StatusCodes.Status418ImATeapot, "Sorry, I'm a teapot!"); - - [GeneratedRegex(@"^[a-z\-_]+$")] - private static partial Regex PageRegex(); -} diff --git a/Foxnouns.Backend/Controllers/Moderation/AuditLogController.cs b/Foxnouns.Backend/Controllers/Moderation/AuditLogController.cs deleted file mode 100644 index 304cfa4..0000000 --- a/Foxnouns.Backend/Controllers/Moderation/AuditLogController.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Middleware; -using Foxnouns.Backend.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Controllers.Moderation; - -[Route("/api/v2/moderation/audit-log")] -[Authorize("user.moderation")] -[Limit(RequireModerator = true)] -public class AuditLogController(DatabaseContext db, ModerationRendererService moderationRenderer) - : ApiControllerBase -{ - public async Task GetAuditLogAsync( - [FromQuery] AuditLogEntryType? type = null, - [FromQuery] int? limit = null, - [FromQuery] Snowflake? before = null, - [FromQuery] Snowflake? after = null, - [FromQuery(Name = "by-moderator")] Snowflake? byModerator = null - ) - { - limit = limit switch - { - > 100 => 100, - < 0 => 100, - null => 100, - _ => limit, - }; - - IQueryable query = db - .AuditLog.Include(e => e.Report) - .OrderByDescending(e => e.Id); - - if (before != null) - query = query.Where(e => e.Id < before.Value); - else if (after != null) - query = query.Where(e => e.Id > after.Value); - - if (type != null) - query = query.Where(e => e.Type == type); - if (byModerator != null) - query = query.Where(e => e.ModeratorId == byModerator.Value); - - List entries = await query.Take(limit!.Value).ToListAsync(); - - return Ok(entries.Select(moderationRenderer.RenderAuditLogEntry)); - } - - [HttpGet("moderators")] - public async Task GetModeratorsAsync(CancellationToken ct = default) - { - var moderators = await db - .Users.Where(u => - !u.Deleted && (u.Role == UserRole.Admin || u.Role == UserRole.Moderator) - ) - .Select(u => new { u.Id, u.Username }) - .OrderBy(u => u.Id) - .ToListAsync(ct); - - return Ok(moderators); - } -} diff --git a/Foxnouns.Backend/Controllers/Moderation/LookupController.cs b/Foxnouns.Backend/Controllers/Moderation/LookupController.cs deleted file mode 100644 index 9e9fa7f..0000000 --- a/Foxnouns.Backend/Controllers/Moderation/LookupController.cs +++ /dev/null @@ -1,96 +0,0 @@ -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Middleware; -using Foxnouns.Backend.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Controllers.Moderation; - -[Route("/api/v2/moderation/lookup")] -[Authorize("user.moderation")] -[Limit(RequireModerator = true)] -public class LookupController( - DatabaseContext db, - UserRendererService userRenderer, - ModerationService moderationService, - ModerationRendererService moderationRenderer -) : ApiControllerBase -{ - [HttpPost] - public async Task QueryUsersAsync( - [FromBody] QueryUsersRequest req, - CancellationToken ct = default - ) - { - var query = db.Users.Select(u => new { u.Id, u.Username }); - query = req.Fuzzy - ? query.Where(u => u.Username.Contains(req.Query)) - : query.Where(u => u.Username == req.Query); - - var users = await query.OrderBy(u => u.Id).Take(100).ToListAsync(ct); - return Ok(users); - } - - [HttpGet("{id}")] - public async Task QueryUserAsync(Snowflake id, CancellationToken ct = default) - { - User user = await db.ResolveUserAsync(id, ct); - - bool showSensitiveData = await moderationService.ShowSensitiveDataAsync( - CurrentUser!, - user, - ct - ); - - List authMethods = showSensitiveData - ? await db - .AuthMethods.Where(a => a.UserId == user.Id) - .Include(a => a.FediverseApplication) - .ToListAsync(ct) - : []; - - return Ok( - new QueryUserResponse( - User: await userRenderer.RenderUserAsync( - user, - renderMembers: false, - renderAuthMethods: false, - ct: ct - ), - MemberListHidden: user.ListHidden, - LastActive: user.LastActive, - LastSidReroll: user.LastSidReroll, - Suspended: user is { Deleted: true, DeletedBy: not null }, - Deleted: user.Deleted, - ShowSensitiveData: showSensitiveData, - AuthMethods: showSensitiveData - ? authMethods.Select(UserRendererService.RenderAuthMethod) - : null - ) - ); - } - - [HttpPost("{id}/sensitive")] - public async Task QuerySensitiveUserDataAsync( - Snowflake id, - [FromBody] QuerySensitiveUserDataRequest req - ) - { - User user = await db.ResolveUserAsync(id); - - // Don't let mods accidentally spam the audit log - bool alreadyAuthorized = await moderationService.ShowSensitiveDataAsync(CurrentUser!, user); - if (alreadyAuthorized) - return NoContent(); - - AuditLogEntry entry = await moderationService.QuerySensitiveDataAsync( - CurrentUser!, - user, - req.Reason - ); - - return Ok(moderationRenderer.RenderAuditLogEntry(entry)); - } -} diff --git a/Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs b/Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs deleted file mode 100644 index 2fb4473..0000000 --- a/Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs +++ /dev/null @@ -1,138 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Net; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Middleware; -using Foxnouns.Backend.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Controllers.Moderation; - -[Route("/api/v2/moderation")] -[Authorize("user.moderation")] -[Limit(RequireModerator = true)] -public class ModActionsController( - DatabaseContext db, - ModerationService moderationService, - ModerationRendererService moderationRenderer -) : ApiControllerBase -{ - [HttpPost("warnings/{id}")] - public async Task WarnUserAsync(Snowflake id, [FromBody] WarnUserRequest req) - { - User user = await db.ResolveUserAsync(id); - if (user.Deleted) - { - throw new ApiError( - "This user is already deleted.", - HttpStatusCode.BadRequest, - ErrorCode.InvalidWarningTarget - ); - } - - if (user.Id == CurrentUser!.Id) - { - throw new ApiError( - "You can't warn yourself.", - HttpStatusCode.BadRequest, - ErrorCode.InvalidWarningTarget - ); - } - - Member? member = null; - if (req.MemberId != null) - { - member = await db.Members.FirstOrDefaultAsync(m => - m.Id == req.MemberId && m.UserId == user.Id - ); - if (member == null) - throw new ApiError.NotFound("No member with that ID found."); - } - - Report? report = null; - if (req.ReportId != null) - { - report = await db.Reports.FindAsync(req.ReportId); - if (report is not { Status: ReportStatus.Open }) - { - throw new ApiError.NotFound( - "No report with that ID found, or it's already closed." - ); - } - } - - AuditLogEntry entry = await moderationService.ExecuteWarningAsync( - CurrentUser, - user, - member, - report, - req.Reason, - req.ClearFields - ); - - return Ok(moderationRenderer.RenderAuditLogEntry(entry)); - } - - [HttpPost("suspensions/{id}")] - public async Task SuspendUserAsync( - Snowflake id, - [FromBody] SuspendUserRequest req - ) - { - User user = await db.ResolveUserAsync(id); - if (user.Deleted) - { - throw new ApiError( - "This user is already deleted.", - HttpStatusCode.BadRequest, - ErrorCode.InvalidWarningTarget - ); - } - - if (user.Id == CurrentUser!.Id) - { - throw new ApiError( - "You can't warn yourself.", - HttpStatusCode.BadRequest, - ErrorCode.InvalidWarningTarget - ); - } - - Report? report = null; - if (req.ReportId != null) - { - report = await db.Reports.FindAsync(req.ReportId); - if (report is not { Status: ReportStatus.Open }) - { - throw new ApiError.NotFound( - "No report with that ID found, or it's already closed." - ); - } - } - - AuditLogEntry entry = await moderationService.ExecuteSuspensionAsync( - CurrentUser, - user, - report, - req.Reason, - req.ClearProfile - ); - - return Ok(moderationRenderer.RenderAuditLogEntry(entry)); - } -} diff --git a/Foxnouns.Backend/Controllers/Moderation/NoticesController.cs b/Foxnouns.Backend/Controllers/Moderation/NoticesController.cs deleted file mode 100644 index 3d2d6bb..0000000 --- a/Foxnouns.Backend/Controllers/Moderation/NoticesController.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Middleware; -using Foxnouns.Backend.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers.Moderation; - -[Route("/api/v2/notices")] -[Authorize("user.moderation")] -[Limit(RequireModerator = true)] -public class NoticesController( - DatabaseContext db, - UserRendererService userRenderer, - ISnowflakeGenerator snowflakeGenerator, - IClock clock -) : ApiControllerBase -{ - [HttpGet] - public async Task GetNoticesAsync(CancellationToken ct = default) - { - List notices = await db - .Notices.Include(n => n.Author) - .OrderByDescending(n => n.Id) - .ToListAsync(ct); - return Ok(notices.Select(RenderNotice)); - } - - [HttpPost] - public async Task CreateNoticeAsync(CreateNoticeRequest req) - { - Instant now = clock.GetCurrentInstant(); - if (req.StartTime < now) - { - throw new ApiError.BadRequest( - "Start time cannot be in the past", - "start_time", - req.StartTime - ); - } - - if (req.EndTime < now) - { - throw new ApiError.BadRequest( - "End time cannot be in the past", - "end_time", - req.EndTime - ); - } - - var notice = new Notice - { - Id = snowflakeGenerator.GenerateSnowflake(), - Message = req.Message, - StartTime = req.StartTime ?? clock.GetCurrentInstant(), - EndTime = req.EndTime, - Author = CurrentUser!, - }; - - db.Add(notice); - await db.SaveChangesAsync(); - - return Ok(RenderNotice(notice)); - } - - private NoticeResponse RenderNotice(Notice notice) => - new( - notice.Id, - notice.Message, - notice.StartTime, - notice.EndTime, - userRenderer.RenderPartialUser(notice.Author) - ); -} diff --git a/Foxnouns.Backend/Controllers/Moderation/ReportsController.cs b/Foxnouns.Backend/Controllers/Moderation/ReportsController.cs deleted file mode 100644 index c5472b3..0000000 --- a/Foxnouns.Backend/Controllers/Moderation/ReportsController.cs +++ /dev/null @@ -1,277 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Net; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -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.Moderation; - -[Route("/api/v2/moderation")] -public class ReportsController( - ILogger logger, - DatabaseContext db, - IClock clock, - ISnowflakeGenerator snowflakeGenerator, - UserRendererService userRenderer, - MemberRendererService memberRenderer, - ModerationRendererService moderationRenderer, - ModerationService moderationService -) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - private Snowflake MaxReportId() => - Snowflake.FromInstant(clock.GetCurrentInstant() - Duration.FromHours(12)); - - [HttpPost("report-user/{id}")] - [Authorize("user.moderation")] - public async Task ReportUserAsync( - Snowflake id, - [FromBody] CreateReportRequest req - ) - { - ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]); - - User target = await db.ResolveUserAsync(id); - - if (target.Id == CurrentUser!.Id) - { - throw new ApiError( - "You can't report yourself.", - HttpStatusCode.BadRequest, - ErrorCode.InvalidReportTarget - ); - } - - Snowflake reportCutoff = MaxReportId(); - if ( - await db - .Reports.Where(r => - r.ReporterId == CurrentUser!.Id - && r.TargetUserId == target.Id - && r.Id > reportCutoff - ) - .AnyAsync() - ) - { - _logger.Debug( - "User {ReporterId} has already reported {TargetId} in the last 12 hours, ignoring report", - CurrentUser!.Id, - target.Id - ); - return NoContent(); - } - - _logger.Information( - "Creating report on {TargetId} by {ReporterId}", - target.Id, - CurrentUser!.Id - ); - - string snapshot = JsonConvert.SerializeObject( - await userRenderer.RenderUserAsync(target, renderMembers: false) - ); - - var report = new Report - { - Id = snowflakeGenerator.GenerateSnowflake(), - ReporterId = CurrentUser.Id, - TargetUserId = target.Id, - TargetMemberId = null, - Reason = req.Reason, - Context = req.Context, - TargetType = ReportTargetType.User, - TargetSnapshot = snapshot, - }; - - db.Reports.Add(report); - await db.SaveChangesAsync(); - return NoContent(); - } - - [HttpPost("report-member/{id}")] - [Authorize("user.moderation")] - public async Task ReportMemberAsync( - Snowflake id, - [FromBody] CreateReportRequest req - ) - { - ValidationUtils.Validate([("context", ValidationUtils.ValidateReportContext(req.Context))]); - - Member target = await db.ResolveMemberAsync(id); - - if (target.User.Id == CurrentUser!.Id) - { - throw new ApiError( - "You can't report yourself.", - HttpStatusCode.BadRequest, - ErrorCode.InvalidReportTarget - ); - } - - Snowflake reportCutoff = MaxReportId(); - if ( - await db - .Reports.Where(r => - r.ReporterId == CurrentUser!.Id - && r.TargetUserId == target.User.Id - && r.Id > reportCutoff - ) - .AnyAsync() - ) - { - _logger.Debug( - "User {ReporterId} has already reported {TargetId} in the last 12 hours, ignoring report", - CurrentUser!.Id, - target.User.Id - ); - return NoContent(); - } - - _logger.Information( - "Creating report on {TargetId} (member {TargetMemberId}) by {ReporterId}", - target.User.Id, - target.Id, - CurrentUser!.Id - ); - - string snapshot = JsonConvert.SerializeObject(memberRenderer.RenderMember(target)); - - var report = new Report - { - Id = snowflakeGenerator.GenerateSnowflake(), - ReporterId = CurrentUser.Id, - TargetUserId = target.User.Id, - TargetMemberId = target.Id, - Reason = req.Reason, - Context = req.Context, - TargetType = ReportTargetType.Member, - TargetSnapshot = snapshot, - }; - - db.Reports.Add(report); - await db.SaveChangesAsync(); - return NoContent(); - } - - [HttpGet("reports")] - [Authorize("user.moderation")] - [Limit(RequireModerator = true)] - public async Task GetReportsAsync( - [FromQuery] int? limit = null, - [FromQuery] Snowflake? before = null, - [FromQuery] Snowflake? after = null, - [FromQuery(Name = "by-reporter")] Snowflake? byReporter = null, - [FromQuery(Name = "by-target")] Snowflake? byTarget = null, - [FromQuery(Name = "include-closed")] bool includeClosed = false - ) - { - limit = limit switch - { - > 100 => 100, - < 0 => 100, - null => 100, - _ => limit, - }; - - IQueryable query = db - .Reports.Include(r => r.Reporter) - .Include(r => r.TargetUser) - .Include(r => r.TargetMember); - - if (byTarget != null && await db.Users.AnyAsync(u => u.Id == byTarget.Value)) - query = query.Where(r => r.TargetUserId == byTarget.Value); - - if (byReporter != null && await db.Users.AnyAsync(u => u.Id == byReporter.Value)) - query = query.Where(r => r.ReporterId == byReporter.Value); - - if (before != null) - query = query.Where(r => r.Id < before.Value).OrderByDescending(r => r.Id); - else if (after != null) - query = query.Where(r => r.Id > after.Value).OrderBy(r => r.Id); - else - query = query.OrderByDescending(r => r.Id); - - if (!includeClosed) - query = query.Where(r => r.Status == ReportStatus.Open); - - List reports = await query.Take(limit!.Value).ToListAsync(); - - return Ok(reports.Select(moderationRenderer.RenderReport)); - } - - [HttpGet("reports/{id}")] - [Authorize("user.moderation")] - [Limit(RequireModerator = true)] - public async Task GetReportAsync(Snowflake id, CancellationToken ct = default) - { - Report? report = await db - .Reports.Include(r => r.Reporter) - .Include(r => r.TargetUser) - .Include(r => r.TargetMember) - .Include(r => r.AuditLogEntry) - .FirstOrDefaultAsync(r => r.Id == id, ct); - if (report == null) - throw new ApiError.NotFound("No report with that ID found."); - - return Ok( - new ReportDetailResponse( - Report: moderationRenderer.RenderReport(report), - User: await userRenderer.RenderUserAsync( - report.TargetUser, - renderMembers: false, - ct: ct - ), - Member: report.TargetMember != null - ? memberRenderer.RenderMember(report.TargetMember) - : null, - AuditLogEntry: report.AuditLogEntry != null - ? moderationRenderer.RenderAuditLogEntry(report.AuditLogEntry) - : null - ) - ); - } - - [HttpPost("reports/{id}/ignore")] - [Authorize("user.moderation")] - [Limit(RequireModerator = true)] - public async Task IgnoreReportAsync( - Snowflake id, - [FromBody] IgnoreReportRequest req - ) - { - Report? report = await db.Reports.FindAsync(id); - if (report == null) - throw new ApiError.NotFound("No report with that ID found."); - if (report.Status != ReportStatus.Open) - throw new ApiError.BadRequest("That report has already been handled."); - - AuditLogEntry entry = await moderationService.IgnoreReportAsync( - CurrentUser!, - report, - req.Reason - ); - - return Ok(moderationRenderer.RenderAuditLogEntry(entry)); - } -} diff --git a/Foxnouns.Backend/Controllers/NotificationsController.cs b/Foxnouns.Backend/Controllers/NotificationsController.cs deleted file mode 100644 index 873344c..0000000 --- a/Foxnouns.Backend/Controllers/NotificationsController.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Middleware; -using Foxnouns.Backend.Services; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Controllers; - -[Route("/api/v2/notifications")] -public class NotificationsController( - DatabaseContext db, - ModerationRendererService moderationRenderer, - IClock clock -) : ApiControllerBase -{ - [HttpGet] - [Authorize("user.moderation")] - [Limit(UsableByDeletedUsers = true)] - public async Task GetNotificationsAsync([FromQuery] bool all = false) - { - IQueryable query = db.Notifications.Where(n => n.TargetId == CurrentUser!.Id); - if (!all) - query = query.Where(n => n.AcknowledgedAt == null); - - List notifications = await query.OrderByDescending(n => n.Id).ToListAsync(); - - return Ok(notifications.Select(moderationRenderer.RenderNotification)); - } - - [HttpPut("{id}/ack")] - [Authorize("user.moderation")] - [Limit(UsableByDeletedUsers = true)] - public async Task AcknowledgeNotificationAsync(Snowflake id) - { - Notification? notification = await db.Notifications.FirstOrDefaultAsync(n => - n.TargetId == CurrentUser!.Id && n.Id == id - ); - if (notification == null) - throw new ApiError.NotFound("Notification not found."); - - if (notification.AcknowledgedAt != null) - return Ok(moderationRenderer.RenderNotification(notification)); - - notification.AcknowledgedAt = clock.GetCurrentInstant(); - db.Update(notification); - await db.SaveChangesAsync(); - - return Ok(moderationRenderer.RenderNotification(notification)); - } -} diff --git a/Foxnouns.Backend/Controllers/SidController.cs b/Foxnouns.Backend/Controllers/SidController.cs deleted file mode 100644 index 3a8db19..0000000 --- a/Foxnouns.Backend/Controllers/SidController.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Diagnostics.CodeAnalysis; -using Foxnouns.Backend.Database; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Controllers; - -[Route("/sid")] -[SuppressMessage( - "Performance", - "CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons", - Justification = "Not usable with EFCore" -)] -[ApiExplorerSettings(IgnoreApi = true)] -public class SidController(Config config, DatabaseContext db) : ApiControllerBase -{ - [HttpGet("{**id}")] - public async Task ResolveSidAsync(string id, CancellationToken ct = default) => - id.Length switch - { - 5 => await ResolveUserSidAsync(id, ct), - 6 => await ResolveMemberSidAsync(id, ct), - _ => Redirect(config.BaseUrl), - }; - - private async Task ResolveUserSidAsync(string id, CancellationToken ct = default) - { - string? username = await db - .Users.Where(u => u.Sid == id.ToLowerInvariant() && !u.Deleted) - .Select(u => u.Username) - .FirstOrDefaultAsync(ct); - if (username == null) - return Redirect(config.BaseUrl); - - return Redirect($"{config.BaseUrl}/@{username}"); - } - - private async Task ResolveMemberSidAsync( - string id, - CancellationToken ct = default - ) - { - var member = await db - .Members.Include(m => m.User) - .Where(m => m.Sid == id.ToLowerInvariant() && !m.User.Deleted) - .Select(m => new { m.Name, m.User.Username }) - .FirstOrDefaultAsync(ct); - if (member == null) - return Redirect(config.BaseUrl); - - return Redirect($"{config.BaseUrl}/@{member.Username}/{member.Name}"); - } -} diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index ed9a48f..26ae497 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,329 +1,34 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using EntityFramework.Exceptions.Common; +using System.Diagnostics; 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; -using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage; -using NodaTime; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users")] -public class UsersController( - DatabaseContext db, - ILogger logger, - UserRendererService userRenderer, - ISnowflakeGenerator snowflakeGenerator, - IClock clock, - ValidationService validationService -) : ApiControllerBase +public class UsersController(DatabaseContext db, UserRendererService userRendererService) : ApiControllerBase { - private readonly ILogger _logger = logger.ForContext(); - [HttpGet("{userRef}")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - [Limit(UsableByDeletedUsers = true)] - public async Task GetUserAsync(string userRef, CancellationToken ct = default) + public async Task GetUser(string userRef) { - User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); - return Ok( - await userRenderer.RenderUserAsync( - user, - CurrentUser, - CurrentToken, - renderMembers: true, - renderAuthMethods: true, - renderSettings: true, - ct: ct - ) - ); + var user = await db.ResolveUserAsync(userRef); + return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); + } + + [HttpGet("@me")] + [Authorize("identify")] + public async Task GetMe() + { + var user = await db.ResolveUserAsync(CurrentUser!.Id); + return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); } [HttpPatch("@me")] - [Authorize("user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task UpdateUserAsync( - [FromBody] UpdateUserRequest req, - CancellationToken ct = default - ) + public Task UpdateUser([FromBody] UpdateUserRequest req) { - await using IDbContextTransaction tx = await db.Database.BeginTransactionAsync(ct); - User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); - var errors = new List<(string, ValidationError?)>(); - - if (req.Username != null && req.Username != user.Username) - { - errors.Add(("username", validationService.ValidateUsername(req.Username))); - user.Username = req.Username; - } - - if (req.HasProperty(nameof(req.DisplayName))) - { - errors.Add(("display_name", validationService.ValidateDisplayName(req.DisplayName))); - user.DisplayName = req.DisplayName; - } - - if (req.HasProperty(nameof(req.Bio))) - { - errors.Add(("bio", validationService.ValidateBio(req.Bio))); - user.Bio = req.Bio; - } - - if (req.HasProperty(nameof(req.Links))) - { - errors.AddRange(validationService.ValidateLinks(req.Links)); - user.Links = req.Links ?? []; - } - - if (req.Names != null) - { - errors.AddRange( - validationService.ValidateFieldEntries( - req.Names, - CurrentUser!.CustomPreferences, - "names" - ) - ); - user.Names = req.Names.ToList(); - } - - if (req.Pronouns != null) - { - errors.AddRange( - validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences) - ); - user.Pronouns = req.Pronouns.ToList(); - } - - if (req.Fields != null) - { - errors.AddRange( - validationService.ValidateFields( - req.Fields.ToList(), - CurrentUser!.CustomPreferences - ) - ); - user.Fields = req.Fields.ToList(); - } - - if (req.Flags != null) - { - ValidationError? flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags); - if (flagError != null) - errors.Add(("flags", flagError)); - } - - if (req.HasProperty(nameof(req.Avatar))) - errors.Add(("avatar", validationService.ValidateAvatar(req.Avatar))); - - if (req.HasProperty(nameof(req.MemberTitle))) - { - if (string.IsNullOrEmpty(req.MemberTitle)) - { - user.MemberTitle = null; - } - else - { - errors.Add( - ("member_title", validationService.ValidateDisplayName(req.MemberTitle)) - ); - user.MemberTitle = req.MemberTitle; - } - } - - if (req.HasProperty(nameof(req.MemberListHidden))) - user.ListHidden = req.MemberListHidden == true; - - if (req.HasProperty(nameof(req.Timezone))) - { - if (string.IsNullOrEmpty(req.Timezone)) - { - user.Timezone = null; - } - else - { - if (TimeZoneInfo.TryFindSystemTimeZoneById(req.Timezone, out _)) - { - user.Timezone = req.Timezone; - } - else - { - errors.Add( - ( - "timezone", - ValidationError.GenericValidationError("Invalid timezone", req.Timezone) - ) - ); - } - } - } - - ValidationUtils.Validate(errors); - // This is fired off regardless of whether the transaction is committed - // (atomic operations are hard when combined with background jobs) - // so it's in a separate block to the validation above. - if (req.HasProperty(nameof(req.Avatar))) - { - UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); - } - - user.LastActive = clock.GetCurrentInstant(); - - try - { - await db.SaveChangesAsync(ct); - } - catch (UniqueConstraintException) - { - _logger.Debug( - "Could not update user {Id} due to name conflict ({CurrentName} / {NewName})", - user.Id, - user.Username, - req.Username - ); - throw new ApiError.BadRequest( - "That username is already taken.", - "username", - req.Username - ); - } - - await tx.CommitAsync(ct); - return Ok( - await userRenderer.RenderUserAsync( - user, - CurrentUser, - renderMembers: false, - renderAuthMethods: false, - ct: ct - ) - ); + throw new NotImplementedException(); } - [HttpPatch("@me/custom-preferences")] - [Authorize("user.update")] - [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task UpdateCustomPreferencesAsync( - [FromBody] List req, - CancellationToken ct = default - ) - { - ValidationUtils.Validate(ValidationUtils.ValidateCustomPreferences(req)); - - User user = await db.ResolveUserAsync(CurrentUser!.Id, ct); - var preferences = user - .CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)) - .ToDictionary(); - - foreach (CustomPreferenceUpdateRequest r in req) - { - if (r.Id != null && preferences.ContainsKey(r.Id.Value)) - { - preferences[r.Id.Value] = new User.CustomPreference - { - Favourite = r.Favourite, - Icon = r.Icon, - Muted = r.Muted, - Size = r.Size, - Tooltip = r.Tooltip, - LegacyId = preferences[r.Id.Value].LegacyId, - }; - } - else - { - preferences[snowflakeGenerator.GenerateSnowflake()] = new User.CustomPreference - { - Favourite = r.Favourite, - Icon = r.Icon, - Muted = r.Muted, - Size = r.Size, - Tooltip = r.Tooltip, - LegacyId = Guid.NewGuid(), - }; - } - } - - user.CustomPreferences = preferences; - user.LastActive = clock.GetCurrentInstant(); - await db.SaveChangesAsync(ct); - - return Ok(user.CustomPreferences); - } - - [HttpPatch("@me/settings")] - [Authorize("user.read_hidden", "user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task UpdateUserSettingsAsync( - [FromBody] UpdateUserSettingsRequest req, - CancellationToken ct = default - ) - { - User user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); - - if (req.HasProperty(nameof(req.DarkMode))) - user.Settings.DarkMode = req.DarkMode; - if (req.HasProperty(nameof(req.LastReadNotice))) - user.Settings.LastReadNotice = req.LastReadNotice; - - user.LastActive = clock.GetCurrentInstant(); - db.Update(user); - await db.SaveChangesAsync(ct); - - return Ok(user.Settings); - } - - [HttpPost("@me/reroll-sid")] - [Authorize("user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task RerollSidAsync() - { - Instant minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); - if (CurrentUser!.LastSidReroll > minTimeAgo) - throw new ApiError.BadRequest("Cannot reroll short ID yet"); - - // Using ExecuteUpdateAsync here as the new short ID is generated by the database - await db - .Users.Where(u => u.Id == CurrentUser.Id) - .ExecuteUpdateAsync(s => - s.SetProperty(u => u.Sid, _ => db.FindFreeUserSid()) - .SetProperty(u => u.LastSidReroll, clock.GetCurrentInstant()) - .SetProperty(u => u.LastActive, clock.GetCurrentInstant()) - ); - - // Get the user's new sid - string newSid = await db - .Users.Where(u => u.Id == CurrentUser.Id) - .Select(u => u.Sid) - .FirstAsync(); - - User user = await db.ResolveUserAsync(CurrentUser.Id); - return Ok( - await userRenderer.RenderUserAsync( - user, - CurrentUser, - CurrentToken, - false, - overrideSid: newSid - ) - ); - } -} + public record UpdateUserRequest(string? Username, string? DisplayName); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/V1/V1ReadController.cs b/Foxnouns.Backend/Controllers/V1/V1ReadController.cs deleted file mode 100644 index 327f03e..0000000 --- a/Foxnouns.Backend/Controllers/V1/V1ReadController.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto.V1; -using Foxnouns.Backend.Middleware; -using Foxnouns.Backend.Services.V1; -using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Controllers.V1; - -[Route("/api/v1")] -public class V1ReadController( - UsersV1Service usersV1Service, - MembersV1Service membersV1Service, - DatabaseContext db -) : ApiControllerBase -{ - [HttpGet("users/@me")] - [Authorize("identify")] - public async Task GetMeAsync(CancellationToken ct = default) - { - User user = await usersV1Service.ResolveUserAsync("@me", CurrentToken, ct); - return Ok(await usersV1Service.RenderCurrentUserAsync(user, ct)); - } - - [HttpGet("users/{userRef}")] - public async Task GetUserAsync(string userRef, CancellationToken ct = default) - { - User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); - return Ok( - await usersV1Service.RenderUserAsync( - user, - CurrentToken, - renderMembers: true, - renderFlags: true, - ct: ct - ) - ); - } - - [HttpGet("members/{id}")] - public async Task GetMemberAsync(string id, CancellationToken ct = default) - { - Member member = await membersV1Service.ResolveMemberAsync(id, ct); - return Ok( - await membersV1Service.RenderMemberAsync( - member, - CurrentToken, - renderFlags: true, - ct: ct - ) - ); - } - - [HttpGet("users/{userRef}/members")] - public async Task GetUserMembersAsync( - string userRef, - CancellationToken ct = default - ) - { - User user = await usersV1Service.ResolveUserAsync(userRef, CurrentToken, ct); - List members = await db - .Members.Where(m => m.UserId == user.Id) - .OrderBy(m => m.Name) - .ToListAsync(ct); - - List responses = []; - foreach (Member member in members) - { - responses.Add( - await membersV1Service.RenderMemberAsync( - member, - CurrentToken, - user, - renderFlags: true, - ct: ct - ) - ); - } - - return Ok(responses); - } - - [HttpGet("users/{userRef}/members/{memberRef}")] - public async Task GetUserMemberAsync( - string userRef, - string memberRef, - CancellationToken ct = default - ) - { - Member member = await membersV1Service.ResolveMemberAsync( - userRef, - memberRef, - CurrentToken, - ct - ); - return Ok( - await membersV1Service.RenderMemberAsync( - member, - CurrentToken, - renderFlags: true, - ct: ct - ) - ); - } -} diff --git a/Foxnouns.Backend/Database/BaseModel.cs b/Foxnouns.Backend/Database/BaseModel.cs index a6ea612..d0dcbca 100644 --- a/Foxnouns.Backend/Database/BaseModel.cs +++ b/Foxnouns.Backend/Database/BaseModel.cs @@ -1,20 +1,6 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . namespace Foxnouns.Backend.Database; public abstract class BaseModel { public required Snowflake Id { get; init; } = SnowflakeGenerator.Instance.GenerateSnowflake(); -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 2bbcbc7..f9ec686 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -1,19 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Diagnostics.CodeAnalysis; -using EntityFramework.Exceptions.PostgreSQL; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Microsoft.EntityFrameworkCore; @@ -23,57 +7,35 @@ using Npgsql; namespace Foxnouns.Backend.Database; -public class DatabaseContext(DbContextOptions options) : DbContext(options) +public class DatabaseContext : DbContext { - private static string GenerateConnectionString(Config.DatabaseConfig config) => - new NpgsqlConnectionStringBuilder(config.Url) + private readonly NpgsqlDataSource _dataSource; + + public DbSet Users { get; set; } + public DbSet Members { get; set; } + public DbSet AuthMethods { get; set; } + public DbSet FediverseApplications { get; set; } + public DbSet Tokens { get; set; } + public DbSet Applications { get; set; } + + public DatabaseContext(Config config) + { + var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { - Pooling = config.EnablePooling ?? true, - Timeout = config.Timeout ?? 5, - MaxPoolSize = config.MaxPoolSize ?? 50, - MinPoolSize = 0, - ConnectionPruningInterval = 10, - ConnectionIdleLifetime = 10, + Timeout = config.Database.Timeout ?? 5, + MaxPoolSize = config.Database.MaxPoolSize ?? 50, }.ConnectionString; - public static NpgsqlDataSource BuildDataSource(Config config) - { - var dataSourceBuilder = new NpgsqlDataSourceBuilder( - GenerateConnectionString(config.Database) - ); + var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); dataSourceBuilder.UseNodaTime(); - dataSourceBuilder.UseJsonNet(); - return dataSourceBuilder.Build(); + _dataSource = dataSourceBuilder.Build(); } - public static DbContextOptionsBuilder BuildOptions( - DbContextOptionsBuilder options, - NpgsqlDataSource dataSource, - ILoggerFactory? loggerFactory - ) => - options - .ConfigureWarnings(c => c.Ignore(CoreEventId.SaveChangesFailed)) - .UseNpgsql(dataSource, o => o.UseNodaTime()) - .UseLoggerFactory(loggerFactory) - .UseSnakeCaseNamingConvention() - .UseExceptionProcessor(); - - public DbSet Users { get; init; } = null!; - public DbSet Members { get; init; } = null!; - public DbSet AuthMethods { get; init; } = null!; - public DbSet FediverseApplications { get; init; } = null!; - public DbSet Tokens { get; init; } = null!; - public DbSet Applications { get; init; } = null!; - public DbSet DataExports { get; init; } = null!; - - public DbSet PrideFlags { get; init; } = null!; - public DbSet UserFlags { get; init; } = null!; - public DbSet MemberFlags { get; init; } = null!; - - public DbSet Reports { get; init; } = null!; - public DbSet AuditLog { get; init; } = null!; - public DbSet Notifications { get; init; } = null!; - public DbSet Notices { get; init; } = null!; + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + => optionsBuilder + .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) + .UseNpgsql(_dataSource, o => o.UseNodaTime()) + .UseSnakeCaseNamingConvention(); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { @@ -84,116 +46,31 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity().HasIndex(u => u.Username).IsUnique(); - modelBuilder.Entity().HasIndex(u => u.Sid).IsUnique(); modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); - modelBuilder.Entity().HasIndex(m => m.Sid).IsUnique(); - modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); - // Two indexes on auth_methods, one for fediverse auth and one for all other types. - modelBuilder - .Entity() - .HasIndex(m => new - { - m.AuthType, - m.RemoteId, - m.FediverseApplicationId, - }) - .HasFilter("fediverse_application_id IS NOT NULL") - .IsUnique(); + modelBuilder.Entity() + .OwnsOne(u => u.Fields, f => f.ToJson()) + .OwnsOne(u => u.Names, n => n.ToJson()) + .OwnsOne(u => u.Pronouns, p => p.ToJson()); - modelBuilder - .Entity() - .HasIndex(m => new { m.AuthType, m.RemoteId }) - .HasFilter("fediverse_application_id IS NULL") - .IsUnique(); - - modelBuilder - .Entity() - .HasOne(e => e.Report) - .WithOne(e => e.AuditLogEntry) - .OnDelete(DeleteBehavior.SetNull); - - modelBuilder.Entity().Property(u => u.Sid).HasDefaultValueSql("find_free_user_sid()"); - modelBuilder.Entity().Property(u => u.Fields).HasColumnType("jsonb"); - modelBuilder.Entity().Property(u => u.Names).HasColumnType("jsonb"); - modelBuilder.Entity().Property(u => u.Pronouns).HasColumnType("jsonb"); - modelBuilder.Entity().Property(u => u.CustomPreferences).HasColumnType("jsonb"); - modelBuilder.Entity().Property(u => u.Settings).HasColumnType("jsonb"); - - modelBuilder - .Entity() - .Property(m => m.Sid) - .HasDefaultValueSql("find_free_member_sid()"); - modelBuilder.Entity().Property(m => m.Fields).HasColumnType("jsonb"); - modelBuilder.Entity().Property(m => m.Names).HasColumnType("jsonb"); - modelBuilder.Entity().Property(m => m.Pronouns).HasColumnType("jsonb"); - - modelBuilder.Entity().Navigation(f => f.PrideFlag).AutoInclude(); - modelBuilder.Entity().Navigation(f => f.PrideFlag).AutoInclude(); - - modelBuilder - .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeUserSid))!) - .HasName("find_free_user_sid"); - - modelBuilder - .HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!) - .HasName("find_free_member_sid"); - - // Indexes for legacy IDs for APIv1 - modelBuilder.Entity().HasIndex(u => u.LegacyId).IsUnique(); - modelBuilder.Entity().HasIndex(m => m.LegacyId).IsUnique(); - modelBuilder.Entity().HasIndex(f => f.LegacyId).IsUnique(); - - // a UUID is not an xid, but this should always be set by the application anyway. - // we're just setting it here to shut EFCore up because squashing migrations is for nerds - modelBuilder - .Entity() - .Property(u => u.LegacyId) - .HasDefaultValueSql("gen_random_uuid()"); - modelBuilder - .Entity() - .Property(m => m.LegacyId) - .HasDefaultValueSql("gen_random_uuid()"); - modelBuilder - .Entity() - .Property(f => f.LegacyId) - .HasDefaultValueSql("gen_random_uuid()"); + modelBuilder.Entity() + .OwnsOne(m => m.Fields, f => f.ToJson()) + .OwnsOne(m => m.Names, n => n.ToJson()) + .OwnsOne(m => m.Pronouns, p => p.ToJson()); } - - /// - /// Dummy method that calls find_free_user_sid() when used in an EF Core query. - /// - public string FindFreeUserSid() => throw new NotSupportedException(); - - /// - /// Dummy method that calls find_free_member_sid() when used in an EF Core query. - /// - public string FindFreeMemberSid() => throw new NotSupportedException(); } -[SuppressMessage( - "ReSharper", - "UnusedType.Global", - Justification = "Used by EF Core's migration generator" -)] public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory { public DatabaseContext CreateDbContext(string[] args) { // Read the configuration file - Config config = - new ConfigurationBuilder() - .AddConfiguration() - .Build() - // Get the configuration as our config class - .Get() ?? new Config(); + var config = new ConfigurationBuilder() + .AddConfiguration() + .Build() + // Get the configuration as our config class + .Get() ?? new(); - NpgsqlDataSource dataSource = DatabaseContext.BuildDataSource(config); - - DbContextOptions options = DatabaseContext - .BuildOptions(new DbContextOptionsBuilder(), dataSource, null) - .Options; - - return new DatabaseContext(options); + return new DatabaseContext(config); } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index d804dfe..40e333c 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -1,205 +1,87 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using System.Security.Cryptography; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; -using NodaTime; namespace Foxnouns.Backend.Database; public static class DatabaseQueryExtensions { - public static async Task ResolveUserAsync( - this DatabaseContext context, - string userRef, - Token? token, - CancellationToken ct = default - ) + public static async Task ResolveUserAsync(this DatabaseContext context, string userRef) { - if (userRef == "@me") - { - // Not filtering deleted users, as a suspended user should still be able to look at their own profile. - return token != null - ? await context.Users.FirstAsync(u => u.Id == token.UserId, ct) - : throw new ApiError.Unauthorized( - "This endpoint requires an authenticated user.", - ErrorCode.AuthenticationRequired - ); - } - User? user; - if (Snowflake.TryParse(userRef, out Snowflake? snowflake)) + if (Snowflake.TryParse(userRef, out var snowflake)) { - user = await context - .Users.Where(u => !u.Deleted || (token != null && token.UserId == u.Id)) - .FirstOrDefaultAsync(u => u.Id == snowflake, ct); - if (user != null) - return user; + user = await context.Users + .FirstOrDefaultAsync(u => u.Id == snowflake); + if (user != null) return user; } - user = await context - .Users.Where(u => !u.Deleted || (token != null && token.UserId == u.Id)) - .FirstOrDefaultAsync(u => u.Username == userRef, ct); - if (user != null) - return user; - throw new ApiError.NotFound( - "No user with that ID or username found.", - ErrorCode.UserNotFound - ); + user = await context.Users + .FirstOrDefaultAsync(u => u.Username == userRef); + if (user != null) return user; + throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound); } - public static async Task ResolveUserAsync( - this DatabaseContext context, - Snowflake id, - CancellationToken ct = default - ) + public static async Task ResolveUserAsync(this DatabaseContext context, Snowflake id) { - User? user = await context - .Users.Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Id == id, ct); - if (user != null) - return user; - throw new ApiError.NotFound("No user with that ID found.", ErrorCode.UserNotFound); + var user = await context.Users + .FirstOrDefaultAsync(u => u.Id == id); + if (user != null) return user; + throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound); } - public static async Task ResolveMemberAsync( - this DatabaseContext context, - Snowflake id, - CancellationToken ct = default - ) + public static async Task ResolveMemberAsync(this DatabaseContext context, Snowflake id) { - Member? member = await context - .Members.Include(m => m.User) - .Where(m => !m.User.Deleted) - .FirstOrDefaultAsync(m => m.Id == id, ct); - if (member != null) - return member; - throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound); + var member = await context.Members + .Include(m => m.User) + .FirstOrDefaultAsync(m => m.Id == id); + if (member != null) return member; + throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound); } - public static async Task ResolveMemberAsync( - this DatabaseContext context, - string userRef, - string memberRef, - Token? token, - CancellationToken ct = default - ) + public static async Task ResolveMemberAsync(this DatabaseContext context, string userRef, string memberRef) { - User user = await context.ResolveUserAsync(userRef, token, ct); - return await context.ResolveMemberAsync(user.Id, memberRef, token, ct); + var user = await context.ResolveUserAsync(userRef); + return await context.ResolveMemberAsync(user.Id, memberRef); } - public static async Task ResolveMemberAsync( - this DatabaseContext context, - Snowflake userId, - string memberRef, - Token? token = null, - CancellationToken ct = default - ) + public static async Task ResolveMemberAsync(this DatabaseContext context, Snowflake userId, + string memberRef) { Member? member; - if (Snowflake.TryParse(memberRef, out Snowflake? snowflake)) + if (Snowflake.TryParse(memberRef, out var snowflake)) { - member = await context - .Members.Include(m => m.User) - .Include(m => m.ProfileFlags) - // Return members if their user isn't deleted or the user querying it is the member's owner - .Where(m => !m.User.Deleted || (token != null && token.UserId == m.UserId)) - .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId, ct); - if (member != null) - return member; + member = await context.Members + .Include(m => m.User) + .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId); + if (member != null) return member; } - member = await context - .Members.Include(m => m.User) - .Include(m => m.ProfileFlags) - // Return members if their user isn't deleted or the user querying it is the member's owner - .Where(m => !m.User.Deleted || (token != null && token.UserId == m.UserId)) - .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId, ct); - if (member != null) - return member; - throw new ApiError.NotFound( - "No member with that ID or name found.", - ErrorCode.MemberNotFound - ); + member = await context.Members + .Include(m => m.User) + .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId); + if (member != null) return member; + throw new ApiError.NotFound("No member with that ID or name found.", code: ErrorCode.MemberNotFound); } - public static async Task GetFrontendApplicationAsync( - this DatabaseContext context, - CancellationToken ct = default - ) + public static async Task GetFrontendApplicationAsync(this DatabaseContext context) { - Application? app = await context.Applications.FirstOrDefaultAsync( - a => a.Id == new Snowflake(0), - ct - ); - if (app != null) - return app; + var app = await context.Applications.FirstOrDefaultAsync(a => a.Id == new Snowflake(0)); + if (app != null) return app; app = new Application { Id = new Snowflake(0), ClientId = RandomNumberGenerator.GetHexString(32, true), - ClientSecret = AuthUtils.RandomToken(), + ClientSecret = OauthUtils.RandomToken(48), Name = "pronouns.cc", Scopes = ["*"], RedirectUris = [], }; context.Add(app); - await context.SaveChangesAsync(ct); + await context.SaveChangesAsync(); return app; } - - public static async Task GetToken( - this DatabaseContext context, - byte[] rawToken, - CancellationToken ct = default - ) - { - byte[] hash = SHA512.HashData(rawToken); - - Token? oauthToken = await context - .Tokens.Include(t => t.Application) - .Include(t => t.User) - .FirstOrDefaultAsync( - t => - t.Hash == hash - && t.ExpiresAt > SystemClock.Instance.GetCurrentInstant() - && !t.ManuallyExpired, - ct - ); - - return oauthToken; - } - - public static async Task GetTokenUserId( - this DatabaseContext context, - byte[] rawToken, - CancellationToken ct = default - ) - { - byte[] hash = SHA512.HashData(rawToken); - return await context - .Tokens.Where(t => - t.Hash == hash - && t.ExpiresAt > SystemClock.Instance.GetCurrentInstant() - && !t.ManuallyExpired - ) - .Select(t => t.UserId) - .FirstOrDefaultAsync(ct); - } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs b/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs deleted file mode 100644 index 282d454..0000000 --- a/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Npgsql; -using Serilog; - -namespace Foxnouns.Backend.Database; - -public static class DatabaseServiceExtensions -{ - public static IServiceCollection AddFoxnounsDatabase( - this IServiceCollection serviceCollection, - Config config - ) - { - NpgsqlDataSource dataSource = DatabaseContext.BuildDataSource(config); - ILoggerFactory loggerFactory = new LoggerFactory().AddSerilog(dispose: false); - - serviceCollection.AddDbContext(options => - DatabaseContext.BuildOptions(options, dataSource, loggerFactory) - ); - - return serviceCollection; - } -} diff --git a/Foxnouns.Backend/Database/FlagQueryExtensions.cs b/Foxnouns.Backend/Database/FlagQueryExtensions.cs deleted file mode 100644 index 953c94a..0000000 --- a/Foxnouns.Backend/Database/FlagQueryExtensions.cs +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Database; - -public static class FlagQueryExtensions -{ - private static async Task> GetFlagsAsync( - this DatabaseContext db, - Snowflake userId - ) => await db.PrideFlags.Where(f => f.UserId == userId).OrderBy(f => f.Id).ToListAsync(); - - /// - /// Sets the user's profile flags to the given IDs. Returns a validation error if any of the flag IDs are unknown - /// or if too many IDs are given. Duplicates are allowed. - /// - public static async Task SetUserFlagsAsync( - this DatabaseContext db, - Snowflake userId, - Snowflake[] flagIds - ) - { - List currentFlags = await db - .UserFlags.Where(f => f.UserId == userId) - .ToListAsync(); - foreach (UserFlag flag in currentFlags) - db.UserFlags.Remove(flag); - - // If there's no new flags to set, we're done - if (flagIds.Length == 0) - return null; - if (flagIds.Length > 100) - return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length); - - List flags = await db.GetFlagsAsync(userId); - Snowflake[] unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); - if (unknownFlagIds.Length != 0) - return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds); - - IEnumerable userFlags = flagIds.Select(id => new UserFlag - { - PrideFlagId = id, - UserId = userId, - }); - db.UserFlags.AddRange(userFlags); - - return null; - } - - public static async Task SetMemberFlagsAsync( - this DatabaseContext db, - Snowflake userId, - Snowflake memberId, - Snowflake[] flagIds - ) - { - List currentFlags = await db - .MemberFlags.Where(f => f.MemberId == memberId) - .ToListAsync(); - foreach (MemberFlag flag in currentFlags) - db.MemberFlags.Remove(flag); - - if (flagIds.Length == 0) - return null; - if (flagIds.Length > 100) - return ValidationError.LengthError("Too many profile flags", 0, 100, flagIds.Length); - - List flags = await db.GetFlagsAsync(userId); - Snowflake[] unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); - if (unknownFlagIds.Length != 0) - return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds); - - IEnumerable memberFlags = flagIds.Select(id => new MemberFlag - { - PrideFlagId = id, - MemberId = memberId, - }); - db.MemberFlags.AddRange(memberFlags); - - return null; - } -} diff --git a/Foxnouns.Backend/Database/ISnowflakeGenerator.cs b/Foxnouns.Backend/Database/ISnowflakeGenerator.cs index 62b0dda..dd76ab3 100644 --- a/Foxnouns.Backend/Database/ISnowflakeGenerator.cs +++ b/Foxnouns.Backend/Database/ISnowflakeGenerator.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using NodaTime; namespace Foxnouns.Backend.Database; @@ -19,4 +5,4 @@ namespace Foxnouns.Backend.Database; public interface ISnowflakeGenerator { Snowflake GenerateSnowflake(Instant? time = null); -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs new file mode 100644 index 0000000..5274ef0 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs @@ -0,0 +1,412 @@ +// +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240527132444_Init")] + partial class Init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthType") + .HasColumnType("integer") + .HasColumnName("auth_type"); + + b.Property("FediverseApplicationId") + .HasColumnType("bigint") + .HasColumnName("fediverse_application_id"); + + b.Property("RemoteId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("remote_id"); + + b.Property("RemoteUsername") + .HasColumnType("text") + .HasColumnName("remote_username"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_methods"); + + b.HasIndex("FediverseApplicationId") + .HasDatabaseName("ix_auth_methods_fediverse_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_methods_user_id"); + + b.ToTable("auth_methods", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("InstanceType") + .HasColumnType("integer") + .HasColumnName("instance_type"); + + b.HasKey("Id") + .HasName("pk_fediverse_applications"); + + b.ToTable("fediverse_applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_members"); + + b.HasIndex("UserId", "Name") + .IsUnique() + .HasDatabaseName("ix_members_user_id_name"); + + b.ToTable("members", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("ManuallyExpired") + .HasColumnType("boolean") + .HasColumnName("manually_expired"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tokens"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("MemberTitle") + .HasColumnType("text") + .HasColumnName("member_title"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") + .WithMany() + .HasForeignKey("FediverseApplicationId") + .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("AuthMethods") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_methods_users_user_id"); + + b.Navigation("FediverseApplication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("Members") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_members_users_user_id"); + + b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Names", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs index 7d2f05c..4a876cf 100644 --- a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs +++ b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; using NodaTime; #nullable disable @@ -7,8 +6,6 @@ using NodaTime; namespace Foxnouns.Backend.Database.Migrations { /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240527132444_Init")] public partial class Init : Migration { /// @@ -22,10 +19,12 @@ namespace Foxnouns.Backend.Database.Migrations domain = table.Column(type: "text", nullable: false), client_id = table.Column(type: "text", nullable: false), client_secret = table.Column(type: "text", nullable: false), - instance_type = table.Column(type: "integer", nullable: false), + instance_type = table.Column(type: "integer", nullable: false) }, - constraints: table => table.PrimaryKey("pk_fediverse_applications", x => x.id) - ); + constraints: table => + { + table.PrimaryKey("pk_fediverse_applications", x => x.id); + }); migrationBuilder.CreateTable( name: "users", @@ -41,10 +40,12 @@ namespace Foxnouns.Backend.Database.Migrations role = table.Column(type: "integer", nullable: false), fields = table.Column(type: "jsonb", nullable: false), names = table.Column(type: "jsonb", nullable: false), - pronouns = table.Column(type: "jsonb", nullable: false), + pronouns = table.Column(type: "jsonb", nullable: false) }, - constraints: table => table.PrimaryKey("pk_users", x => x.id) - ); + constraints: table => + { + table.PrimaryKey("pk_users", x => x.id); + }); migrationBuilder.CreateTable( name: "auth_methods", @@ -55,7 +56,7 @@ namespace Foxnouns.Backend.Database.Migrations remote_id = table.Column(type: "text", nullable: false), remote_username = table.Column(type: "text", nullable: true), user_id = table.Column(type: "bigint", nullable: false), - fediverse_application_id = table.Column(type: "bigint", nullable: true), + fediverse_application_id = table.Column(type: "bigint", nullable: true) }, constraints: table => { @@ -64,17 +65,14 @@ namespace Foxnouns.Backend.Database.Migrations name: "fk_auth_methods_fediverse_applications_fediverse_application_id", column: x => x.fediverse_application_id, principalTable: "fediverse_applications", - principalColumn: "id" - ); + principalColumn: "id"); table.ForeignKey( name: "fk_auth_methods_users_user_id", column: x => x.user_id, principalTable: "users", principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); + onDelete: ReferentialAction.Cascade); + }); migrationBuilder.CreateTable( name: "members", @@ -90,7 +88,7 @@ namespace Foxnouns.Backend.Database.Migrations user_id = table.Column(type: "bigint", nullable: false), fields = table.Column(type: "jsonb", nullable: false), names = table.Column(type: "jsonb", nullable: false), - pronouns = table.Column(type: "jsonb", nullable: false), + pronouns = table.Column(type: "jsonb", nullable: false) }, constraints: table => { @@ -100,23 +98,18 @@ namespace Foxnouns.Backend.Database.Migrations column: x => x.user_id, principalTable: "users", principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); + onDelete: ReferentialAction.Cascade); + }); migrationBuilder.CreateTable( name: "tokens", columns: table => new { id = table.Column(type: "bigint", nullable: false), - expires_at = table.Column( - type: "timestamp with time zone", - nullable: false - ), + expires_at = table.Column(type: "timestamp with time zone", nullable: false), scopes = table.Column(type: "text[]", nullable: false), manually_expired = table.Column(type: "boolean", nullable: false), - user_id = table.Column(type: "bigint", nullable: false), + user_id = table.Column(type: "bigint", nullable: false) }, constraints: table => { @@ -126,56 +119,53 @@ namespace Foxnouns.Backend.Database.Migrations column: x => x.user_id, principalTable: "users", principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); + onDelete: ReferentialAction.Cascade); + }); migrationBuilder.CreateIndex( name: "ix_auth_methods_fediverse_application_id", table: "auth_methods", - column: "fediverse_application_id" - ); + column: "fediverse_application_id"); migrationBuilder.CreateIndex( name: "ix_auth_methods_user_id", table: "auth_methods", - column: "user_id" - ); + column: "user_id"); // EF Core doesn't support creating indexes on arbitrary expressions, so we have to create it manually. // Due to historical reasons (I made a mistake while writing the initial migration for the Go version) // only members have case-insensitive names. - migrationBuilder.Sql( - "CREATE UNIQUE INDEX ix_members_user_id_name ON members (user_id, lower(name))" - ); + migrationBuilder.Sql("CREATE UNIQUE INDEX ix_members_user_id_name ON members (user_id, lower(name))"); migrationBuilder.CreateIndex( name: "ix_tokens_user_id", table: "tokens", - column: "user_id" - ); + column: "user_id"); migrationBuilder.CreateIndex( name: "ix_users_username", table: "users", column: "username", - unique: true - ); + unique: true); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable(name: "auth_methods"); + migrationBuilder.DropTable( + name: "auth_methods"); - migrationBuilder.DropTable(name: "members"); + migrationBuilder.DropTable( + name: "members"); - migrationBuilder.DropTable(name: "tokens"); + migrationBuilder.DropTable( + name: "tokens"); - migrationBuilder.DropTable(name: "fediverse_applications"); + migrationBuilder.DropTable( + name: "fediverse_applications"); - migrationBuilder.DropTable(name: "users"); + migrationBuilder.DropTable( + name: "users"); } } } diff --git a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.Designer.cs new file mode 100644 index 0000000..2b660a0 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.Designer.cs @@ -0,0 +1,470 @@ +// +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240528125310_AddApplications")] + partial class AddApplications + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RedirectUris") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("redirect_uris"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.ToTable("applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthType") + .HasColumnType("integer") + .HasColumnName("auth_type"); + + b.Property("FediverseApplicationId") + .HasColumnType("bigint") + .HasColumnName("fediverse_application_id"); + + b.Property("RemoteId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("remote_id"); + + b.Property("RemoteUsername") + .HasColumnType("text") + .HasColumnName("remote_username"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_methods"); + + b.HasIndex("FediverseApplicationId") + .HasDatabaseName("ix_auth_methods_fediverse_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_methods_user_id"); + + b.ToTable("auth_methods", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("InstanceType") + .HasColumnType("integer") + .HasColumnName("instance_type"); + + b.HasKey("Id") + .HasName("pk_fediverse_applications"); + + b.ToTable("fediverse_applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_members"); + + b.HasIndex("UserId", "Name") + .IsUnique() + .HasDatabaseName("ix_members_user_id_name"); + + b.ToTable("members", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("bigint") + .HasColumnName("application_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("ManuallyExpired") + .HasColumnType("boolean") + .HasColumnName("manually_expired"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tokens"); + + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_tokens_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("MemberTitle") + .HasColumnType("text") + .HasColumnName("member_title"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") + .WithMany() + .HasForeignKey("FediverseApplicationId") + .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("AuthMethods") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_methods_users_user_id"); + + b.Navigation("FediverseApplication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("Members") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_members_users_user_id"); + + b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Names", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_applications_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + + b.Navigation("Application"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs index 4fced78..d6694cd 100644 --- a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs +++ b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs @@ -1,13 +1,10 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Foxnouns.Backend.Database.Migrations { /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240528125310_AddApplications")] public partial class AddApplications : Migration { /// @@ -18,16 +15,14 @@ namespace Foxnouns.Backend.Database.Migrations table: "tokens", type: "bigint", nullable: false, - defaultValue: 0L - ); + defaultValue: 0L); migrationBuilder.AddColumn( name: "hash", table: "tokens", type: "bytea", nullable: false, - defaultValue: Array.Empty() - ); + defaultValue: new byte[0]); migrationBuilder.CreateTable( name: "applications", @@ -38,16 +33,17 @@ namespace Foxnouns.Backend.Database.Migrations client_secret = table.Column(type: "text", nullable: false), name = table.Column(type: "text", nullable: false), scopes = table.Column(type: "text[]", nullable: false), - redirect_uris = table.Column(type: "text[]", nullable: false), + redirect_uris = table.Column(type: "text[]", nullable: false) }, - constraints: table => table.PrimaryKey("pk_applications", x => x.id) - ); + constraints: table => + { + table.PrimaryKey("pk_applications", x => x.id); + }); migrationBuilder.CreateIndex( name: "ix_tokens_application_id", table: "tokens", - column: "application_id" - ); + column: "application_id"); migrationBuilder.AddForeignKey( name: "fk_tokens_applications_application_id", @@ -55,8 +51,7 @@ namespace Foxnouns.Backend.Database.Migrations column: "application_id", principalTable: "applications", principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); + onDelete: ReferentialAction.Cascade); } /// @@ -64,16 +59,22 @@ namespace Foxnouns.Backend.Database.Migrations { migrationBuilder.DropForeignKey( name: "fk_tokens_applications_application_id", - table: "tokens" - ); + table: "tokens"); - migrationBuilder.DropTable(name: "applications"); + migrationBuilder.DropTable( + name: "applications"); - migrationBuilder.DropIndex(name: "ix_tokens_application_id", table: "tokens"); + migrationBuilder.DropIndex( + name: "ix_tokens_application_id", + table: "tokens"); - migrationBuilder.DropColumn(name: "application_id", table: "tokens"); + migrationBuilder.DropColumn( + name: "application_id", + table: "tokens"); - migrationBuilder.DropColumn(name: "hash", table: "tokens"); + migrationBuilder.DropColumn( + name: "hash", + table: "tokens"); } } } diff --git a/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs new file mode 100644 index 0000000..07d8181 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs @@ -0,0 +1,474 @@ +// +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240528145744_AddListHidden")] + partial class AddListHidden + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RedirectUris") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("redirect_uris"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.ToTable("applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthType") + .HasColumnType("integer") + .HasColumnName("auth_type"); + + b.Property("FediverseApplicationId") + .HasColumnType("bigint") + .HasColumnName("fediverse_application_id"); + + b.Property("RemoteId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("remote_id"); + + b.Property("RemoteUsername") + .HasColumnType("text") + .HasColumnName("remote_username"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_methods"); + + b.HasIndex("FediverseApplicationId") + .HasDatabaseName("ix_auth_methods_fediverse_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_methods_user_id"); + + b.ToTable("auth_methods", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("InstanceType") + .HasColumnType("integer") + .HasColumnName("instance_type"); + + b.HasKey("Id") + .HasName("pk_fediverse_applications"); + + b.ToTable("fediverse_applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_members"); + + b.HasIndex("UserId", "Name") + .IsUnique() + .HasDatabaseName("ix_members_user_id_name"); + + b.ToTable("members", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("bigint") + .HasColumnName("application_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("ManuallyExpired") + .HasColumnType("boolean") + .HasColumnName("manually_expired"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tokens"); + + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_tokens_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("ListHidden") + .HasColumnType("boolean") + .HasColumnName("list_hidden"); + + b.Property("MemberTitle") + .HasColumnType("text") + .HasColumnName("member_title"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") + .WithMany() + .HasForeignKey("FediverseApplicationId") + .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("AuthMethods") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_methods_users_user_id"); + + b.Navigation("FediverseApplication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("Members") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_members_users_user_id"); + + b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Names", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_applications_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + + b.Navigation("Application"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.cs b/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.cs index e440cc1..1b40552 100644 --- a/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.cs +++ b/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.cs @@ -1,13 +1,10 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Foxnouns.Backend.Database.Migrations { /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240528145744_AddListHidden")] public partial class AddListHidden : Migration { /// @@ -18,14 +15,15 @@ namespace Foxnouns.Backend.Database.Migrations table: "users", type: "boolean", nullable: false, - defaultValue: false - ); + defaultValue: false); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn(name: "list_hidden", table: "users"); + migrationBuilder.DropColumn( + name: "list_hidden", + table: "users"); } } } diff --git a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs new file mode 100644 index 0000000..2c92566 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs @@ -0,0 +1,478 @@ +// +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20240604142522_AddPassword")] + partial class AddPassword + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("RedirectUris") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("redirect_uris"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.HasKey("Id") + .HasName("pk_applications"); + + b.ToTable("applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("AuthType") + .HasColumnType("integer") + .HasColumnName("auth_type"); + + b.Property("FediverseApplicationId") + .HasColumnType("bigint") + .HasColumnName("fediverse_application_id"); + + b.Property("RemoteId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("remote_id"); + + b.Property("RemoteUsername") + .HasColumnType("text") + .HasColumnName("remote_username"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_auth_methods"); + + b.HasIndex("FediverseApplicationId") + .HasDatabaseName("ix_auth_methods_fediverse_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_auth_methods_user_id"); + + b.ToTable("auth_methods", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ClientId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_id"); + + b.Property("ClientSecret") + .IsRequired() + .HasColumnType("text") + .HasColumnName("client_secret"); + + b.Property("Domain") + .IsRequired() + .HasColumnType("text") + .HasColumnName("domain"); + + b.Property("InstanceType") + .HasColumnType("integer") + .HasColumnName("instance_type"); + + b.HasKey("Id") + .HasName("pk_fediverse_applications"); + + b.ToTable("fediverse_applications", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_members"); + + b.HasIndex("UserId", "Name") + .IsUnique() + .HasDatabaseName("ix_members_user_id_name"); + + b.ToTable("members", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("ApplicationId") + .HasColumnType("bigint") + .HasColumnName("application_id"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires_at"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("hash"); + + b.Property("ManuallyExpired") + .HasColumnType("boolean") + .HasColumnName("manually_expired"); + + b.Property("Scopes") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("scopes"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_tokens"); + + b.HasIndex("ApplicationId") + .HasDatabaseName("ix_tokens_application_id"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_tokens_user_id"); + + b.ToTable("tokens", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Avatar") + .HasColumnType("text") + .HasColumnName("avatar"); + + b.Property("Bio") + .HasColumnType("text") + .HasColumnName("bio"); + + b.Property("DisplayName") + .HasColumnType("text") + .HasColumnName("display_name"); + + b.Property("Links") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("links"); + + b.Property("ListHidden") + .HasColumnType("boolean") + .HasColumnName("list_hidden"); + + b.Property("MemberTitle") + .HasColumnType("text") + .HasColumnName("member_title"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("Role") + .HasColumnType("integer") + .HasColumnName("role"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + b.HasIndex("Username") + .IsUnique() + .HasDatabaseName("ix_users_username"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") + .WithMany() + .HasForeignKey("FediverseApplicationId") + .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("AuthMethods") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_auth_methods_users_user_id"); + + b.Navigation("FediverseApplication"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany("Members") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_members_users_user_id"); + + b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Names", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") + .WithMany() + .HasForeignKey("ApplicationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_applications_application_id"); + + b.HasOne("Foxnouns.Backend.Database.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_tokens_users_user_id"); + + b.Navigation("Application"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + }); + + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => + { + b.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs index 6d73c86..23671a8 100644 --- a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs +++ b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs @@ -1,13 +1,10 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable namespace Foxnouns.Backend.Database.Migrations { /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240604142522_AddPassword")] public partial class AddPassword : Migration { /// @@ -17,14 +14,15 @@ namespace Foxnouns.Backend.Database.Migrations name: "password", table: "users", type: "text", - nullable: true - ); + nullable: true); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn(name: "password", table: "users"); + migrationBuilder.DropColumn( + name: "password", + table: "users"); } } } diff --git a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs deleted file mode 100644 index 931e8ab..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240611225328_AddTemporaryKeyCache")] - public partial class AddTemporaryKeyCache : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "temporary_keys", - columns: table => new - { - id = table - .Column(type: "bigint", nullable: false) - .Annotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ), - key = table.Column(type: "text", nullable: false), - value = table.Column(type: "text", nullable: false), - expires = table.Column( - type: "timestamp with time zone", - nullable: false - ), - }, - constraints: table => table.PrimaryKey("pk_temporary_keys", x => x.id) - ); - - migrationBuilder.CreateIndex( - name: "ix_temporary_keys_key", - table: "temporary_keys", - column: "key", - unique: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "temporary_keys"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs deleted file mode 100644 index 732724e..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240712233806_AddUserLastActive")] - public partial class AddUserLastActive : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "last_active", - table: "users", - type: "timestamp with time zone", - nullable: false, - defaultValueSql: "now()" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "last_active", table: "users"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs deleted file mode 100644 index 79675a3..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240713000719_AddDeleted")] - public partial class AddDeleted : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "deleted", - table: "users", - type: "boolean", - nullable: false, - defaultValue: false - ); - - migrationBuilder.AddColumn( - name: "deleted_at", - table: "users", - type: "timestamp with time zone", - nullable: true - ); - - migrationBuilder.AddColumn( - name: "deleted_by", - table: "users", - type: "bigint", - nullable: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "deleted", table: "users"); - - migrationBuilder.DropColumn(name: "deleted_at", table: "users"); - - migrationBuilder.DropColumn(name: "deleted_by", table: "users"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs deleted file mode 100644 index 0ec835d..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240821210355_AddCustomPreferences")] - public partial class AddCustomPreferences : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn>( - name: "custom_preferences", - table: "users", - type: "jsonb", - nullable: false, - defaultValueSql: "'{}'" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "custom_preferences", table: "users"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs deleted file mode 100644 index a44b29c..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240905191709_AddUserSettings")] - public partial class AddUserSettings : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "settings", - table: "users", - type: "jsonb", - nullable: false, - defaultValueSql: "'{}'" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "settings", table: "users"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs deleted file mode 100644 index 6cdc1b4..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240926124950_AddSids")] - public partial class AddSids : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "sid", - table: "users", - type: "text", - nullable: true - ); - - migrationBuilder.AddColumn( - name: "last_sid_reroll", - table: "users", - type: "timestamp with time zone", - nullable: false, - defaultValueSql: "now() - '1 hour'::interval" - ); - - migrationBuilder.AddColumn( - name: "sid", - table: "members", - type: "text", - nullable: true - ); - - migrationBuilder.CreateIndex( - name: "ix_users_sid", - table: "users", - column: "sid", - unique: true - ); - - migrationBuilder.CreateIndex( - name: "ix_members_sid", - table: "members", - column: "sid", - unique: true - ); - - migrationBuilder.Sql( - @"create function generate_sid(len int) returns text as $$ - select string_agg(substr('abcdefghijklmnopqrstuvwxyz', ceil(random() * 26)::integer, 1), '') from generate_series(1, len) -$$ language sql volatile; -" - ); - migrationBuilder.Sql( - @"create function find_free_user_sid() returns text as $$ -declare new_sid text; -begin - loop - new_sid := generate_sid(5); - if not exists (select 1 from users where sid = new_sid) then return new_sid; end if; - end loop; -end -$$ language plpgsql volatile; -" - ); - migrationBuilder.Sql( - @"create function find_free_member_sid() returns text as $$ -declare new_sid text; -begin - loop - new_sid := generate_sid(6); - if not exists (select 1 from members where sid = new_sid) then return new_sid; end if; - end loop; -end -$$ language plpgsql volatile;" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("drop function find_free_member_sid;"); - migrationBuilder.Sql("drop function find_free_user_sid;"); - migrationBuilder.Sql("drop function generate_sid;"); - - migrationBuilder.DropIndex(name: "ix_users_sid", table: "users"); - - migrationBuilder.DropIndex(name: "ix_members_sid", table: "members"); - - migrationBuilder.DropColumn(name: "sid", table: "users"); - - migrationBuilder.DropColumn(name: "last_sid_reroll", table: "users"); - - migrationBuilder.DropColumn(name: "sid", table: "members"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs deleted file mode 100644 index 8e7821a..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs +++ /dev/null @@ -1,64 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240926130208_NonNullableSids")] - public partial class NonNullableSids : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "sid", - table: "users", - type: "text", - nullable: false, - defaultValueSql: "find_free_user_sid()", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true - ); - - migrationBuilder.AlterColumn( - name: "sid", - table: "members", - type: "text", - nullable: false, - defaultValueSql: "find_free_member_sid()", - oldClrType: typeof(string), - oldType: "text", - oldNullable: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "sid", - table: "users", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldDefaultValueSql: "find_free_user_sid()" - ); - - migrationBuilder.AlterColumn( - name: "sid", - table: "members", - type: "text", - nullable: true, - oldClrType: typeof(string), - oldType: "text", - oldDefaultValueSql: "find_free_member_sid()" - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs b/Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs deleted file mode 100644 index 08097d6..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs +++ /dev/null @@ -1,147 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20240926180037_AddFlags")] - public partial class AddFlags : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "pride_flags", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - user_id = table.Column(type: "bigint", nullable: false), - hash = table.Column(type: "text", nullable: false), - name = table.Column(type: "text", nullable: false), - description = table.Column(type: "text", nullable: true), - }, - constraints: table => - { - table.PrimaryKey("pk_pride_flags", x => x.id); - table.ForeignKey( - name: "fk_pride_flags_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateTable( - name: "member_flags", - columns: table => new - { - id = table - .Column(type: "bigint", nullable: false) - .Annotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ), - member_id = table.Column(type: "bigint", nullable: false), - pride_flag_id = table.Column(type: "bigint", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_member_flags", x => x.id); - table.ForeignKey( - name: "fk_member_flags_members_member_id", - column: x => x.member_id, - principalTable: "members", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - table.ForeignKey( - name: "fk_member_flags_pride_flags_pride_flag_id", - column: x => x.pride_flag_id, - principalTable: "pride_flags", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateTable( - name: "user_flags", - columns: table => new - { - id = table - .Column(type: "bigint", nullable: false) - .Annotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ), - user_id = table.Column(type: "bigint", nullable: false), - pride_flag_id = table.Column(type: "bigint", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_user_flags", x => x.id); - table.ForeignKey( - name: "fk_user_flags_pride_flags_pride_flag_id", - column: x => x.pride_flag_id, - principalTable: "pride_flags", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - table.ForeignKey( - name: "fk_user_flags_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_member_flags_member_id", - table: "member_flags", - column: "member_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_member_flags_pride_flag_id", - table: "member_flags", - column: "pride_flag_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_pride_flags_user_id", - table: "pride_flags", - column: "user_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_user_flags_pride_flag_id", - table: "user_flags", - column: "pride_flag_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_user_flags_user_id", - table: "user_flags", - column: "user_id" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "member_flags"); - - migrationBuilder.DropTable(name: "user_flags"); - - migrationBuilder.DropTable(name: "pride_flags"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241006125003_AddFediverseAccessTokens.cs b/Foxnouns.Backend/Database/Migrations/20241006125003_AddFediverseAccessTokens.cs deleted file mode 100644 index 37023f0..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241006125003_AddFediverseAccessTokens.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241006125003_AddFediverseAccessTokens")] - public partial class AddFediverseAccessTokens : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "access_token", - table: "fediverse_applications", - type: "text", - nullable: true - ); - - migrationBuilder.AddColumn( - name: "token_valid_until", - table: "fediverse_applications", - type: "timestamp with time zone", - nullable: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "access_token", table: "fediverse_applications"); - - migrationBuilder.DropColumn(name: "token_valid_until", table: "fediverse_applications"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs b/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs deleted file mode 100644 index fbc8d3d..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241123210306_RemoveFediverseApplicationTokens")] - public partial class RemoveFediverseApplicationTokens : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "access_token", table: "fediverse_applications"); - - migrationBuilder.DropColumn(name: "token_valid_until", table: "fediverse_applications"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "access_token", - table: "fediverse_applications", - type: "text", - nullable: true - ); - - migrationBuilder.AddColumn( - name: "token_valid_until", - table: "fediverse_applications", - type: "timestamp with time zone", - nullable: true - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241124201309_AddUserTimezone.cs b/Foxnouns.Backend/Database/Migrations/20241124201309_AddUserTimezone.cs deleted file mode 100644 index e317f65..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241124201309_AddUserTimezone.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241124201309_AddUserTimezone")] - public partial class AddUserTimezone : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "timezone", - table: "users", - type: "text", - nullable: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "timezone", table: "users"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241128202508_AddAuthMethodUniqueIndex.cs b/Foxnouns.Backend/Database/Migrations/20241128202508_AddAuthMethodUniqueIndex.cs deleted file mode 100644 index f6a00b5..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241128202508_AddAuthMethodUniqueIndex.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241128202508_AddAuthMethodUniqueIndex")] - public partial class AddAuthMethodUniqueIndex : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateIndex( - name: "ix_auth_methods_auth_type_remote_id", - table: "auth_methods", - columns: new[] { "auth_type", "remote_id" }, - unique: true, - filter: "fediverse_application_id IS NULL" - ); - - migrationBuilder.CreateIndex( - name: "ix_auth_methods_auth_type_remote_id_fediverse_application_id", - table: "auth_methods", - columns: new[] { "auth_type", "remote_id", "fediverse_application_id" }, - unique: true, - filter: "fediverse_application_id IS NOT NULL" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "ix_auth_methods_auth_type_remote_id", - table: "auth_methods" - ); - - migrationBuilder.DropIndex( - name: "ix_auth_methods_auth_type_remote_id_fediverse_application_id", - table: "auth_methods" - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241202153736_AddDataExports.cs b/Foxnouns.Backend/Database/Migrations/20241202153736_AddDataExports.cs deleted file mode 100644 index 8a4347f..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241202153736_AddDataExports.cs +++ /dev/null @@ -1,57 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241202153736_AddDataExports")] - public partial class AddDataExports : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "data_exports", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - user_id = table.Column(type: "bigint", nullable: false), - filename = table.Column(type: "text", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_data_exports", x => x.id); - table.ForeignKey( - name: "fk_data_exports_users_user_id", - column: x => x.user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_data_exports_filename", - table: "data_exports", - column: "filename", - unique: true - ); - - migrationBuilder.CreateIndex( - name: "ix_data_exports_user_id", - table: "data_exports", - column: "user_id" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "data_exports"); - } - } -} 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/20241211193653_AddSentEmailCache.cs b/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs deleted file mode 100644 index e0fe00d..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241211193653_AddSentEmailCache")] - public partial class AddSentEmailCache : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "sent_emails", - columns: table => new - { - id = table - .Column(type: "integer", nullable: false) - .Annotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ), - email = table.Column(type: "text", nullable: false), - sent_at = table.Column( - type: "timestamp with time zone", - nullable: false - ), - }, - constraints: table => - { - table.PrimaryKey("pk_sent_emails", x => x.id); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_sent_emails_email_sent_at", - table: "sent_emails", - columns: new[] { "email", "sent_at" } - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "sent_emails"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241217010207_AddReports.cs b/Foxnouns.Backend/Database/Migrations/20241217010207_AddReports.cs deleted file mode 100644 index 22a1cf8..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241217010207_AddReports.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241217010207_AddReports")] - public partial class AddReports : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterDatabase().Annotation("Npgsql:PostgresExtension:hstore", ",,"); - - migrationBuilder.CreateTable( - name: "notifications", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - target_id = table.Column(type: "bigint", nullable: false), - type = table.Column(type: "integer", nullable: false), - message = table.Column(type: "text", nullable: true), - localization_key = table.Column(type: "text", nullable: true), - localization_params = table.Column>( - type: "hstore", - nullable: false - ), - acknowledged_at = table.Column( - type: "timestamp with time zone", - nullable: true - ), - }, - constraints: table => - { - table.PrimaryKey("pk_notifications", x => x.id); - table.ForeignKey( - name: "fk_notifications_users_target_id", - column: x => x.target_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateTable( - name: "reports", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - reporter_id = table.Column(type: "bigint", nullable: false), - target_user_id = table.Column(type: "bigint", nullable: false), - target_member_id = table.Column(type: "bigint", nullable: true), - status = table.Column(type: "integer", nullable: false), - reason = table.Column(type: "integer", nullable: false), - target_type = table.Column(type: "integer", nullable: false), - target_snapshot = table.Column(type: "text", nullable: true), - }, - constraints: table => - { - table.PrimaryKey("pk_reports", x => x.id); - table.ForeignKey( - name: "fk_reports_members_target_member_id", - column: x => x.target_member_id, - principalTable: "members", - principalColumn: "id" - ); - table.ForeignKey( - name: "fk_reports_users_reporter_id", - column: x => x.reporter_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - table.ForeignKey( - name: "fk_reports_users_target_user_id", - column: x => x.target_user_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateTable( - name: "audit_log", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - moderator_id = table.Column(type: "bigint", nullable: false), - moderator_username = table.Column(type: "text", nullable: false), - target_user_id = table.Column(type: "bigint", nullable: true), - target_username = table.Column(type: "text", nullable: true), - target_member_id = table.Column(type: "bigint", nullable: true), - target_member_name = table.Column(type: "text", nullable: true), - report_id = table.Column(type: "bigint", nullable: true), - type = table.Column(type: "integer", nullable: false), - reason = table.Column(type: "text", nullable: true), - cleared_fields = table.Column(type: "text[]", nullable: true), - }, - constraints: table => - { - table.PrimaryKey("pk_audit_log", x => x.id); - table.ForeignKey( - name: "fk_audit_log_reports_report_id", - column: x => x.report_id, - principalTable: "reports", - principalColumn: "id" - ); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_audit_log_report_id", - table: "audit_log", - column: "report_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_notifications_target_id", - table: "notifications", - column: "target_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_reports_reporter_id", - table: "reports", - column: "reporter_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_reports_target_member_id", - table: "reports", - column: "target_member_id" - ); - - migrationBuilder.CreateIndex( - name: "ix_reports_target_user_id", - table: "reports", - column: "target_user_id" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "audit_log"); - - migrationBuilder.DropTable(name: "notifications"); - - migrationBuilder.DropTable(name: "reports"); - - migrationBuilder.AlterDatabase().OldAnnotation("Npgsql:PostgresExtension:hstore", ",,"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241217195351_AddFediAppForceRefresh.cs b/Foxnouns.Backend/Database/Migrations/20241217195351_AddFediAppForceRefresh.cs deleted file mode 100644 index 8340273..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241217195351_AddFediAppForceRefresh.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241217195351_AddFediAppForceRefresh")] - public partial class AddFediAppForceRefresh : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn>( - name: "localization_params", - table: "notifications", - type: "hstore", - nullable: false, - oldClrType: typeof(Dictionary), - oldType: "hstore", - oldNullable: true - ); - - migrationBuilder.AddColumn( - name: "force_refresh", - table: "fediverse_applications", - type: "boolean", - nullable: false, - defaultValue: false - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "force_refresh", table: "fediverse_applications"); - - migrationBuilder.AlterColumn>( - name: "localization_params", - table: "notifications", - type: "hstore", - nullable: true, - oldClrType: typeof(Dictionary), - oldType: "hstore" - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241218195457_AddContextToReports.cs b/Foxnouns.Backend/Database/Migrations/20241218195457_AddContextToReports.cs deleted file mode 100644 index 3dc6029..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241218195457_AddContextToReports.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241218195457_AddContextToReports")] - public partial class AddContextToReports : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "context", - table: "reports", - type: "text", - nullable: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "context", table: "reports"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241218201855_MakeAuditLogReportsNullable.cs b/Foxnouns.Backend/Database/Migrations/20241218201855_MakeAuditLogReportsNullable.cs deleted file mode 100644 index 53a1f72..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241218201855_MakeAuditLogReportsNullable.cs +++ /dev/null @@ -1,65 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241218201855_MakeAuditLogReportsNullable")] - public partial class MakeAuditLogReportsNullable : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "fk_audit_log_reports_report_id", - table: "audit_log" - ); - - migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log"); - - migrationBuilder.CreateIndex( - name: "ix_audit_log_report_id", - table: "audit_log", - column: "report_id", - unique: true - ); - - migrationBuilder.AddForeignKey( - name: "fk_audit_log_reports_report_id", - table: "audit_log", - column: "report_id", - principalTable: "reports", - principalColumn: "id", - onDelete: ReferentialAction.SetNull - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "fk_audit_log_reports_report_id", - table: "audit_log" - ); - - migrationBuilder.DropIndex(name: "ix_audit_log_report_id", table: "audit_log"); - - migrationBuilder.CreateIndex( - name: "ix_audit_log_report_id", - table: "audit_log", - column: "report_id" - ); - - migrationBuilder.AddForeignKey( - name: "fk_audit_log_reports_report_id", - table: "audit_log", - column: "report_id", - principalTable: "reports", - principalColumn: "id" - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20241225155818_AddLegacyIds.cs b/Foxnouns.Backend/Database/Migrations/20241225155818_AddLegacyIds.cs deleted file mode 100644 index b8330cb..0000000 --- a/Foxnouns.Backend/Database/Migrations/20241225155818_AddLegacyIds.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20241225155818_AddLegacyIds")] - public partial class AddLegacyIds : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "legacy_id", - table: "users", - type: "text", - nullable: false, - defaultValueSql: "gen_random_uuid()" - ); - - migrationBuilder.AddColumn( - name: "legacy_id", - table: "pride_flags", - type: "text", - nullable: false, - defaultValueSql: "gen_random_uuid()" - ); - - migrationBuilder.AddColumn( - name: "legacy_id", - table: "members", - type: "text", - nullable: false, - defaultValueSql: "gen_random_uuid()" - ); - - migrationBuilder.CreateIndex( - name: "ix_users_legacy_id", - table: "users", - column: "legacy_id", - unique: true - ); - - migrationBuilder.CreateIndex( - name: "ix_pride_flags_legacy_id", - table: "pride_flags", - column: "legacy_id", - unique: true - ); - - migrationBuilder.CreateIndex( - name: "ix_members_legacy_id", - table: "members", - column: "legacy_id", - unique: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex(name: "ix_users_legacy_id", table: "users"); - - migrationBuilder.DropIndex(name: "ix_pride_flags_legacy_id", table: "pride_flags"); - - migrationBuilder.DropIndex(name: "ix_members_legacy_id", table: "members"); - - migrationBuilder.DropColumn(name: "legacy_id", table: "users"); - - migrationBuilder.DropColumn(name: "legacy_id", table: "pride_flags"); - - migrationBuilder.DropColumn(name: "legacy_id", table: "members"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs b/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs deleted file mode 100644 index 27a8ada..0000000 --- a/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - [DbContext(typeof(DatabaseContext))] - [Migration("20250304155708_RemoveTemporaryKeys")] - public partial class RemoveTemporaryKeys : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "temporary_keys"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "temporary_keys", - columns: table => new - { - id = table - .Column(type: "bigint", nullable: false) - .Annotation( - "Npgsql:ValueGenerationStrategy", - NpgsqlValueGenerationStrategy.IdentityByDefaultColumn - ), - expires = table.Column( - type: "timestamp with time zone", - nullable: false - ), - key = table.Column(type: "text", nullable: false), - value = table.Column(type: "text", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_temporary_keys", x => x.id); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_temporary_keys_key", - table: "temporary_keys", - column: "key", - unique: true - ); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs b/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs deleted file mode 100644 index d2df141..0000000 --- a/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.Designer.cs +++ /dev/null @@ -1,915 +0,0 @@ -// -using System.Collections.Generic; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20250329131053_AddNotices")] - partial class AddNotices - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("ClientId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_id"); - - b.Property("ClientSecret") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_secret"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.PrimitiveCollection("RedirectUris") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("redirect_uris"); - - b.PrimitiveCollection("Scopes") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("scopes"); - - b.HasKey("Id") - .HasName("pk_applications"); - - b.ToTable("applications", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.PrimitiveCollection("ClearedFields") - .HasColumnType("text[]") - .HasColumnName("cleared_fields"); - - b.Property("ModeratorId") - .HasColumnType("bigint") - .HasColumnName("moderator_id"); - - b.Property("ModeratorUsername") - .IsRequired() - .HasColumnType("text") - .HasColumnName("moderator_username"); - - b.Property("Reason") - .HasColumnType("text") - .HasColumnName("reason"); - - b.Property("ReportId") - .HasColumnType("bigint") - .HasColumnName("report_id"); - - b.Property("TargetMemberId") - .HasColumnType("bigint") - .HasColumnName("target_member_id"); - - b.Property("TargetMemberName") - .HasColumnType("text") - .HasColumnName("target_member_name"); - - b.Property("TargetUserId") - .HasColumnType("bigint") - .HasColumnName("target_user_id"); - - b.Property("TargetUsername") - .HasColumnType("text") - .HasColumnName("target_username"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_audit_log"); - - b.HasIndex("ReportId") - .IsUnique() - .HasDatabaseName("ix_audit_log_report_id"); - - b.ToTable("audit_log", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AuthType") - .HasColumnType("integer") - .HasColumnName("auth_type"); - - b.Property("FediverseApplicationId") - .HasColumnType("bigint") - .HasColumnName("fediverse_application_id"); - - b.Property("RemoteId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("remote_id"); - - b.Property("RemoteUsername") - .HasColumnType("text") - .HasColumnName("remote_username"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_auth_methods"); - - b.HasIndex("FediverseApplicationId") - .HasDatabaseName("ix_auth_methods_fediverse_application_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_auth_methods_user_id"); - - b.HasIndex("AuthType", "RemoteId") - .IsUnique() - .HasDatabaseName("ix_auth_methods_auth_type_remote_id") - .HasFilter("fediverse_application_id IS NULL"); - - b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId") - .IsUnique() - .HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id") - .HasFilter("fediverse_application_id IS NOT NULL"); - - b.ToTable("auth_methods", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Filename") - .IsRequired() - .HasColumnType("text") - .HasColumnName("filename"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_data_exports"); - - b.HasIndex("Filename") - .IsUnique() - .HasDatabaseName("ix_data_exports_filename"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_data_exports_user_id"); - - b.ToTable("data_exports", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("ClientId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_id"); - - b.Property("ClientSecret") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_secret"); - - b.Property("Domain") - .IsRequired() - .HasColumnType("text") - .HasColumnName("domain"); - - b.Property("ForceRefresh") - .HasColumnType("boolean") - .HasColumnName("force_refresh"); - - b.Property("InstanceType") - .HasColumnType("integer") - .HasColumnName("instance_type"); - - b.HasKey("Id") - .HasName("pk_fediverse_applications"); - - b.ToTable("fediverse_applications", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Avatar") - .HasColumnType("text") - .HasColumnName("avatar"); - - b.Property("Bio") - .HasColumnType("text") - .HasColumnName("bio"); - - b.Property("DisplayName") - .HasColumnType("text") - .HasColumnName("display_name"); - - b.Property>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.PrimitiveCollection("Links") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("links"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property>("Names") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("names"); - - b.Property>("Pronouns") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("pronouns"); - - b.Property("Sid") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("sid") - .HasDefaultValueSql("find_free_member_sid()"); - - b.Property("Unlisted") - .HasColumnType("boolean") - .HasColumnName("unlisted"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_members"); - - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_members_legacy_id"); - - b.HasIndex("Sid") - .IsUnique() - .HasDatabaseName("ix_members_sid"); - - b.HasIndex("UserId", "Name") - .IsUnique() - .HasDatabaseName("ix_members_user_id_name"); - - b.ToTable("members", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("MemberId") - .HasColumnType("bigint") - .HasColumnName("member_id"); - - b.Property("PrideFlagId") - .HasColumnType("bigint") - .HasColumnName("pride_flag_id"); - - b.HasKey("Id") - .HasName("pk_member_flags"); - - b.HasIndex("MemberId") - .HasDatabaseName("ix_member_flags_member_id"); - - b.HasIndex("PrideFlagId") - .HasDatabaseName("ix_member_flags_pride_flag_id"); - - b.ToTable("member_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AuthorId") - .HasColumnType("bigint") - .HasColumnName("author_id"); - - b.Property("EndTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_time"); - - b.Property("Message") - .IsRequired() - .HasColumnType("text") - .HasColumnName("message"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_time"); - - b.HasKey("Id") - .HasName("pk_notices"); - - b.HasIndex("AuthorId") - .HasDatabaseName("ix_notices_author_id"); - - b.ToTable("notices", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AcknowledgedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("acknowledged_at"); - - b.Property("LocalizationKey") - .HasColumnType("text") - .HasColumnName("localization_key"); - - b.Property>("LocalizationParams") - .IsRequired() - .HasColumnType("hstore") - .HasColumnName("localization_params"); - - b.Property("Message") - .HasColumnType("text") - .HasColumnName("message"); - - b.Property("TargetId") - .HasColumnType("bigint") - .HasColumnName("target_id"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_notifications"); - - b.HasIndex("TargetId") - .HasDatabaseName("ix_notifications_target_id"); - - b.ToTable("notifications", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Description") - .HasColumnType("text") - .HasColumnName("description"); - - b.Property("Hash") - .HasColumnType("text") - .HasColumnName("hash"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_pride_flags"); - - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_pride_flags_legacy_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_pride_flags_user_id"); - - b.ToTable("pride_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Context") - .HasColumnType("text") - .HasColumnName("context"); - - b.Property("Reason") - .HasColumnType("integer") - .HasColumnName("reason"); - - b.Property("ReporterId") - .HasColumnType("bigint") - .HasColumnName("reporter_id"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("TargetMemberId") - .HasColumnType("bigint") - .HasColumnName("target_member_id"); - - b.Property("TargetSnapshot") - .HasColumnType("text") - .HasColumnName("target_snapshot"); - - b.Property("TargetType") - .HasColumnType("integer") - .HasColumnName("target_type"); - - b.Property("TargetUserId") - .HasColumnType("bigint") - .HasColumnName("target_user_id"); - - b.HasKey("Id") - .HasName("pk_reports"); - - b.HasIndex("ReporterId") - .HasDatabaseName("ix_reports_reporter_id"); - - b.HasIndex("TargetMemberId") - .HasDatabaseName("ix_reports_target_member_id"); - - b.HasIndex("TargetUserId") - .HasDatabaseName("ix_reports_target_user_id"); - - b.ToTable("reports", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("ApplicationId") - .HasColumnType("bigint") - .HasColumnName("application_id"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("Hash") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hash"); - - b.Property("ManuallyExpired") - .HasColumnType("boolean") - .HasColumnName("manually_expired"); - - b.PrimitiveCollection("Scopes") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("scopes"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_tokens"); - - b.HasIndex("ApplicationId") - .HasDatabaseName("ix_tokens_application_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_tokens_user_id"); - - b.ToTable("tokens", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Avatar") - .HasColumnType("text") - .HasColumnName("avatar"); - - b.Property("Bio") - .HasColumnType("text") - .HasColumnName("bio"); - - b.Property>("CustomPreferences") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("custom_preferences"); - - b.Property("Deleted") - .HasColumnType("boolean") - .HasColumnName("deleted"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("DeletedBy") - .HasColumnType("bigint") - .HasColumnName("deleted_by"); - - b.Property("DisplayName") - .HasColumnType("text") - .HasColumnName("display_name"); - - b.Property>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("LastActive") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_active"); - - b.Property("LastSidReroll") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_sid_reroll"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.PrimitiveCollection("Links") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("links"); - - b.Property("ListHidden") - .HasColumnType("boolean") - .HasColumnName("list_hidden"); - - b.Property("MemberTitle") - .HasColumnType("text") - .HasColumnName("member_title"); - - b.Property>("Names") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("names"); - - b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); - - b.Property>("Pronouns") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("pronouns"); - - b.Property("Role") - .HasColumnType("integer") - .HasColumnName("role"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("settings"); - - b.Property("Sid") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("sid") - .HasDefaultValueSql("find_free_user_sid()"); - - b.Property("Timezone") - .HasColumnType("text") - .HasColumnName("timezone"); - - b.Property("Username") - .IsRequired() - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("Id") - .HasName("pk_users"); - - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_users_legacy_id"); - - b.HasIndex("Sid") - .IsUnique() - .HasDatabaseName("ix_users_sid"); - - b.HasIndex("Username") - .IsUnique() - .HasDatabaseName("ix_users_username"); - - b.ToTable("users", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("PrideFlagId") - .HasColumnType("bigint") - .HasColumnName("pride_flag_id"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_user_flags"); - - b.HasIndex("PrideFlagId") - .HasDatabaseName("ix_user_flags_pride_flag_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_user_flags_user_id"); - - b.ToTable("user_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report") - .WithOne("AuditLogEntry") - .HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("fk_audit_log_reports_report_id"); - - b.Navigation("Report"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") - .WithMany() - .HasForeignKey("FediverseApplicationId") - .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("AuthMethods") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_auth_methods_users_user_id"); - - b.Navigation("FediverseApplication"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("DataExports") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_data_exports_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("Members") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_members_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Member", null) - .WithMany("ProfileFlags") - .HasForeignKey("MemberId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_member_flags_members_member_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") - .WithMany() - .HasForeignKey("PrideFlagId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_member_flags_pride_flags_pride_flag_id"); - - b.Navigation("PrideFlag"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_notices_users_author_id"); - - b.Navigation("Author"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Target") - .WithMany() - .HasForeignKey("TargetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_notifications_users_target_id"); - - b.Navigation("Target"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", null) - .WithMany("Flags") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_pride_flags_users_user_id"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter") - .WithMany() - .HasForeignKey("ReporterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_reports_users_reporter_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember") - .WithMany() - .HasForeignKey("TargetMemberId") - .HasConstraintName("fk_reports_members_target_member_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser") - .WithMany() - .HasForeignKey("TargetUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_reports_users_target_user_id"); - - b.Navigation("Reporter"); - - b.Navigation("TargetMember"); - - b.Navigation("TargetUser"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_tokens_applications_application_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_tokens_users_user_id"); - - b.Navigation("Application"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") - .WithMany() - .HasForeignKey("PrideFlagId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_user_flags_pride_flags_pride_flag_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", null) - .WithMany("ProfileFlags") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_user_flags_users_user_id"); - - b.Navigation("PrideFlag"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => - { - b.Navigation("ProfileFlags"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.Navigation("AuditLogEntry"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => - { - b.Navigation("AuthMethods"); - - b.Navigation("DataExports"); - - b.Navigation("Flags"); - - b.Navigation("Members"); - - b.Navigation("ProfileFlags"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.cs b/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.cs deleted file mode 100644 index 24c5166..0000000 --- a/Foxnouns.Backend/Database/Migrations/20250329131053_AddNotices.cs +++ /dev/null @@ -1,56 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using NodaTime; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - public partial class AddNotices : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "notices", - columns: table => new - { - id = table.Column(type: "bigint", nullable: false), - message = table.Column(type: "text", nullable: false), - start_time = table.Column( - type: "timestamp with time zone", - nullable: false - ), - end_time = table.Column( - type: "timestamp with time zone", - nullable: false - ), - author_id = table.Column(type: "bigint", nullable: false), - }, - constraints: table => - { - table.PrimaryKey("pk_notices", x => x.id); - table.ForeignKey( - name: "fk_notices_users_author_id", - column: x => x.author_id, - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade - ); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_notices_author_id", - table: "notices", - column: "author_id" - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "notices"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs deleted file mode 100644 index cb9377d..0000000 --- a/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.Designer.cs +++ /dev/null @@ -1,923 +0,0 @@ -// -using System.Collections.Generic; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using NodaTime; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20250410192220_AddAvatarMigrations")] - partial class AddAvatarMigrations - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.2") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("ClientId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_id"); - - b.Property("ClientSecret") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_secret"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.PrimitiveCollection("RedirectUris") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("redirect_uris"); - - b.PrimitiveCollection("Scopes") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("scopes"); - - b.HasKey("Id") - .HasName("pk_applications"); - - b.ToTable("applications", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.PrimitiveCollection("ClearedFields") - .HasColumnType("text[]") - .HasColumnName("cleared_fields"); - - b.Property("ModeratorId") - .HasColumnType("bigint") - .HasColumnName("moderator_id"); - - b.Property("ModeratorUsername") - .IsRequired() - .HasColumnType("text") - .HasColumnName("moderator_username"); - - b.Property("Reason") - .HasColumnType("text") - .HasColumnName("reason"); - - b.Property("ReportId") - .HasColumnType("bigint") - .HasColumnName("report_id"); - - b.Property("TargetMemberId") - .HasColumnType("bigint") - .HasColumnName("target_member_id"); - - b.Property("TargetMemberName") - .HasColumnType("text") - .HasColumnName("target_member_name"); - - b.Property("TargetUserId") - .HasColumnType("bigint") - .HasColumnName("target_user_id"); - - b.Property("TargetUsername") - .HasColumnType("text") - .HasColumnName("target_username"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_audit_log"); - - b.HasIndex("ReportId") - .IsUnique() - .HasDatabaseName("ix_audit_log_report_id"); - - b.ToTable("audit_log", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AuthType") - .HasColumnType("integer") - .HasColumnName("auth_type"); - - b.Property("FediverseApplicationId") - .HasColumnType("bigint") - .HasColumnName("fediverse_application_id"); - - b.Property("RemoteId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("remote_id"); - - b.Property("RemoteUsername") - .HasColumnType("text") - .HasColumnName("remote_username"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_auth_methods"); - - b.HasIndex("FediverseApplicationId") - .HasDatabaseName("ix_auth_methods_fediverse_application_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_auth_methods_user_id"); - - b.HasIndex("AuthType", "RemoteId") - .IsUnique() - .HasDatabaseName("ix_auth_methods_auth_type_remote_id") - .HasFilter("fediverse_application_id IS NULL"); - - b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId") - .IsUnique() - .HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id") - .HasFilter("fediverse_application_id IS NOT NULL"); - - b.ToTable("auth_methods", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Filename") - .IsRequired() - .HasColumnType("text") - .HasColumnName("filename"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_data_exports"); - - b.HasIndex("Filename") - .IsUnique() - .HasDatabaseName("ix_data_exports_filename"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_data_exports_user_id"); - - b.ToTable("data_exports", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("ClientId") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_id"); - - b.Property("ClientSecret") - .IsRequired() - .HasColumnType("text") - .HasColumnName("client_secret"); - - b.Property("Domain") - .IsRequired() - .HasColumnType("text") - .HasColumnName("domain"); - - b.Property("ForceRefresh") - .HasColumnType("boolean") - .HasColumnName("force_refresh"); - - b.Property("InstanceType") - .HasColumnType("integer") - .HasColumnName("instance_type"); - - b.HasKey("Id") - .HasName("pk_fediverse_applications"); - - b.ToTable("fediverse_applications", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Avatar") - .HasColumnType("text") - .HasColumnName("avatar"); - - b.Property("AvatarMigrated") - .HasColumnType("boolean") - .HasColumnName("avatar_migrated"); - - b.Property("Bio") - .HasColumnType("text") - .HasColumnName("bio"); - - b.Property("DisplayName") - .HasColumnType("text") - .HasColumnName("display_name"); - - b.Property>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.PrimitiveCollection("Links") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("links"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property>("Names") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("names"); - - b.Property>("Pronouns") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("pronouns"); - - b.Property("Sid") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("sid") - .HasDefaultValueSql("find_free_member_sid()"); - - b.Property("Unlisted") - .HasColumnType("boolean") - .HasColumnName("unlisted"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_members"); - - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_members_legacy_id"); - - b.HasIndex("Sid") - .IsUnique() - .HasDatabaseName("ix_members_sid"); - - b.HasIndex("UserId", "Name") - .IsUnique() - .HasDatabaseName("ix_members_user_id_name"); - - b.ToTable("members", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("MemberId") - .HasColumnType("bigint") - .HasColumnName("member_id"); - - b.Property("PrideFlagId") - .HasColumnType("bigint") - .HasColumnName("pride_flag_id"); - - b.HasKey("Id") - .HasName("pk_member_flags"); - - b.HasIndex("MemberId") - .HasDatabaseName("ix_member_flags_member_id"); - - b.HasIndex("PrideFlagId") - .HasDatabaseName("ix_member_flags_pride_flag_id"); - - b.ToTable("member_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AuthorId") - .HasColumnType("bigint") - .HasColumnName("author_id"); - - b.Property("EndTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_time"); - - b.Property("Message") - .IsRequired() - .HasColumnType("text") - .HasColumnName("message"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_time"); - - b.HasKey("Id") - .HasName("pk_notices"); - - b.HasIndex("AuthorId") - .HasDatabaseName("ix_notices_author_id"); - - b.ToTable("notices", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AcknowledgedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("acknowledged_at"); - - b.Property("LocalizationKey") - .HasColumnType("text") - .HasColumnName("localization_key"); - - b.Property>("LocalizationParams") - .IsRequired() - .HasColumnType("hstore") - .HasColumnName("localization_params"); - - b.Property("Message") - .HasColumnType("text") - .HasColumnName("message"); - - b.Property("TargetId") - .HasColumnType("bigint") - .HasColumnName("target_id"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_notifications"); - - b.HasIndex("TargetId") - .HasDatabaseName("ix_notifications_target_id"); - - b.ToTable("notifications", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Description") - .HasColumnType("text") - .HasColumnName("description"); - - b.Property("Hash") - .HasColumnType("text") - .HasColumnName("hash"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_pride_flags"); - - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_pride_flags_legacy_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_pride_flags_user_id"); - - b.ToTable("pride_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Context") - .HasColumnType("text") - .HasColumnName("context"); - - b.Property("Reason") - .HasColumnType("integer") - .HasColumnName("reason"); - - b.Property("ReporterId") - .HasColumnType("bigint") - .HasColumnName("reporter_id"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("TargetMemberId") - .HasColumnType("bigint") - .HasColumnName("target_member_id"); - - b.Property("TargetSnapshot") - .HasColumnType("text") - .HasColumnName("target_snapshot"); - - b.Property("TargetType") - .HasColumnType("integer") - .HasColumnName("target_type"); - - b.Property("TargetUserId") - .HasColumnType("bigint") - .HasColumnName("target_user_id"); - - b.HasKey("Id") - .HasName("pk_reports"); - - b.HasIndex("ReporterId") - .HasDatabaseName("ix_reports_reporter_id"); - - b.HasIndex("TargetMemberId") - .HasDatabaseName("ix_reports_target_member_id"); - - b.HasIndex("TargetUserId") - .HasDatabaseName("ix_reports_target_user_id"); - - b.ToTable("reports", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("ApplicationId") - .HasColumnType("bigint") - .HasColumnName("application_id"); - - b.Property("ExpiresAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires_at"); - - b.Property("Hash") - .IsRequired() - .HasColumnType("bytea") - .HasColumnName("hash"); - - b.Property("ManuallyExpired") - .HasColumnType("boolean") - .HasColumnName("manually_expired"); - - b.PrimitiveCollection("Scopes") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("scopes"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_tokens"); - - b.HasIndex("ApplicationId") - .HasDatabaseName("ix_tokens_application_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_tokens_user_id"); - - b.ToTable("tokens", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Avatar") - .HasColumnType("text") - .HasColumnName("avatar"); - - b.Property("AvatarMigrated") - .HasColumnType("boolean") - .HasColumnName("avatar_migrated"); - - b.Property("Bio") - .HasColumnType("text") - .HasColumnName("bio"); - - b.Property>("CustomPreferences") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("custom_preferences"); - - b.Property("Deleted") - .HasColumnType("boolean") - .HasColumnName("deleted"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("DeletedBy") - .HasColumnType("bigint") - .HasColumnName("deleted_by"); - - b.Property("DisplayName") - .HasColumnType("text") - .HasColumnName("display_name"); - - b.Property>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("LastActive") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_active"); - - b.Property("LastSidReroll") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_sid_reroll"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.PrimitiveCollection("Links") - .IsRequired() - .HasColumnType("text[]") - .HasColumnName("links"); - - b.Property("ListHidden") - .HasColumnType("boolean") - .HasColumnName("list_hidden"); - - b.Property("MemberTitle") - .HasColumnType("text") - .HasColumnName("member_title"); - - b.Property>("Names") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("names"); - - b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); - - b.Property>("Pronouns") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("pronouns"); - - b.Property("Role") - .HasColumnType("integer") - .HasColumnName("role"); - - b.Property("Settings") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("settings"); - - b.Property("Sid") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("sid") - .HasDefaultValueSql("find_free_user_sid()"); - - b.Property("Timezone") - .HasColumnType("text") - .HasColumnName("timezone"); - - b.Property("Username") - .IsRequired() - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("Id") - .HasName("pk_users"); - - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_users_legacy_id"); - - b.HasIndex("Sid") - .IsUnique() - .HasDatabaseName("ix_users_sid"); - - b.HasIndex("Username") - .IsUnique() - .HasDatabaseName("ix_users_username"); - - b.ToTable("users", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("PrideFlagId") - .HasColumnType("bigint") - .HasColumnName("pride_flag_id"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_user_flags"); - - b.HasIndex("PrideFlagId") - .HasDatabaseName("ix_user_flags_pride_flag_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_user_flags_user_id"); - - b.ToTable("user_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report") - .WithOne("AuditLogEntry") - .HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("fk_audit_log_reports_report_id"); - - b.Navigation("Report"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") - .WithMany() - .HasForeignKey("FediverseApplicationId") - .HasConstraintName("fk_auth_methods_fediverse_applications_fediverse_application_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("AuthMethods") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_auth_methods_users_user_id"); - - b.Navigation("FediverseApplication"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("DataExports") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_data_exports_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("Members") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_members_users_user_id"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Member", null) - .WithMany("ProfileFlags") - .HasForeignKey("MemberId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_member_flags_members_member_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") - .WithMany() - .HasForeignKey("PrideFlagId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_member_flags_pride_flags_pride_flag_id"); - - b.Navigation("PrideFlag"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_notices_users_author_id"); - - b.Navigation("Author"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Target") - .WithMany() - .HasForeignKey("TargetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_notifications_users_target_id"); - - b.Navigation("Target"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", null) - .WithMany("Flags") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_pride_flags_users_user_id"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter") - .WithMany() - .HasForeignKey("ReporterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_reports_users_reporter_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember") - .WithMany() - .HasForeignKey("TargetMemberId") - .HasConstraintName("fk_reports_members_target_member_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser") - .WithMany() - .HasForeignKey("TargetUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_reports_users_target_user_id"); - - b.Navigation("Reporter"); - - b.Navigation("TargetMember"); - - b.Navigation("TargetUser"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") - .WithMany() - .HasForeignKey("ApplicationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_tokens_applications_application_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany() - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_tokens_users_user_id"); - - b.Navigation("Application"); - - b.Navigation("User"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") - .WithMany() - .HasForeignKey("PrideFlagId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_user_flags_pride_flags_pride_flag_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", null) - .WithMany("ProfileFlags") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_user_flags_users_user_id"); - - b.Navigation("PrideFlag"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => - { - b.Navigation("ProfileFlags"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.Navigation("AuditLogEntry"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => - { - b.Navigation("AuthMethods"); - - b.Navigation("DataExports"); - - b.Navigation("Flags"); - - b.Navigation("Members"); - - b.Navigation("ProfileFlags"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs b/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs deleted file mode 100644 index ca88605..0000000 --- a/Foxnouns.Backend/Database/Migrations/20250410192220_AddAvatarMigrations.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Foxnouns.Backend.Database.Migrations -{ - /// - public partial class AddAvatarMigrations : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "avatar_migrated", - table: "users", - type: "boolean", - nullable: false, - defaultValue: false - ); - - migrationBuilder.AddColumn( - name: "avatar_migrated", - table: "members", - type: "boolean", - nullable: false, - defaultValue: false - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn(name: "avatar_migrated", table: "users"); - - migrationBuilder.DropColumn(name: "avatar_migrated", table: "members"); - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 92db9f9..d0cb607 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -1,7 +1,5 @@ // -using System.Collections.Generic; using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -19,10 +17,9 @@ namespace Foxnouns.Backend.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.2") + .HasAnnotation("ProductVersion", "8.0.5") .HasAnnotation("Relational:MaxIdentifierLength", 63); - NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => @@ -46,12 +43,12 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("name"); - b.PrimitiveCollection("RedirectUris") + b.Property("RedirectUris") .IsRequired() .HasColumnType("text[]") .HasColumnName("redirect_uris"); - b.PrimitiveCollection("Scopes") + b.Property("Scopes") .IsRequired() .HasColumnType("text[]") .HasColumnName("scopes"); @@ -62,63 +59,6 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("applications", (string)null); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.PrimitiveCollection("ClearedFields") - .HasColumnType("text[]") - .HasColumnName("cleared_fields"); - - b.Property("ModeratorId") - .HasColumnType("bigint") - .HasColumnName("moderator_id"); - - b.Property("ModeratorUsername") - .IsRequired() - .HasColumnType("text") - .HasColumnName("moderator_username"); - - b.Property("Reason") - .HasColumnType("text") - .HasColumnName("reason"); - - b.Property("ReportId") - .HasColumnType("bigint") - .HasColumnName("report_id"); - - b.Property("TargetMemberId") - .HasColumnType("bigint") - .HasColumnName("target_member_id"); - - b.Property("TargetMemberName") - .HasColumnType("text") - .HasColumnName("target_member_name"); - - b.Property("TargetUserId") - .HasColumnType("bigint") - .HasColumnName("target_user_id"); - - b.Property("TargetUsername") - .HasColumnType("text") - .HasColumnName("target_username"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_audit_log"); - - b.HasIndex("ReportId") - .IsUnique() - .HasDatabaseName("ix_audit_log_report_id"); - - b.ToTable("audit_log", (string)null); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => { b.Property("Id") @@ -155,47 +95,9 @@ namespace Foxnouns.Backend.Database.Migrations b.HasIndex("UserId") .HasDatabaseName("ix_auth_methods_user_id"); - b.HasIndex("AuthType", "RemoteId") - .IsUnique() - .HasDatabaseName("ix_auth_methods_auth_type_remote_id") - .HasFilter("fediverse_application_id IS NULL"); - - b.HasIndex("AuthType", "RemoteId", "FediverseApplicationId") - .IsUnique() - .HasDatabaseName("ix_auth_methods_auth_type_remote_id_fediverse_application_id") - .HasFilter("fediverse_application_id IS NOT NULL"); - b.ToTable("auth_methods", (string)null); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Filename") - .IsRequired() - .HasColumnType("text") - .HasColumnName("filename"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_data_exports"); - - b.HasIndex("Filename") - .IsUnique() - .HasDatabaseName("ix_data_exports_filename"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_data_exports_user_id"); - - b.ToTable("data_exports", (string)null); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.FediverseApplication", b => { b.Property("Id") @@ -217,10 +119,6 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("domain"); - b.Property("ForceRefresh") - .HasColumnType("boolean") - .HasColumnName("force_refresh"); - b.Property("InstanceType") .HasColumnType("integer") .HasColumnName("instance_type"); @@ -241,10 +139,6 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("avatar"); - b.Property("AvatarMigrated") - .HasColumnType("boolean") - .HasColumnName("avatar_migrated"); - b.Property("Bio") .HasColumnType("text") .HasColumnName("bio"); @@ -253,19 +147,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("display_name"); - b.Property>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.PrimitiveCollection("Links") + b.Property("Links") .IsRequired() .HasColumnType("text[]") .HasColumnName("links"); @@ -275,23 +157,6 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("name"); - b.Property>("Names") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("names"); - - b.Property>("Pronouns") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("pronouns"); - - b.Property("Sid") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("sid") - .HasDefaultValueSql("find_free_member_sid()"); - b.Property("Unlisted") .HasColumnType("boolean") .HasColumnName("unlisted"); @@ -303,14 +168,6 @@ namespace Foxnouns.Backend.Database.Migrations b.HasKey("Id") .HasName("pk_members"); - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_members_legacy_id"); - - b.HasIndex("Sid") - .IsUnique() - .HasDatabaseName("ix_members_sid"); - b.HasIndex("UserId", "Name") .IsUnique() .HasDatabaseName("ix_members_user_id_name"); @@ -318,203 +175,6 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("members", (string)null); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("MemberId") - .HasColumnType("bigint") - .HasColumnName("member_id"); - - b.Property("PrideFlagId") - .HasColumnType("bigint") - .HasColumnName("pride_flag_id"); - - b.HasKey("Id") - .HasName("pk_member_flags"); - - b.HasIndex("MemberId") - .HasDatabaseName("ix_member_flags_member_id"); - - b.HasIndex("PrideFlagId") - .HasDatabaseName("ix_member_flags_pride_flag_id"); - - b.ToTable("member_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AuthorId") - .HasColumnType("bigint") - .HasColumnName("author_id"); - - b.Property("EndTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("end_time"); - - b.Property("Message") - .IsRequired() - .HasColumnType("text") - .HasColumnName("message"); - - b.Property("StartTime") - .HasColumnType("timestamp with time zone") - .HasColumnName("start_time"); - - b.HasKey("Id") - .HasName("pk_notices"); - - b.HasIndex("AuthorId") - .HasDatabaseName("ix_notices_author_id"); - - b.ToTable("notices", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("AcknowledgedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("acknowledged_at"); - - b.Property("LocalizationKey") - .HasColumnType("text") - .HasColumnName("localization_key"); - - b.Property>("LocalizationParams") - .IsRequired() - .HasColumnType("hstore") - .HasColumnName("localization_params"); - - b.Property("Message") - .HasColumnType("text") - .HasColumnName("message"); - - b.Property("TargetId") - .HasColumnType("bigint") - .HasColumnName("target_id"); - - b.Property("Type") - .HasColumnType("integer") - .HasColumnName("type"); - - b.HasKey("Id") - .HasName("pk_notifications"); - - b.HasIndex("TargetId") - .HasDatabaseName("ix_notifications_target_id"); - - b.ToTable("notifications", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Description") - .HasColumnType("text") - .HasColumnName("description"); - - b.Property("Hash") - .HasColumnType("text") - .HasColumnName("hash"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text") - .HasColumnName("name"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_pride_flags"); - - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_pride_flags_legacy_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_pride_flags_user_id"); - - b.ToTable("pride_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.Property("Id") - .HasColumnType("bigint") - .HasColumnName("id"); - - b.Property("Context") - .HasColumnType("text") - .HasColumnName("context"); - - b.Property("Reason") - .HasColumnType("integer") - .HasColumnName("reason"); - - b.Property("ReporterId") - .HasColumnType("bigint") - .HasColumnName("reporter_id"); - - b.Property("Status") - .HasColumnType("integer") - .HasColumnName("status"); - - b.Property("TargetMemberId") - .HasColumnType("bigint") - .HasColumnName("target_member_id"); - - b.Property("TargetSnapshot") - .HasColumnType("text") - .HasColumnName("target_snapshot"); - - b.Property("TargetType") - .HasColumnType("integer") - .HasColumnName("target_type"); - - b.Property("TargetUserId") - .HasColumnType("bigint") - .HasColumnName("target_user_id"); - - b.HasKey("Id") - .HasName("pk_reports"); - - b.HasIndex("ReporterId") - .HasDatabaseName("ix_reports_reporter_id"); - - b.HasIndex("TargetMemberId") - .HasDatabaseName("ix_reports_target_member_id"); - - b.HasIndex("TargetUserId") - .HasDatabaseName("ix_reports_target_user_id"); - - b.ToTable("reports", (string)null); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => { b.Property("Id") @@ -538,7 +198,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("boolean") .HasColumnName("manually_expired"); - b.PrimitiveCollection("Scopes") + b.Property("Scopes") .IsRequired() .HasColumnType("text[]") .HasColumnName("scopes"); @@ -569,56 +229,15 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("avatar"); - b.Property("AvatarMigrated") - .HasColumnType("boolean") - .HasColumnName("avatar_migrated"); - b.Property("Bio") .HasColumnType("text") .HasColumnName("bio"); - b.Property>("CustomPreferences") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("custom_preferences"); - - b.Property("Deleted") - .HasColumnType("boolean") - .HasColumnName("deleted"); - - b.Property("DeletedAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("deleted_at"); - - b.Property("DeletedBy") - .HasColumnType("bigint") - .HasColumnName("deleted_by"); - b.Property("DisplayName") .HasColumnType("text") .HasColumnName("display_name"); - b.Property>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("LastActive") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_active"); - - b.Property("LastSidReroll") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_sid_reroll"); - - b.Property("LegacyId") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("legacy_id") - .HasDefaultValueSql("gen_random_uuid()"); - - b.PrimitiveCollection("Links") + b.Property("Links") .IsRequired() .HasColumnType("text[]") .HasColumnName("links"); @@ -631,40 +250,14 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("member_title"); - b.Property>("Names") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("names"); - b.Property("Password") .HasColumnType("text") .HasColumnName("password"); - b.Property>("Pronouns") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("pronouns"); - b.Property("Role") .HasColumnType("integer") .HasColumnName("role"); - b.Property("Settings") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("settings"); - - b.Property("Sid") - .IsRequired() - .ValueGeneratedOnAdd() - .HasColumnType("text") - .HasColumnName("sid") - .HasDefaultValueSql("find_free_user_sid()"); - - b.Property("Timezone") - .HasColumnType("text") - .HasColumnName("timezone"); - b.Property("Username") .IsRequired() .HasColumnType("text") @@ -673,14 +266,6 @@ namespace Foxnouns.Backend.Database.Migrations b.HasKey("Id") .HasName("pk_users"); - b.HasIndex("LegacyId") - .IsUnique() - .HasDatabaseName("ix_users_legacy_id"); - - b.HasIndex("Sid") - .IsUnique() - .HasDatabaseName("ix_users_sid"); - b.HasIndex("Username") .IsUnique() .HasDatabaseName("ix_users_username"); @@ -688,46 +273,6 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("users", (string)null); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("PrideFlagId") - .HasColumnType("bigint") - .HasColumnName("pride_flag_id"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_user_flags"); - - b.HasIndex("PrideFlagId") - .HasDatabaseName("ix_user_flags_pride_flag_id"); - - b.HasIndex("UserId") - .HasDatabaseName("ix_user_flags_user_id"); - - b.ToTable("user_flags", (string)null); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report") - .WithOne("AuditLogEntry") - .HasForeignKey("Foxnouns.Backend.Database.Models.AuditLogEntry", "ReportId") - .OnDelete(DeleteBehavior.SetNull) - .HasConstraintName("fk_audit_log_reports_report_id"); - - b.Navigation("Report"); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => { b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") @@ -747,18 +292,6 @@ namespace Foxnouns.Backend.Database.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.DataExport", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "User") - .WithMany("DataExports") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_data_exports_users_user_id"); - - b.Navigation("User"); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => { b.HasOne("Foxnouns.Backend.Database.Models.User", "User") @@ -768,90 +301,75 @@ namespace Foxnouns.Backend.Database.Migrations .IsRequired() .HasConstraintName("fk_members_users_user_id"); + b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Names", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => + { + b1.Property("MemberId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("MemberId"); + + b1.ToTable("members"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("MemberId") + .HasConstraintName("fk_members_members_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); + b.Navigation("User"); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.MemberFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.Member", null) - .WithMany("ProfileFlags") - .HasForeignKey("MemberId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_member_flags_members_member_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") - .WithMany() - .HasForeignKey("PrideFlagId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_member_flags_pride_flags_pride_flag_id"); - - b.Navigation("PrideFlag"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notice", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Author") - .WithMany() - .HasForeignKey("AuthorId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_notices_users_author_id"); - - b.Navigation("Author"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Notification", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Target") - .WithMany() - .HasForeignKey("TargetId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_notifications_users_target_id"); - - b.Navigation("Target"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.PrideFlag", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", null) - .WithMany("Flags") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_pride_flags_users_user_id"); - }); - - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.HasOne("Foxnouns.Backend.Database.Models.User", "Reporter") - .WithMany() - .HasForeignKey("ReporterId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_reports_users_reporter_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.Member", "TargetMember") - .WithMany() - .HasForeignKey("TargetMemberId") - .HasConstraintName("fk_reports_members_target_member_id"); - - b.HasOne("Foxnouns.Backend.Database.Models.User", "TargetUser") - .WithMany() - .HasForeignKey("TargetUserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_reports_users_target_user_id"); - - b.Navigation("Reporter"); - - b.Navigation("TargetMember"); - - b.Navigation("TargetUser"); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => { b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") @@ -873,46 +391,83 @@ namespace Foxnouns.Backend.Database.Migrations b.Navigation("User"); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.UserFlag", b => + modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => { - b.HasOne("Foxnouns.Backend.Database.Models.PrideFlag", "PrideFlag") - .WithMany() - .HasForeignKey("PrideFlagId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_user_flags_pride_flags_pride_flag_id"); + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); - b.HasOne("Foxnouns.Backend.Database.Models.User", null) - .WithMany("ProfileFlags") - .HasForeignKey("UserId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired() - .HasConstraintName("fk_user_flags_users_user_id"); + b1.Property("Capacity") + .HasColumnType("integer"); - b.Navigation("PrideFlag"); - }); + b1.HasKey("UserId") + .HasName("pk_users"); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Member", b => - { - b.Navigation("ProfileFlags"); - }); + b1.ToTable("users"); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => - { - b.Navigation("AuditLogEntry"); + b1.ToJson("fields"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("names"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + { + b1.Property("UserId") + .HasColumnType("bigint"); + + b1.Property("Capacity") + .HasColumnType("integer"); + + b1.HasKey("UserId") + .HasName("pk_users"); + + b1.ToTable("users"); + + b1.ToJson("pronouns"); + + b1.WithOwner() + .HasForeignKey("UserId") + .HasConstraintName("fk_users_users_user_id"); + }); + + b.Navigation("Fields") + .IsRequired(); + + b.Navigation("Names") + .IsRequired(); + + b.Navigation("Pronouns") + .IsRequired(); }); modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => { b.Navigation("AuthMethods"); - b.Navigation("DataExports"); - - b.Navigation("Flags"); - b.Navigation("Members"); - - b.Navigation("ProfileFlags"); }); #pragma warning restore 612, 618 } diff --git a/Foxnouns.Backend/Database/Models/Application.cs b/Foxnouns.Backend/Database/Models/Application.cs index e10d1c8..95416f1 100644 --- a/Foxnouns.Backend/Database/Models/Application.cs +++ b/Foxnouns.Backend/Database/Models/Application.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using System.Security.Cryptography; using Foxnouns.Backend.Utils; @@ -23,32 +9,22 @@ public class Application : BaseModel public required string ClientSecret { get; init; } public required string Name { get; init; } public required string[] Scopes { get; init; } - public required string[] RedirectUris { get; init; } + public required string[] RedirectUris { get; set; } - public static Application Create( - ISnowflakeGenerator snowflakeGenerator, - string name, - string[] scopes, - string[] redirectUrls - ) + public static Application Create(ISnowflakeGenerator snowflakeGenerator, string name, string[] scopes, + string[] redirectUrls) { - string clientId = RandomNumberGenerator.GetHexString(32, true); - string clientSecret = AuthUtils.RandomToken(); + var clientId = RandomNumberGenerator.GetHexString(32, true); + var clientSecret = OauthUtils.RandomToken(); - if (scopes.Except(AuthUtils.ApplicationScopes).Any()) + if (scopes.Except(OauthUtils.ApplicationScopes).Any()) { - throw new ArgumentException( - "Invalid scopes passed to Application.Create", - nameof(scopes) - ); + throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes)); } - if (redirectUrls.Any(s => !AuthUtils.ValidateRedirectUri(s))) + if (redirectUrls.Any(s => !OauthUtils.ValidateRedirectUri(s))) { - throw new ArgumentException( - "Invalid redirect URLs passed to Application.Create", - nameof(redirectUrls) - ); + throw new ArgumentException("Invalid redirect URLs passed to Application.Create", nameof(redirectUrls)); } return new Application @@ -58,7 +34,7 @@ public class Application : BaseModel ClientSecret = clientSecret, Name = name, Scopes = scopes, - RedirectUris = redirectUrls, + RedirectUris = redirectUrls }; } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/AuditLogEntry.cs b/Foxnouns.Backend/Database/Models/AuditLogEntry.cs deleted file mode 100644 index 84e1a43..0000000 --- a/Foxnouns.Backend/Database/Models/AuditLogEntry.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.ComponentModel.DataAnnotations.Schema; -using Foxnouns.Backend.Utils; -using Newtonsoft.Json; - -namespace Foxnouns.Backend.Database.Models; - -public class AuditLogEntry : BaseModel -{ - public Snowflake ModeratorId { get; init; } - public string ModeratorUsername { get; init; } = string.Empty; - public Snowflake? TargetUserId { get; init; } - public string? TargetUsername { get; init; } - public Snowflake? TargetMemberId { get; init; } - public string? TargetMemberName { get; init; } - public Snowflake? ReportId { get; init; } - public Report? Report { get; init; } - - public AuditLogEntryType Type { get; init; } - public string? Reason { get; init; } - public string[]? ClearedFields { get; init; } -} - -[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] -public enum AuditLogEntryType -{ - IgnoreReport, - WarnUser, - WarnUserAndClearProfile, - SuspendUser, - QuerySensitiveUserData, -} diff --git a/Foxnouns.Backend/Database/Models/AuthMethod.cs b/Foxnouns.Backend/Database/Models/AuthMethod.cs index 07cfb79..3b6de20 100644 --- a/Foxnouns.Backend/Database/Models/AuthMethod.cs +++ b/Foxnouns.Backend/Database/Models/AuthMethod.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . namespace Foxnouns.Backend.Database.Models; public class AuthMethod : BaseModel @@ -34,4 +20,4 @@ public enum AuthType Tumblr, Fediverse, Email, -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/DataExport.cs b/Foxnouns.Backend/Database/Models/DataExport.cs deleted file mode 100644 index 464a54a..0000000 --- a/Foxnouns.Backend/Database/Models/DataExport.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using NodaTime; - -namespace Foxnouns.Backend.Database.Models; - -public class DataExport : BaseModel -{ - public Snowflake UserId { get; init; } - public User User { get; init; } = null!; - public required string Filename { get; init; } - - public static readonly Duration Expiration = Duration.FromDays(15); -} diff --git a/Foxnouns.Backend/Database/Models/FediverseApplication.cs b/Foxnouns.Backend/Database/Models/FediverseApplication.cs index 9c61937..8da8b29 100644 --- a/Foxnouns.Backend/Database/Models/FediverseApplication.cs +++ b/Foxnouns.Backend/Database/Models/FediverseApplication.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . namespace Foxnouns.Backend.Database.Models; public class FediverseApplication : BaseModel @@ -20,11 +6,10 @@ public class FediverseApplication : BaseModel public required string ClientId { get; set; } public required string ClientSecret { get; set; } public required FediverseInstanceType InstanceType { get; set; } - public bool ForceRefresh { get; set; } } public enum FediverseInstanceType { MastodonApi, - MisskeyApi, -} + MisskeyApi +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/Field.cs b/Foxnouns.Backend/Database/Models/Field.cs index 7c44b2f..c123e5b 100644 --- a/Foxnouns.Backend/Database/Models/Field.cs +++ b/Foxnouns.Backend/Database/Models/Field.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . namespace Foxnouns.Backend.Database.Models; public class Field @@ -29,4 +15,4 @@ public class FieldEntry public class Pronoun : FieldEntry { public string? DisplayText { get; set; } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/Member.cs b/Foxnouns.Backend/Database/Models/Member.cs index 85b39f3..cd0d9cd 100644 --- a/Foxnouns.Backend/Database/Models/Member.cs +++ b/Foxnouns.Backend/Database/Models/Member.cs @@ -1,24 +1,8 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . namespace Foxnouns.Backend.Database.Models; public class Member : BaseModel { public required string Name { get; set; } - public string Sid { get; set; } = string.Empty; - public required string LegacyId { get; init; } public string? DisplayName { get; set; } public string? Bio { get; set; } public string? Avatar { get; set; } @@ -29,11 +13,6 @@ public class Member : BaseModel public List Pronouns { get; set; } = []; public List Fields { get; set; } = []; - // Only used by avatar-proxy and avatar-migration. - public bool AvatarMigrated { get; set; } = true; - - public List ProfileFlags { get; set; } = []; - public Snowflake UserId { get; init; } public User User { get; init; } = null!; -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/Notice.cs b/Foxnouns.Backend/Database/Models/Notice.cs deleted file mode 100644 index c3e6f0d..0000000 --- a/Foxnouns.Backend/Database/Models/Notice.cs +++ /dev/null @@ -1,13 +0,0 @@ -using NodaTime; - -namespace Foxnouns.Backend.Database.Models; - -public class Notice : BaseModel -{ - public required string Message { get; set; } - public required Instant StartTime { get; set; } - public required Instant EndTime { get; set; } - - public Snowflake AuthorId { get; init; } - public User Author { get; init; } = null!; -} diff --git a/Foxnouns.Backend/Database/Models/Notification.cs b/Foxnouns.Backend/Database/Models/Notification.cs deleted file mode 100644 index 59bf1c3..0000000 --- a/Foxnouns.Backend/Database/Models/Notification.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Utils; -using Newtonsoft.Json; -using NodaTime; - -namespace Foxnouns.Backend.Database.Models; - -public class Notification : BaseModel -{ - public Snowflake TargetId { get; init; } - public User Target { get; init; } = null!; - - public NotificationType Type { get; init; } - - public string? Message { get; init; } - public string? LocalizationKey { get; init; } - public Dictionary LocalizationParams { get; init; } = []; - - public Instant? AcknowledgedAt { get; set; } -} - -[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] -public enum NotificationType -{ - Notice, - Warning, - Suspension, -} diff --git a/Foxnouns.Backend/Database/Models/PrideFlag.cs b/Foxnouns.Backend/Database/Models/PrideFlag.cs deleted file mode 100644 index 0c04ab5..0000000 --- a/Foxnouns.Backend/Database/Models/PrideFlag.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -namespace Foxnouns.Backend.Database.Models; - -public class PrideFlag : BaseModel -{ - public required Snowflake UserId { get; init; } - public required string LegacyId { get; init; } - - // A null hash means the flag hasn't been processed yet. - public string? Hash { get; set; } - public required string Name { get; set; } - public string? Description { get; set; } -} - -public class UserFlag -{ - public long Id { get; init; } - public required Snowflake UserId { get; init; } - public required Snowflake PrideFlagId { get; init; } - public PrideFlag PrideFlag { get; init; } = null!; -} - -public class MemberFlag -{ - public long Id { get; init; } - public required Snowflake MemberId { get; init; } - public required Snowflake PrideFlagId { get; init; } - public PrideFlag PrideFlag { get; init; } = null!; -} diff --git a/Foxnouns.Backend/Database/Models/Report.cs b/Foxnouns.Backend/Database/Models/Report.cs deleted file mode 100644 index 47b994f..0000000 --- a/Foxnouns.Backend/Database/Models/Report.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Utils; -using Newtonsoft.Json; - -namespace Foxnouns.Backend.Database.Models; - -public class Report : BaseModel -{ - public Snowflake ReporterId { get; init; } - public User Reporter { get; init; } = null!; - public Snowflake TargetUserId { get; init; } - public User TargetUser { get; init; } = null!; - - public Snowflake? TargetMemberId { get; init; } - public Member? TargetMember { get; init; } - - public ReportStatus Status { get; set; } - public ReportReason Reason { get; init; } - public string? Context { get; init; } - - public ReportTargetType TargetType { get; init; } - public string? TargetSnapshot { get; init; } - - public AuditLogEntry? AuditLogEntry { get; set; } -} - -[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] -public enum ReportTargetType -{ - User, - Member, -} - -[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] -public enum ReportStatus -{ - Open, - Closed, -} - -[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] -public enum ReportReason -{ - Totalitarianism, - HateSpeech, - Racism, - Homophobia, - Transphobia, - Queerphobia, - Exclusionism, - Sexism, - Ableism, - ChildPornography, - PedophiliaAdvocacy, - Harassment, - Impersonation, - Doxxing, - EncouragingSelfHarm, - Spam, - Trolling, - Advertisement, - CopyrightViolation, -} diff --git a/Foxnouns.Backend/Database/Models/Token.cs b/Foxnouns.Backend/Database/Models/Token.cs index ba9b016..1078cf1 100644 --- a/Foxnouns.Backend/Database/Models/Token.cs +++ b/Foxnouns.Backend/Database/Models/Token.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using NodaTime; namespace Foxnouns.Backend.Database.Models; @@ -28,4 +14,4 @@ public class Token : BaseModel public Snowflake ApplicationId { get; set; } public Application Application { get; set; } = null!; -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index fe97b6c..238f306 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -1,84 +1,24 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable UnusedAutoPropertyAccessor.Global -using System.ComponentModel.DataAnnotations.Schema; -using Foxnouns.Backend.Utils; -using Newtonsoft.Json; -using NodaTime; - namespace Foxnouns.Backend.Database.Models; public class User : BaseModel { public required string Username { get; set; } - public string Sid { get; set; } = string.Empty; - public required string LegacyId { get; init; } public string? DisplayName { get; set; } public string? Bio { get; set; } public string? MemberTitle { get; set; } public string? Avatar { get; set; } public string[] Links { get; set; } = []; public bool ListHidden { get; set; } - public string? Timezone { get; set; } public List Names { get; set; } = []; public List Pronouns { get; set; } = []; public List Fields { get; set; } = []; - public Dictionary CustomPreferences { get; set; } = []; - - public List Flags { get; set; } = []; - public List ProfileFlags { get; set; } = []; public UserRole Role { get; set; } = UserRole.User; public string? Password { get; set; } // Password may be null if the user doesn't authenticate with an email address public List Members { get; } = []; public List AuthMethods { get; } = []; - public List DataExports { get; } = []; - public UserSettings Settings { get; set; } = new(); - - public required Instant LastActive { get; set; } - public Instant LastSidReroll { get; set; } - - public bool Deleted { get; set; } - public Instant? DeletedAt { get; set; } - public Snowflake? DeletedBy { get; set; } - - // Only used by avatar-proxy and avatar-migration. - public bool AvatarMigrated { get; set; } = true; - - [NotMapped] - public bool? SelfDelete => Deleted ? DeletedBy != null : null; - - public class CustomPreference - { - public required string Icon { get; set; } - public required string Tooltip { get; set; } - public bool Muted { get; set; } - public bool Favourite { get; set; } - - // This type is generally serialized directly, so the converter is applied here. - [JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] - public PreferenceSize Size { get; set; } - - public Guid LegacyId { get; init; } = Guid.NewGuid(); - } - - public static readonly Duration DeleteAfter = Duration.FromDays(30); - public static readonly Duration DeleteSuspendedAfter = Duration.FromDays(180); } public enum UserRole @@ -86,17 +26,4 @@ public enum UserRole User, Moderator, Admin, -} - -public enum PreferenceSize -{ - Large, - Normal, - Small, -} - -public class UserSettings -{ - public bool? DarkMode { get; set; } - public Snowflake? LastReadNotice { get; set; } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index a0f127a..feaf27b 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -1,32 +1,12 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.ComponentModel; using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; using NodaTime; -using JsonSerializer = Newtonsoft.Json.JsonSerializer; namespace Foxnouns.Backend.Database; [JsonConverter(typeof(JsonConverter))] -[System.Text.Json.Serialization.JsonConverter(typeof(SystemJsonConverter))] -[TypeConverter(typeof(TypeConverter))] -public readonly struct Snowflake(ulong value) : IEquatable +public readonly struct Snowflake(ulong value) { public const long Epoch = 1_640_995_200_000; // 2022-01-01 at 00:00:00 UTC public readonly ulong Value = value; @@ -57,102 +37,47 @@ public readonly struct Snowflake(ulong value) : IEquatable public short Increment => (short)(Value & 0xFFF); public static bool operator <(Snowflake arg1, Snowflake arg2) => arg1.Value < arg2.Value; - public static bool operator >(Snowflake arg1, Snowflake arg2) => arg1.Value > arg2.Value; - public static bool operator ==(Snowflake arg1, Snowflake arg2) => arg1.Value == arg2.Value; - public static bool operator !=(Snowflake arg1, Snowflake arg2) => arg1.Value != arg2.Value; public static implicit operator ulong(Snowflake s) => s.Value; - public static implicit operator long(Snowflake s) => (long)s.Value; - public static implicit operator Snowflake(ulong n) => new(n); - public static implicit operator Snowflake(long n) => new((ulong)n); public static bool TryParse(string input, [NotNullWhen(true)] out Snowflake? snowflake) { snowflake = null; - if (!ulong.TryParse(input, out ulong res)) - return false; + if (!ulong.TryParse(input, out var res)) return false; snowflake = new Snowflake(res); return true; } - public static Snowflake FromInstant(Instant instant) => - new((ulong)(instant.ToUnixTimeMilliseconds() - Epoch) << 22); - public override bool Equals(object? obj) => obj is Snowflake other && Value == other.Value; - - public bool Equals(Snowflake other) => Value == other.Value; - public override int GetHashCode() => Value.GetHashCode(); - public override string ToString() => Value.ToString(); - /// /// An Entity Framework ValueConverter for Snowflakes to longs. /// // ReSharper disable once ClassNeverInstantiated.Global - public class ValueConverter() : ValueConverter(x => x, x => x); + public class ValueConverter() : ValueConverter( + convertToProviderExpression: x => x, + convertFromProviderExpression: x => x + ); - private class SystemJsonConverter : System.Text.Json.Serialization.JsonConverter + private class JsonConverter : JsonConverter { - public override Snowflake Read( - ref Utf8JsonReader reader, - Type typeToConvert, - JsonSerializerOptions options - ) => ulong.Parse(reader.GetString()!); - - public override void Write( - Utf8JsonWriter writer, - Snowflake value, - JsonSerializerOptions options - ) => writer.WriteStringValue(value.Value.ToString()); - } - - private class JsonConverter : JsonConverter - { - public override void WriteJson( - JsonWriter writer, - Snowflake? value, - JsonSerializer serializer - ) + public override void WriteJson(JsonWriter writer, Snowflake value, JsonSerializer serializer) { - if (value != null) - writer.WriteValue(value.Value.ToString()); - else - writer.WriteNull(); + writer.WriteValue(value.Value.ToString()); } - public override Snowflake? ReadJson( - JsonReader reader, - Type objectType, - Snowflake? existingValue, + public override Snowflake ReadJson(JsonReader reader, Type objectType, Snowflake existingValue, bool hasExistingValue, - JsonSerializer serializer - ) => - reader.TokenType is not (JsonToken.None or JsonToken.Null) - ? ulong.Parse((string)reader.Value!) - : null; + JsonSerializer serializer) + { + return ulong.Parse((string)reader.Value!); + } } - - private class TypeConverter : System.ComponentModel.TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => - sourceType == typeof(string); - - public override bool CanConvertTo( - ITypeDescriptorContext? context, - [NotNullWhen(true)] Type? destinationType - ) => destinationType == typeof(Snowflake); - - public override object? ConvertFrom( - ITypeDescriptorContext? context, - CultureInfo? culture, - object value - ) => TryParse((string)value, out Snowflake? snowflake) ? snowflake : null; - } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/SnowflakeGenerator.cs b/Foxnouns.Backend/Database/SnowflakeGenerator.cs index fd188f5..e070e20 100644 --- a/Foxnouns.Backend/Database/SnowflakeGenerator.cs +++ b/Foxnouns.Backend/Database/SnowflakeGenerator.cs @@ -1,17 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using NodaTime; namespace Foxnouns.Backend.Database; @@ -42,21 +28,18 @@ public class SnowflakeGenerator : ISnowflakeGenerator public Snowflake GenerateSnowflake(Instant? time = null) { time ??= SystemClock.Instance.GetCurrentInstant(); - long increment = Interlocked.Increment(ref _increment); - int threadId = Environment.CurrentManagedThreadId % 32; - long timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch; + var increment = Interlocked.Increment(ref _increment); + var threadId = Environment.CurrentManagedThreadId % 32; + var timestamp = time.Value.ToUnixTimeMilliseconds() - Snowflake.Epoch; - return (timestamp << 22) - | (uint)(_processId << 17) - | (uint)(threadId << 12) - | (increment % 4096); + return (timestamp << 22) | (uint)(_processId << 17) | (uint)(threadId << 12) | (increment % 4096); } } public static class SnowflakeGeneratorServiceExtensions { - public static IServiceCollection AddSnowflakeGenerator( - this IServiceCollection services, - int? processId = null - ) => services.AddSingleton(new SnowflakeGenerator(processId)); -} + public static IServiceCollection AddSnowflakeGenerator(this IServiceCollection services, int? processId = null) + { + return services.AddSingleton(new SnowflakeGenerator(processId)); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/prune-designer-cs-files.sh b/Foxnouns.Backend/Database/prune-designer-cs-files.sh deleted file mode 100644 index 41b96cc..0000000 --- a/Foxnouns.Backend/Database/prune-designer-cs-files.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/bash -set -e - -# Original script by zotan for Iceshrimp.NET -# Source: https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/commit/7c93dcf79dda54fc1a4ea9772e3f80874e6bcefb/Iceshrimp.Backend/Core/Database/prune-designer-cs-files.sh - -if [[ $(uname) == "Darwin" ]]; then - SED="gsed" -else - SED="sed" -fi - -import="using Microsoft.EntityFrameworkCore.Infrastructure;" -dbc=" [DbContext(typeof(DatabaseContext))]" - -for file in $(find "$(dirname $0)/Migrations" -name '*.Designer.cs'); do - echo "$file" - csfile="${file%.Designer.cs}.cs" - if [[ ! -f $csfile ]]; then - echo "$csfile doesn't exist, exiting" - exit 1 - fi - lineno=$($SED -n '/^{/=' "$csfile") - ((lineno+=2)) - migr=$(grep "\[Migration" "$file") - $SED -i "${lineno}i \\$migr" "$csfile" - $SED -i "${lineno}i \\$dbc" "$csfile" - $SED -i "2i $import" "$csfile" - rm "$file" -done diff --git a/Foxnouns.Backend/Dto/Auth.cs b/Foxnouns.Backend/Dto/Auth.cs deleted file mode 100644 index fbf5951..0000000 --- a/Foxnouns.Backend/Dto/Auth.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// 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 EmailForgotPasswordRequest(string Email); - -public record EmailResetPasswordRequest(string State, string Password); - -public record FediverseCallbackRequest(string Instance, string Code, string? State = null); diff --git a/Foxnouns.Backend/Dto/DataExport.cs b/Foxnouns.Backend/Dto/DataExport.cs deleted file mode 100644 index 9fc0a7d..0000000 --- a/Foxnouns.Backend/Dto/DataExport.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// 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 203442b..0000000 --- a/Foxnouns.Backend/Dto/Flag.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable NotAccessedPositionalProperty.Global -// ReSharper disable UnusedAutoPropertyAccessor.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 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 eecfb3b..0000000 --- a/Foxnouns.Backend/Dto/Internal.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// 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 4fcc147..0000000 --- a/Foxnouns.Backend/Dto/Member.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable NotAccessedPositionalProperty.Global -// ReSharper disable UnusedAutoPropertyAccessor.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 168327a..0000000 --- a/Foxnouns.Backend/Dto/Meta.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable NotAccessedPositionalProperty.Global -using Foxnouns.Backend.Database; - -namespace Foxnouns.Backend.Dto; - -public record MetaResponse( - string Repository, - string Version, - string Hash, - int Members, - UserInfoResponse Users, - LimitsResponse Limits, - MetaNoticeResponse? Notice -); - -public record MetaNoticeResponse(Snowflake Id, string Message); - -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/Moderation.cs b/Foxnouns.Backend/Dto/Moderation.cs deleted file mode 100644 index bcc7e8e..0000000 --- a/Foxnouns.Backend/Dto/Moderation.cs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable NotAccessedPositionalProperty.Global -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using NodaTime; - -namespace Foxnouns.Backend.Dto; - -public record ReportResponse( - Snowflake Id, - PartialUser Reporter, - PartialUser TargetUser, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - PartialMember? TargetMember, - ReportStatus Status, - ReportReason Reason, - string? Context, - ReportTargetType TargetType, - JObject? Snapshot -); - -public record ReportDetailResponse( - ReportResponse Report, - UserResponse User, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] MemberResponse? Member, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - AuditLogResponse? AuditLogEntry -); - -public record AuditLogResponse( - Snowflake Id, - AuditLogEntity Moderator, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - AuditLogEntity? TargetUser, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - AuditLogEntity? TargetMember, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] PartialReport? Report, - AuditLogEntryType Type, - string? Reason, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string[]? ClearedFields -); - -public record PartialReport( - Snowflake Id, - Snowflake ReporterId, - Snowflake TargetUserId, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - Snowflake? TargetMemberId, - ReportReason Reason, - string? Context, - ReportTargetType TargetType -); - -public record NotificationResponse( - Snowflake Id, - NotificationType Type, - string? Message, - string? LocalizationKey, - Dictionary LocalizationParams, - bool Acknowledged -); - -public record AuditLogEntity(Snowflake Id, string Username); - -public record CreateReportRequest(ReportReason Reason, string? Context = null); - -public record IgnoreReportRequest(string? Reason = null); - -public class WarnUserRequest -{ - public required string Reason { get; init; } - public FieldsToClear[]? ClearFields { get; init; } - public Snowflake? MemberId { get; init; } - public Snowflake? ReportId { get; init; } -} - -public record SuspendUserRequest(string Reason, bool ClearProfile, Snowflake? ReportId = null); - -[JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] -public enum FieldsToClear -{ - DisplayName, - Avatar, - Bio, - Links, - Names, - Pronouns, - Fields, - Flags, - CustomPreferences, -} - -public record QueryUsersRequest(string Query, bool Fuzzy); - -public record QueryUserResponse( - UserResponse User, - bool MemberListHidden, - Instant LastActive, - Instant LastSidReroll, - bool Suspended, - bool Deleted, - bool ShowSensitiveData, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - IEnumerable? AuthMethods -); - -public record QuerySensitiveUserDataRequest(string Reason); - -public record NoticeResponse( - Snowflake Id, - string Message, - Instant StartTime, - Instant EndTime, - PartialUser Author -); - -public record CreateNoticeRequest(string Message, Instant? StartTime, Instant EndTime); diff --git a/Foxnouns.Backend/Dto/User.cs b/Foxnouns.Backend/Dto/User.cs deleted file mode 100644 index 2ae38f1..0000000 --- a/Foxnouns.Backend/Dto/User.cs +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable NotAccessedPositionalProperty.Global -// ReSharper disable ClassNeverInstantiated.Global -// ReSharper disable UnusedAutoPropertyAccessor.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, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Suspended, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] UserSettings? Settings -); - -public record CustomPreferenceResponse( - string Icon, - string Tooltip, - bool Muted, - bool Favourite, - [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] PreferenceSize Size -); - -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 Snowflake? LastReadNotice { 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/Dto/V1/Member.cs b/Foxnouns.Backend/Dto/V1/Member.cs deleted file mode 100644 index c745187..0000000 --- a/Foxnouns.Backend/Dto/V1/Member.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable NotAccessedPositionalProperty.Global -using Foxnouns.Backend.Database; -using Newtonsoft.Json; - -namespace Foxnouns.Backend.Dto.V1; - -public record PartialMember( - string Id, - Snowflake IdNew, - string Sid, - string Name, - string? DisplayName, - string? Bio, - string? Avatar, - string[] Links, - FieldEntry[] Names, - PronounEntry[] Pronouns -); - -public record MemberResponse( - string Id, - Snowflake IdNew, - string Sid, - string Name, - string? DisplayName, - string? Bio, - string? Avatar, - string[] Links, - FieldEntry[] Names, - PronounEntry[] Pronouns, - ProfileField[] Fields, - PrideFlag[] Flags, - PartialUser User, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted -); - -public record PartialUser( - string Id, - Snowflake IdNew, - string Name, - string? DisplayName, - string? Avatar, - Dictionary CustomPreferences -); diff --git a/Foxnouns.Backend/Dto/V1/User.cs b/Foxnouns.Backend/Dto/V1/User.cs deleted file mode 100644 index e80a355..0000000 --- a/Foxnouns.Backend/Dto/V1/User.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -// ReSharper disable NotAccessedPositionalProperty.Global -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Services.V1; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; -using NodaTime; - -namespace Foxnouns.Backend.Dto.V1; - -public record UserResponse( - string Id, - Snowflake IdNew, - string Sid, - string Name, - string? DisplayName, - string? Bio, - string? MemberTitle, - string? Avatar, - string[] Links, - FieldEntry[] Names, - PronounEntry[] Pronouns, - ProfileField[] Fields, - PrideFlag[] Flags, - PartialMember[] Members, - int? UtcOffset, - Dictionary CustomPreferences -); - -public record CurrentUserResponse( - string Id, - Snowflake IdNew, - string Sid, - string Name, - string? DisplayName, - string? Bio, - string? MemberTitle, - string? Avatar, - string[] Links, - FieldEntry[] Names, - PronounEntry[] Pronouns, - ProfileField[] Fields, - PrideFlag[] Flags, - PartialMember[] Members, - int? UtcOffset, - Dictionary CustomPreferences, - Instant CreatedAt, - string? Timezone, - bool IsAdmin, - bool ListPrivate, - Instant LastSidReroll, - string? Discord, - string? DiscordUsername, - string? Google, - string? GoogleUsername, - string? Tumblr, - string? TumblrUsername, - string? Fediverse, - string? FediverseUsername, - string? FediverseInstance -); - -public record CustomPreference( - string Icon, - string Tooltip, - [property: JsonConverter(typeof(StringEnumConverter), typeof(SnakeCaseNamingStrategy))] - PreferenceSize Size, - bool Muted, - bool Favourite -); - -public record ProfileField(string Name, FieldEntry[] Entries) -{ - public static ProfileField FromField( - Field field, - Dictionary customPreferences - ) => new(field.Name, FieldEntry.FromEntries(field.Entries, customPreferences)); - - public static ProfileField[] FromFields( - IEnumerable fields, - Dictionary customPreferences - ) => fields.Select(f => FromField(f, customPreferences)).ToArray(); -} - -public record FieldEntry(string Value, string Status) -{ - public static FieldEntry[] FromEntries( - IEnumerable entries, - Dictionary customPreferences - ) => - entries - .Select(e => new FieldEntry( - e.Value, - V1Utils.TranslateStatus(e.Status, customPreferences) - )) - .ToArray(); -} - -public record PronounEntry(string Pronouns, string? DisplayText, string Status) -{ - public static PronounEntry[] FromPronouns( - IEnumerable pronouns, - Dictionary customPreferences - ) => - pronouns - .Select(p => new PronounEntry( - p.Value, - p.DisplayText, - V1Utils.TranslateStatus(p.Status, customPreferences) - )) - .ToArray(); -} - -public record PrideFlag(string Id, Snowflake IdNew, string Hash, string Name, string? Description); diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 647688b..d05571b 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -1,20 +1,6 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using System.Net; +using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc.ModelBinding; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Foxnouns.Backend; @@ -23,122 +9,50 @@ public class FoxnounsError(string message, Exception? inner = null) : Exception( { public Exception? Inner => inner; - public class DatabaseError(string message, Exception? inner = null) - : FoxnounsError(message, inner); + public class DatabaseError(string message, Exception? inner = null) : FoxnounsError(message, inner); public class UnknownEntityError(Type entityType, Exception? inner = null) : DatabaseError($"Entity of type {entityType.Name} not found", inner); - - public class RemoteAuthError(string message, string? errorBody = null, Exception? inner = null) - : FoxnounsError(message, inner) - { - public string? ErrorBody => errorBody; - - public override string ToString() => - $"{Message}: {ErrorBody} {(Inner != null ? $"({Inner})" : "")}"; - } } -public class ApiError( - string message, - HttpStatusCode? statusCode = null, - ErrorCode? errorCode = null -) : FoxnounsError(message) +public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCode? errorCode = null) + : FoxnounsError(message) { public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError; public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError; - public class Unauthorized(string message, ErrorCode errorCode = ErrorCode.AuthenticationError) - : ApiError(message, HttpStatusCode.Unauthorized, errorCode); + public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized); - public class Forbidden( - string message, - IEnumerable? scopes = null, - ErrorCode errorCode = ErrorCode.Forbidden - ) : ApiError(message, HttpStatusCode.Forbidden, errorCode) + public class Forbidden(string message, IEnumerable? scopes = null) + : ApiError(message, statusCode: HttpStatusCode.Forbidden) { public readonly string[] Scopes = scopes?.ToArray() ?? []; } - public class BadRequest( - string message, - IReadOnlyDictionary>? errors = null - ) : ApiError(message, HttpStatusCode.BadRequest) - { - public BadRequest(string message, string field, object? actualValue) - : this( - "Error validating input", - new Dictionary> - { - { field, [ValidationError.GenericValidationError(message, actualValue)] }, - } - ) { } - - public JObject ToJson() - { - var o = new JObject - { - { "status", (int)HttpStatusCode.BadRequest }, - { "message", Message }, - { "code", "BAD_REQUEST" }, - }; - if (errors == null) - return o; - - var a = new JArray(); - foreach (KeyValuePair> error in errors) - { - var errorObj = new JObject - { - { "key", error.Key }, - { "errors", JArray.FromObject(error.Value) }, - }; - a.Add(errorObj); - } - - o.Add("errors", a); - return o; - } - } - - /// - /// A special version of BadRequest that ASP.NET generates when it encounters an invalid request. - /// Any other methods should use instead. - /// - public class AspBadRequest(string message, ModelStateDictionary? modelState = null) - : ApiError(message, HttpStatusCode.BadRequest) + public class BadRequest(string message, ModelStateDictionary? modelState = null) + : ApiError(message, statusCode: HttpStatusCode.BadRequest) { public JObject ToJson() { var o = new JObject { { "status", (int)HttpStatusCode.BadRequest }, - { "message", Message }, - { "code", "BAD_REQUEST" }, + { "code", ErrorCode.BadRequest.ToString() } }; - if (modelState == null) - return o; + if (modelState == null) return o; var a = new JArray(); - foreach ( - KeyValuePair error in modelState.Where(e => - e.Value is { Errors.Count: > 0 } - ) - ) + foreach (var error in modelState.Where(e => e.Value is { Errors.Count: > 0 })) { var errorObj = new JObject { { "key", error.Key }, { "errors", - new JArray( - error.Value!.Errors.Select(e => new JObject - { - { "message", e.ErrorMessage }, - }) - ) - }, + new JArray(error.Value!.Errors.Select(e => new JObject { { "message", e.ErrorMessage } })) + } }; + a.Add(errorObj); } @@ -148,9 +62,9 @@ public class ApiError( } public class NotFound(string message, ErrorCode? code = null) - : ApiError(message, HttpStatusCode.NotFound, code); + : ApiError(message, statusCode: HttpStatusCode.NotFound, errorCode: code); - public class AuthenticationError(string message) : ApiError(message, HttpStatusCode.BadRequest); + public class AuthenticationError(string message) : ApiError(message, statusCode: HttpStatusCode.BadRequest); } public enum ErrorCode @@ -159,63 +73,7 @@ public enum ErrorCode Forbidden, BadRequest, AuthenticationError, - AuthenticationRequired, - MissingScopes, GenericApiError, UserNotFound, MemberNotFound, - PageNotFound, - AccountAlreadyLinked, - LastAuthMethod, - InvalidReportTarget, - InvalidWarningTarget, -} - -public class ValidationError -{ - public required string Message { get; init; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public int? MinLength { get; init; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public int? MaxLength { get; init; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public int? ActualLength { get; init; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable? AllowedValues { get; init; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public object? ActualValue { get; init; } - - public static ValidationError LengthError( - string message, - int minLength, - int maxLength, - int actualLength - ) => - new() - { - Message = message, - MinLength = minLength, - MaxLength = maxLength, - ActualLength = actualLength, - }; - - public static ValidationError DisallowedValueError( - string message, - IEnumerable allowedValues, - object actualValue - ) => - new() - { - Message = message, - AllowedValues = allowedValues, - ActualValue = actualValue, - }; - - public static ValidationError GenericValidationError(string message, object? actualValue) => - new() { Message = message, ActualValue = actualValue }; -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs deleted file mode 100644 index 2d3108b..0000000 --- a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Security.Cryptography; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Jobs; -using Foxnouns.Backend.Services; -using Foxnouns.Backend.Utils; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Transforms; - -namespace Foxnouns.Backend.Extensions; - -public static class ImageObjectExtensions -{ - private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"]; - - public static async Task DeleteMemberAvatarAsync( - this ObjectStorageService objectStorageService, - Snowflake id, - string hash, - CancellationToken ct = default - ) => await objectStorageService.RemoveObjectAsync(MemberAvatarUpdateJob.Path(id, hash), ct); - - public static async Task DeleteUserAvatarAsync( - this ObjectStorageService objectStorageService, - Snowflake id, - string hash, - CancellationToken ct = default - ) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateJob.Path(id, hash), ct); - - public static async Task DeleteFlagAsync( - this ObjectStorageService objectStorageService, - string hash, - CancellationToken ct = default - ) => await objectStorageService.RemoveObjectAsync(CreateFlagJob.Path(hash), ct); - - public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage( - string uri, - int size, - bool crop - ) - { - if (!uri.StartsWith("data:image/")) - throw new ArgumentException("Not a data URI", nameof(uri)); - - string[] split = uri.Remove(0, "data:".Length).Split(";base64,"); - string contentType = split[0]; - string encoded = split[1]; - if (!ValidContentTypes.Contains(contentType)) - throw new ArgumentException("Invalid content type for image", nameof(uri)); - - if (!AuthUtils.TryFromBase64String(encoded, out byte[]? rawImage)) - throw new ArgumentException("Invalid base64 string", nameof(uri)); - - var image = Image.Load(rawImage); - - var processor = new ResizeProcessor( - new ResizeOptions - { - Size = new Size(size), - Mode = crop ? ResizeMode.Crop : ResizeMode.Max, - Position = AnchorPositionMode.Center, - }, - image.Size - ); - - image.Mutate(x => x.ApplyProcessor(processor)); - - var stream = new MemoryStream(64 * 1024); - await image.SaveAsync(stream, new WebpEncoder { Quality = 95, NearLossless = false }); - - stream.Seek(0, SeekOrigin.Begin); - string hash = Convert.ToHexString(await SHA256.HashDataAsync(stream)).ToLower(); - stream.Seek(0, SeekOrigin.Begin); - - return (hash, stream); - } -} diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs deleted file mode 100644 index a4fb444..0000000 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Services; -using Foxnouns.Backend.Utils; -using Newtonsoft.Json; -using NodaTime; - -namespace Foxnouns.Backend.Extensions; - -public static class KeyCacheExtensions -{ - public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheService) - { - string state = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); - return state; - } - - public static async Task ValidateAuthStateAsync( - this KeyCacheService keyCacheService, - string state - ) - { - string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}"); - if (val == null) - throw new ApiError.BadRequest("Invalid OAuth state"); - } - - public static async Task GenerateRegisterEmailStateAsync( - this KeyCacheService keyCacheService, - string email, - Snowflake? userId = null - ) - { - string state = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync( - $"email_state:{state}", - new RegisterEmailState(email, userId), - Duration.FromDays(1) - ); - return state; - } - - public static async Task GetRegisterEmailStateAsync( - this KeyCacheService keyCacheService, - string state - ) => await keyCacheService.GetKeyAsync($"email_state:{state}"); - - public static async Task GenerateAddExtraAccountStateAsync( - this KeyCacheService keyCacheService, - AuthType authType, - Snowflake userId, - string? instance = null - ) - { - string state = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync( - $"add_account:{state}", - new AddExtraAccountState(authType, userId, instance), - Duration.FromDays(1) - ); - return state; - } - - public static async Task GetAddExtraAccountStateAsync( - this KeyCacheService keyCacheService, - string state - ) => await keyCacheService.GetKeyAsync($"add_account:{state}", true); - - public static async Task GenerateForgotPasswordStateAsync( - this KeyCacheService keyCacheService, - string email, - Snowflake userId - ) - { - string state = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync( - $"forgot_password:{state}", - new ForgotPasswordState(email, userId), - Duration.FromHours(1) - ); - return state; - } - - public static async Task GetForgotPasswordStateAsync( - this KeyCacheService keyCacheService, - string state, - bool delete = true - ) => await keyCacheService.GetKeyAsync($"forgot_password:{state}", delete); -} - -public record RegisterEmailState( - string Email, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Snowflake? ExistingUserId -); - -public record ForgotPasswordState(string Email, Snowflake UserId); - -public record AddExtraAccountState(AuthType AuthType, Snowflake UserId, string? Instance = null); diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index c3efda6..84381c4 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -1,38 +1,10 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Coravel; -using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; -using Foxnouns.Backend.Services.Auth; -using Foxnouns.Backend.Services.Caching; -using Foxnouns.Backend.Services.V1; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Http.Resilience; -using Minio; using NodaTime; -using Polly; -using Prometheus; using Serilog; using Serilog.Events; -using Serilog.Sinks.SystemConsole.Themes; -using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; -using IClock = NodaTime.IClock; namespace Foxnouns.Backend.Extensions; @@ -43,33 +15,28 @@ public static class WebApplicationExtensions /// public static WebApplicationBuilder AddSerilog(this WebApplicationBuilder builder) { - Config config = builder.Configuration.Get() ?? new Config(); + var config = builder.Configuration.Get() ?? new(); - LoggerConfiguration logCfg = new LoggerConfiguration() + var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() - .MinimumLevel.Is(config.Logging.LogEventLevel) + .MinimumLevel.Is(config.LogEventLevel) // ASP.NET's built in request logs are extremely verbose, so we use Serilog's instead. // Serilog doesn't disable the built-in logs, so we do it here. .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override( - "Microsoft.EntityFrameworkCore.Database.Command", - config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal - ) - // These spam the output even on INF level .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) - // Hangfire's debug-level logs are extremely spammy for no reason - .MinimumLevel.Override("Hangfire", LogEventLevel.Information) - .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen); + .WriteTo.Console(); - if (config.Logging.SeqLogUrl != null) + if (config.SeqLogUrl != null) { - logCfg.WriteTo.Seq(config.Logging.SeqLogUrl); + logCfg.WriteTo.Seq(config.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); } + Log.Logger = logCfg.CreateLogger(); + // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. - builder.Services.AddSerilog().AddSingleton(Log.Logger = logCfg.CreateLogger()); + builder.Services.AddSerilog().AddSingleton(Log.Logger); return builder; } @@ -79,144 +46,47 @@ public static class WebApplicationExtensions builder.Configuration.Sources.Clear(); builder.Configuration.AddConfiguration(); - Config config = builder.Configuration.Get() ?? new Config(); + var config = builder.Configuration.Get() ?? new(); builder.Services.AddSingleton(config); return config; } public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder) { - string file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini"; + var file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini"; return builder .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appSettings.json", true) - .AddIniFile(file, false, true) + .AddIniFile(file, optional: false, reloadOnChange: true) .AddEnvironmentVariables(); } - /// - /// Adds required services to the WebApplicationBuilder. - /// This should only add services that are not ASP.NET-related (i.e. no middleware). - /// - public static IServiceCollection AddServices(this WebApplicationBuilder builder, Config config) - { - builder.Host.ConfigureServices( - (ctx, services) => - { - // create a single HTTP client for all requests. - // it's also configured with a retry mechanism, so that requests aren't immediately lost to the void if they fail - services.AddSingleton(_ => - { - // ReSharper disable once SuggestVarOrType_Elsewhere - var retryPipeline = new ResiliencePipelineBuilder() - .AddRetry( - new HttpRetryStrategyOptions - { - BackoffType = DelayBackoffType.Linear, - MaxRetryAttempts = 3, - } - ) - .Build(); + public static IServiceCollection AddCustomServices(this IServiceCollection services) => services + .AddSingleton(SystemClock.Instance) + .AddSnowflakeGenerator() + .AddScoped() + .AddScoped() + .AddScoped(); - var resilienceHandler = new ResilienceHandler(retryPipeline) - { - InnerHandler = new SocketsHttpHandler - { - PooledConnectionLifetime = TimeSpan.FromMinutes(15), - }, - }; + public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services + .AddScoped() + .AddScoped() + .AddScoped(); - var client = new HttpClient(resilienceHandler); - client.DefaultRequestHeaders.Remove("User-Agent"); - client.DefaultRequestHeaders.Remove("Accept"); - client.DefaultRequestHeaders.Add( - "User-Agent", - $"pronouns.cc/{BuildInfo.Version}" - ); - client.DefaultRequestHeaders.Add("Accept", "application/json"); - return client; - }); - - services - .AddQueue() - .AddSmtpMailer(ctx.Configuration) - .AddFoxnounsDatabase(config) - .AddMetricServer(o => o.Port = config.Logging.MetricsPort) - .AddMinio(c => - c.WithEndpoint(config.Storage.Endpoint) - .WithCredentials(config.Storage.AccessKey, config.Storage.SecretKey) - .Build() - ) - .AddSingleton() - .AddSingleton(SystemClock.Instance) - .AddSnowflakeGenerator() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddTransient() - .AddTransient() - .AddSingleton() - // Background services - .AddHostedService() - // Transient jobs - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() - // Legacy services - .AddScoped() - .AddScoped(); - - if (!config.Logging.EnableMetrics) - services.AddHostedService(); - } - ); - - return builder.Services; - } - - public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => - services - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped(); - - public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) => - app.UseMiddleware() - .UseMiddleware() - .UseMiddleware() - .UseMiddleware(); + public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) => app + .UseMiddleware() + .UseMiddleware() + .UseMiddleware(); public static async Task Initialize(this WebApplication app, string[] args) { - app.Services.ConfigureQueue() - .LogQueuedTaskProgress(app.Services.GetRequiredService>()); + await BuildInfo.ReadBuildInfo(); - await using AsyncServiceScope scope = app.Services.CreateAsyncScope(); - - // The types of these variables are obvious from the methods being called to create them - // ReSharper disable SuggestVarOrType_SimpleTypes - var logger = scope - .ServiceProvider.GetRequiredService() - .ForContext(); + await using var scope = app.Services.CreateAsyncScope(); + var logger = scope.ServiceProvider.GetRequiredService().ForContext(); var db = scope.ServiceProvider.GetRequiredService(); - // ReSharper restore SuggestVarOrType_SimpleTypes - logger.Information( - "Starting Foxnouns.NET {Version} ({Hash})", - BuildInfo.Version, - BuildInfo.Hash - ); + logger.Information("Starting Foxnouns.NET {Version} ({Hash})", BuildInfo.Version, BuildInfo.Hash); var pendingMigrations = (await db.Database.GetPendingMigrationsAsync()).ToList(); if (args.Contains("--migrate") || args.Contains("--migrate-and-start")) @@ -232,19 +102,17 @@ public static class WebApplicationExtensions logger.Information("Successfully migrated database"); } - if (!args.Contains("--migrate-and-start")) - Environment.Exit(0); + if (!args.Contains("--migrate-and-start")) Environment.Exit(0); } else if (pendingMigrations.Count > 0) { logger.Fatal( "There are {Count} pending migrations, run server with --migrate or --migrate-and-start to run migrations.", - pendingMigrations.Count - ); + pendingMigrations.Count); Environment.Exit(1); } logger.Information("Initializing frontend OAuth application"); _ = await db.GetFrontendApplicationAsync(); } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 42a3beb..8438390 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -1,69 +1,32 @@ - net9.0 + net8.0 enable enable - true - Linux - - - - - - - - - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - + + + + + + + + - - - - - - - - .dockerignore - - diff --git a/Foxnouns.Backend/FoxnounsMetrics.cs b/Foxnouns.Backend/FoxnounsMetrics.cs deleted file mode 100644 index c2fe8da..0000000 --- a/Foxnouns.Backend/FoxnounsMetrics.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Prometheus; - -namespace Foxnouns.Backend; - -public static class FoxnounsMetrics -{ - public static readonly Gauge UsersCount = Metrics.CreateGauge( - "foxnouns_user_count", - "Number of total users" - ); - - public static readonly Gauge UsersActiveMonthCount = Metrics.CreateGauge( - "foxnouns_user_count_active_month", - "Number of users active in the last month" - ); - - public static readonly Gauge UsersActiveWeekCount = Metrics.CreateGauge( - "foxnouns_user_count_active_week", - "Number of users active in the last week" - ); - - public static readonly Gauge UsersActiveDayCount = Metrics.CreateGauge( - "foxnouns_user_count_active_day", - "Number of users active in the last day" - ); - - public static readonly Gauge MemberCount = Metrics.CreateGauge( - "foxnouns_member_count", - "Number of total members" - ); - - public static readonly Summary MetricsCollectionTime = Metrics.CreateSummary( - "foxnouns_time_metrics", - "Time it took to collect metrics" - ); - - public static Gauge ProcessPhysicalMemory => - Metrics.CreateGauge("foxnouns_process_physical_memory", "Process physical memory"); - - public static Gauge ProcessVirtualMemory => - Metrics.CreateGauge("foxnouns_process_virtual_memory", "Process virtual memory"); - - public static Gauge ProcessPrivateMemory => - Metrics.CreateGauge("foxnouns_process_private_memory", "Process private memory"); - - public static Gauge ProcessThreads => - Metrics.CreateGauge("foxnouns_process_threads", "Process thread count"); - - public static Gauge ProcessHandles => - Metrics.CreateGauge("foxnouns_process_handles", "Process handle count"); -} diff --git a/Foxnouns.Backend/GlobalUsing.cs b/Foxnouns.Backend/GlobalUsing.cs index e9f359b..5275c8d 100644 --- a/Foxnouns.Backend/GlobalUsing.cs +++ b/Foxnouns.Backend/GlobalUsing.cs @@ -1,16 +1,2 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . global using ILogger = Serilog.ILogger; global using Log = Serilog.Log; diff --git a/Foxnouns.Backend/Jobs/CreateDataExportJob.cs b/Foxnouns.Backend/Jobs/CreateDataExportJob.cs deleted file mode 100644 index 1c392f7..0000000 --- a/Foxnouns.Backend/Jobs/CreateDataExportJob.cs +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.IO.Compression; -using System.Net; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Services; -using Foxnouns.Backend.Utils; -using Hangfire; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; -using NodaTime; -using NodaTime.Text; - -namespace Foxnouns.Backend.Jobs; - -public class CreateDataExportJob( - HttpClient client, - DatabaseContext db, - IClock clock, - UserRendererService userRenderer, - MemberRendererService memberRenderer, - ObjectStorageService objectStorageService, - ISnowflakeGenerator snowflakeGenerator, - ILogger logger -) -{ - private readonly ILogger _logger = logger.ForContext(); - - public static void Enqueue(Snowflake userId) - { - BackgroundJob.Enqueue(j => j.InvokeAsync(userId)); - } - - public async Task InvokeAsync(Snowflake userId) - { - try - { - await InvokeAsyncInner(userId); - } - catch (Exception e) - { - _logger.Error(e, "Error generating data export for user {UserId}", userId); - } - } - - private async Task InvokeAsyncInner(Snowflake userId) - { - User? user = await db - .Users.Include(u => u.AuthMethods) - .Include(u => u.Flags) - .Include(u => u.ProfileFlags) - .AsSplitQuery() - .FirstOrDefaultAsync(u => u.Id == userId); - if (user == null) - { - _logger.Warning( - "Received create data export request for user {UserId} but no such user exists, deleted or otherwise. Ignoring request", - userId - ); - return; - } - - _logger.Information("Generating data export for user {UserId}", user.Id); - - await using var stream = new MemoryStream(); - using var zip = new ZipArchive(stream, ZipArchiveMode.Create, true); - zip.Comment = - $"This archive for {user.Username} ({user.Id}) was generated at {InstantPattern.General.Format(clock.GetCurrentInstant())}"; - - // Write the user's info and avatar - WriteJson( - zip, - "user.json", - await userRenderer.RenderUserInnerAsync(user, true, ["*"], false, true) - ); - await WriteS3Object(zip, "user-avatar.webp", userRenderer.AvatarUrlFor(user)); - - foreach (PrideFlag? flag in user.Flags) - await WritePrideFlag(zip, flag); - - List members = await db - .Members.Include(m => m.User) - .Include(m => m.ProfileFlags) - .Where(m => m.UserId == user.Id) - .ToListAsync(); - foreach (Member? member in members) - await WriteMember(zip, member); - - // We want to dispose the ZipArchive on an error, but we need to dispose it manually to upload to object storage. - // Calling Dispose() multiple times is fine for this class, though. - // ReSharper disable once DisposeOnUsingVariable - zip.Dispose(); - stream.Seek(0, SeekOrigin.Begin); - - // Upload the file! - string filename = AuthUtils.RandomToken(); - await objectStorageService.PutObjectAsync( - ExportPath(user.Id, filename), - stream, - "application/zip" - ); - - db.Add( - new DataExport - { - Id = snowflakeGenerator.GenerateSnowflake(), - UserId = user.Id, - Filename = filename, - } - ); - await db.SaveChangesAsync(); - } - - 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 = $""" - {flag.Name} - ---- - {flag.Description ?? ""} - """; - - try - { - await WriteS3Object(zip, $"flag-{flag.Id}/flag.webp", userRenderer.ImageUrlFor(flag)); - } - catch (Exception e) - { - _logger.Warning(e, "Could not write image for flag {FlagId}", flag.Id); - return; - } - - ZipArchiveEntry entry = zip.CreateEntry($"flag-{flag.Id}/flag.txt"); - await using Stream stream = entry.Open(); - await using var writer = new StreamWriter(stream); - await writer.WriteAsync(flagData); - } - - private async Task WriteMember(ZipArchive zip, Member member) - { - _logger.Debug("Writing member {MemberId}", member.Id); - - WriteJson( - zip, - $"members/{member.Name} ({member.Id}).json", - memberRenderer.RenderMember(member) - ); - - try - { - await WriteS3Object( - zip, - $"members/{member.Name} ({member.Id}).webp", - memberRenderer.AvatarUrlFor(member) - ); - } - catch (Exception e) - { - _logger.Warning(e, "Error writing avatar for member {MemberId}", member.Id); - } - } - - private void WriteJson(ZipArchive zip, string filename, object data) - { - string json = JsonConvert.SerializeObject(data, Formatting.Indented); - - _logger.Debug( - "Writing file {Filename} to archive with size {Length}", - filename, - json.Length - ); - - ZipArchiveEntry entry = zip.CreateEntry(filename); - using Stream stream = entry.Open(); - using var writer = new StreamWriter(stream); - writer.Write(json); - } - - private async Task WriteS3Object(ZipArchive zip, string filename, string? s3Path) - { - if (s3Path == null) - return; - - HttpResponseMessage resp = await client.GetAsync(s3Path); - if (resp.StatusCode != HttpStatusCode.OK) - { - _logger.Warning("S3 path {S3Path} returned a non-200 status, not saving file", s3Path); - return; - } - - await using Stream respStream = await resp.Content.ReadAsStreamAsync(); - - _logger.Debug( - "Writing file {Filename} to archive with size {Length}", - filename, - respStream.Length - ); - - ZipArchiveEntry entry = zip.CreateEntry(filename); - await using Stream entryStream = entry.Open(); - - respStream.Seek(0, SeekOrigin.Begin); - await respStream.CopyToAsync(entryStream); - } - - private static string ExportPath(Snowflake userId, string b64) => - $"data-exports/{userId}/{b64}/data-export.zip"; -} diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs deleted file mode 100644 index e40bfa4..0000000 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; -using Foxnouns.Backend.Services; -using Hangfire; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Jobs; - -public class CreateFlagJob( - DatabaseContext db, - ObjectStorageService objectStorageService, - ILogger logger -) -{ - private readonly ILogger _logger = logger.ForContext(); - - public static void Enqueue(CreateFlagPayload payload) - { - BackgroundJob.Enqueue(j => j.InvokeAsync(payload)); - } - - public async Task InvokeAsync(CreateFlagPayload payload) - { - _logger.Information( - "Creating flag {FlagId} for user {UserId} with image data length {DataLength}", - payload.Id, - payload.UserId, - payload.ImageData.Length - ); - - 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, - false - ); - await objectStorageService.PutObjectAsync(Path(hash), image, "image/webp"); - - flag.Hash = hash; - db.Update(flag); - await db.SaveChangesAsync(); - - _logger.Information("Uploaded flag {FlagId} with hash {Hash}", flag.Id, flag.Hash); - } - catch (ArgumentException ae) - { - _logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", payload.Id, ae.Message); - } - - throw new NotImplementedException(); - } - - public static string Path(string hash) => $"flags/{hash}.webp"; -} diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs deleted file mode 100644 index c1d2df4..0000000 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; -using Foxnouns.Backend.Services; -using Hangfire; - -namespace Foxnouns.Backend.Jobs; - -public class MemberAvatarUpdateJob( - DatabaseContext db, - ObjectStorageService objectStorageService, - ILogger logger -) -{ - private readonly ILogger _logger = logger.ForContext(); - - public static void Enqueue(AvatarUpdatePayload payload) - { - BackgroundJob.Enqueue(j => j.InvokeAsync(payload)); - } - - public async Task InvokeAsync(AvatarUpdatePayload payload) - { - if (payload.NewAvatar != null) - await UpdateMemberAvatarAsync(payload.Id, payload.NewAvatar); - else - await ClearMemberAvatarAsync(payload.Id); - } - - private async Task UpdateMemberAvatarAsync(Snowflake id, string newAvatar) - { - _logger.Debug("Updating avatar for member {MemberId}", id); - - Member? member = await db.Members.FindAsync(id); - if (member == null) - { - _logger.Warning( - "Update avatar job queued for {MemberId} but no member with that ID exists", - id - ); - return; - } - - try - { - (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( - newAvatar, - 512, - true - ); - string? prevHash = member.Avatar; - - await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); - - member.Avatar = hash; - member.AvatarMigrated = true; - await db.SaveChangesAsync(); - - if (prevHash != null && prevHash != hash) - await objectStorageService.RemoveObjectAsync(Path(id, prevHash)); - - _logger.Information("Updated avatar for member {MemberId}", id); - } - catch (ArgumentException ae) - { - _logger.Warning( - "Invalid data URI for new avatar for member {MemberId}: {Reason}", - id, - ae.Message - ); - } - } - - private async Task ClearMemberAvatarAsync(Snowflake id) - { - _logger.Debug("Clearing avatar for member {MemberId}", id); - - Member? member = await db.Members.FindAsync(id); - if (member == null) - { - _logger.Warning( - "Clear avatar job queued for {MemberId} but no member with that ID exists", - id - ); - return; - } - - if (member.Avatar == null) - { - _logger.Warning("Clear avatar job queued for {MemberId} with null avatar", id); - return; - } - - await objectStorageService.RemoveObjectAsync(Path(member.Id, member.Avatar)); - - member.Avatar = null; - await db.SaveChangesAsync(); - } - - public static string Path(Snowflake id, string hash) => $"members/{id}/avatars/{hash}.webp"; -} diff --git a/Foxnouns.Backend/Jobs/Payloads.cs b/Foxnouns.Backend/Jobs/Payloads.cs deleted file mode 100644 index 1f76ea2..0000000 --- a/Foxnouns.Backend/Jobs/Payloads.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; - -namespace Foxnouns.Backend.Jobs; - -public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); - -public record CreateFlagPayload(Snowflake Id, Snowflake UserId, string ImageData); diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs deleted file mode 100644 index cf7bed0..0000000 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; -using Foxnouns.Backend.Services; -using Hangfire; - -namespace Foxnouns.Backend.Jobs; - -public class UserAvatarUpdateJob( - DatabaseContext db, - ObjectStorageService objectStorageService, - ILogger logger -) -{ - private readonly ILogger _logger = logger.ForContext(); - - public static void Enqueue(AvatarUpdatePayload payload) - { - BackgroundJob.Enqueue(j => j.InvokeAsync(payload)); - } - - public async Task InvokeAsync(AvatarUpdatePayload payload) - { - if (payload.NewAvatar != null) - await UpdateUserAvatarAsync(payload.Id, payload.NewAvatar); - else - await ClearUserAvatarAsync(payload.Id); - } - - private async Task UpdateUserAvatarAsync(Snowflake id, string newAvatar) - { - _logger.Debug("Updating avatar for user {MemberId}", id); - - User? user = await db.Users.FindAsync(id); - if (user == null) - { - _logger.Warning( - "Update avatar job queued for {UserId} but no user with that ID exists", - id - ); - return; - } - - try - { - (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( - newAvatar, - 512, - true - ); - image.Seek(0, SeekOrigin.Begin); - string? prevHash = user.Avatar; - - await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); - - user.Avatar = hash; - user.AvatarMigrated = true; - await db.SaveChangesAsync(); - - if (prevHash != null && prevHash != hash) - await objectStorageService.RemoveObjectAsync(Path(id, prevHash)); - - _logger.Information("Updated avatar for user {UserId}", id); - } - catch (ArgumentException ae) - { - _logger.Warning( - "Invalid data URI for new avatar for user {UserId}: {Reason}", - id, - ae.Message - ); - } - } - - private async Task ClearUserAvatarAsync(Snowflake id) - { - _logger.Debug("Clearing avatar for user {MemberId}", id); - - User? user = await db.Users.FindAsync(id); - if (user == null) - { - _logger.Warning( - "Clear avatar job queued for {UserId} but no user with that ID exists", - id - ); - return; - } - - if (user.Avatar == null) - { - _logger.Warning("Clear avatar job queued for {UserId} with null avatar", id); - return; - } - - await objectStorageService.RemoveObjectAsync(Path(user.Id, user.Avatar)); - - user.Avatar = null; - await db.SaveChangesAsync(); - } - - public static string Path(Snowflake id, string hash) => $"users/{id}/avatars/{hash}.webp"; -} diff --git a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs deleted file mode 100644 index 41a6609..0000000 --- a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Coravel.Mailer.Mail; - -namespace Foxnouns.Backend.Mailables; - -public class AccountCreationMailable(Config config, AccountCreationMailableView view) - : Mailable -{ - private string PlainText() => - $""" - Please continue creating a new pronouns.cc account by using the following link: - {view.BaseUrl}/auth/callback/email/{view.Code} - Note that this link will expire in one hour. - - If you didn't mean to create a new account, feel free to ignore this email. - """; - - public override void Build() - { - To(view.To) - .From(config.EmailAuth.From!) - .Subject("Create an account") - .View("~/Views/Mail/AccountCreation.cshtml", view) - .Text(PlainText()); - } -} - -public class AccountCreationMailableView : BaseView -{ - public required string Code { get; init; } -} diff --git a/Foxnouns.Backend/Mailables/AddEmailMailable.cs b/Foxnouns.Backend/Mailables/AddEmailMailable.cs deleted file mode 100644 index 1c381f2..0000000 --- a/Foxnouns.Backend/Mailables/AddEmailMailable.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Coravel.Mailer.Mail; - -namespace Foxnouns.Backend.Mailables; - -public class AddEmailMailable(Config config, AddEmailMailableView view) - : Mailable -{ - private string PlainText() => - $""" - Hello @{view.Username}, please confirm adding this email address to your account by using the following link: - {view.BaseUrl}/auth/callback/email/{view.Code} - Note that this link will expire in one hour. - - If you didn't mean to link this email address to @{view.Username}, feel free to ignore this email. - """; - - public override void Build() - { - To(view.To) - .From(config.EmailAuth.From!) - .Subject("Confirm adding this email address to an existing account") - .View("~/Views/Mail/AddEmail.cshtml", view) - .Text(PlainText()); - } -} - -public class AddEmailMailableView : BaseView -{ - public required string Code { get; init; } - public required string Username { get; init; } -} diff --git a/Foxnouns.Backend/Mailables/BaseView.cs b/Foxnouns.Backend/Mailables/BaseView.cs deleted file mode 100644 index bc3d2bf..0000000 --- a/Foxnouns.Backend/Mailables/BaseView.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -namespace Foxnouns.Backend.Mailables; - -public abstract class BaseView -{ - public required string BaseUrl { get; init; } - public required string To { get; init; } -} diff --git a/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs b/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs deleted file mode 100644 index 46a0309..0000000 --- a/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Coravel.Mailer.Mail; - -namespace Foxnouns.Backend.Mailables; - -public class PasswordChangedMailable(Config config, PasswordChangedMailableView view) - : Mailable -{ - private string PlainText() => - $""" - Your password has been changed using a "forgot password" link. - If this wasn't you, request a password reset immediately: - {view.BaseUrl}/auth/forgot-password - """; - - public override void Build() - { - To(view.To) - .From(config.EmailAuth.From!) - .Subject("Your password has been changed") - .View("~/Views/Mail/PasswordChanged.cshtml", view) - .Text(PlainText()); - } -} - -public class PasswordChangedMailableView : BaseView; diff --git a/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs b/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs deleted file mode 100644 index ee06732..0000000 --- a/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Coravel.Mailer.Mail; - -namespace Foxnouns.Backend.Mailables; - -public class ResetPasswordMailable(Config config, ResetPasswordMailableView view) - : Mailable -{ - private string PlainText() => - $""" - Somebody (hopefully you!) has requested a password reset. - You can use the following link to do this: - {view.BaseUrl}/auth/forgot-password/{view.Code} - Note that this link will expire in one hour. - - If you weren't expecting this email, you don't have to do anything. - Your password can't be changed without the above link. - """; - - public override void Build() - { - To(view.To) - .From(config.EmailAuth.From!) - .Subject("Reset your account's password") - .View("~/Views/Mail/ResetPassword.cshtml", view) - .Text(PlainText()); - } -} - -public class ResetPasswordMailableView : BaseView -{ - public required string Code { get; init; } -} diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index 6a1b631..4435fa6 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -1,29 +1,18 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . +using System.Security.Cryptography; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; +using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Foxnouns.Backend.Middleware; -public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware +public class AuthenticationMiddleware(DatabaseContext db, IClock clock) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - Endpoint? endpoint = ctx.GetEndpoint(); - AuthenticateAttribute? metadata = endpoint?.Metadata.GetMetadata(); + var endpoint = ctx.GetEndpoint(); + var metadata = endpoint?.Metadata.GetMetadata(); if (metadata == null) { @@ -31,18 +20,18 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware return; } - if ( - !AuthUtils.TryParseToken( - ctx.Request.Headers.Authorization.ToString(), - out byte[]? rawToken - ) - ) + var header = ctx.Request.Headers.Authorization.ToString(); + if (!OauthUtils.TryFromBase64String(header, out var rawToken)) { await next(ctx); return; } - Token? oauthToken = await db.GetToken(rawToken); + var hash = SHA512.HashData(rawToken); + var oauthToken = await db.Tokens + .Include(t => t.Application) + .Include(t => t.User) + .FirstOrDefaultAsync(t => t.Hash == hash && t.ExpiresAt > clock.GetCurrentInstant() && !t.ManuallyExpired); if (oauthToken == null) { await next(ctx); @@ -50,6 +39,7 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware } ctx.SetToken(oauthToken); + await next(ctx); } } @@ -59,7 +49,6 @@ public static class HttpContextExtensions private const string Key = "token"; public static void SetToken(this HttpContext ctx, Token token) => ctx.Items.Add(Key, token); - public static User? GetUser(this HttpContext ctx) => ctx.GetToken()?.User; public static User GetUserOrThrow(this HttpContext ctx) => @@ -67,11 +56,11 @@ public static class HttpContextExtensions public static Token? GetToken(this HttpContext ctx) { - if (ctx.Items.TryGetValue(Key, out object? token)) + if (ctx.Items.TryGetValue(Key, out var token)) return token as Token; return null; } } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AuthenticateAttribute : Attribute; +public class AuthenticateAttribute : Attribute; \ No newline at end of file diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index fc216f5..570d917 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -1,18 +1,3 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; namespace Foxnouns.Backend.Middleware; @@ -21,33 +6,21 @@ public class AuthorizationMiddleware : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { - Endpoint? endpoint = ctx.GetEndpoint(); - AuthorizeAttribute? attribute = endpoint?.Metadata.GetMetadata(); + var endpoint = ctx.GetEndpoint(); + var attribute = endpoint?.Metadata.GetMetadata(); - if (attribute == null || attribute.Scopes.Length == 0) + if (attribute == null) { await next(ctx); return; } - Token? token = ctx.GetToken(); - + var token = ctx.GetToken(); if (token == null) - { - throw new ApiError.Unauthorized( - "This endpoint requires an authenticated user.", - ErrorCode.AuthenticationRequired - ); - } - - if (attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any()) - { - throw new ApiError.Forbidden( - "This endpoint requires ungranted scopes.", - attribute.Scopes.Except(token.Scopes.ExpandScopes()), - ErrorCode.MissingScopes - ); - } + throw new ApiError.Unauthorized("This endpoint requires an authenticated user."); + if (attribute.Scopes.Length > 0 && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any()) + throw new ApiError.Forbidden("This endpoint requires ungranted scopes.", + attribute.Scopes.Except(token.Scopes.ExpandScopes())); await next(ctx); } @@ -56,5 +29,8 @@ public class AuthorizationMiddleware : IMiddleware [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AuthorizeAttribute(params string[] scopes) : Attribute { - public readonly string[] Scopes = scopes.Except([":admin", ":moderator", ":deleted"]).ToArray(); -} + public readonly bool RequireAdmin = scopes.Contains(":admin"); + public readonly bool RequireModerator = scopes.Contains(":moderator"); + + public readonly string[] Scopes = scopes.Except([":admin", ":moderator"]).ToArray(); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index db08916..da4804b 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,25 +1,10 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using System.Net; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Utils; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; namespace Foxnouns.Backend.Middleware; -public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddleware +public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { @@ -29,35 +14,14 @@ public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddlewa } catch (Exception e) { - Type type = e.TargetSite?.DeclaringType ?? typeof(ErrorHandlerMiddleware); - string typeName = e.TargetSite?.DeclaringType?.FullName ?? ""; - ILogger logger = baseLogger.ForContext(type); + var type = e.TargetSite?.DeclaringType ?? typeof(ErrorHandlerMiddleware); + var typeName = e.TargetSite?.DeclaringType?.FullName ?? ""; + var logger = baseLogger.ForContext(type); if (ctx.Response.HasStarted) { - logger.Error( - e, - "Error in {ClassName} ({Path}) after response started being sent", - typeName, - ctx.Request.Path - ); - - sentry.CaptureException( - e, - scope => - { - User? user = ctx.GetUser(); - if (user != null) - { - scope.User = new SentryUser - { - Id = user.Id.ToString(), - Username = user.Username, - }; - } - } - ); - + logger.Error(e, "Error in {ClassName} ({Path}) after response started being sent", typeName, + ctx.Request.Path); return; } @@ -68,83 +32,43 @@ public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddlewa ctx.Response.ContentType = "application/json; charset=utf-8"; if (ae is ApiError.Forbidden fe) { - await ctx.Response.WriteAsync( - JsonConvert.SerializeObject( - new HttpApiError - { - Status = (int)fe.StatusCode, - Code = ErrorCode.Forbidden, - Message = fe.Message, - Scopes = fe.Scopes.Length > 0 ? fe.Scopes : null, - } - ) - ); + await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError + { + Status = (int)fe.StatusCode, + Code = ErrorCode.Forbidden, + Message = fe.Message, + Scopes = fe.Scopes.Length > 0 ? fe.Scopes : null + })); return; } - if (ae is ApiError.BadRequest br) + await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError { - await ctx.Response.WriteAsync(br.ToJson().ToString()); - return; - } - - await ctx.Response.WriteAsync( - JsonConvert.SerializeObject( - new HttpApiError - { - Status = (int)ae.StatusCode, - Code = ae.ErrorCode, - Message = ae.Message, - } - ) - ); + Status = (int)ae.StatusCode, + Code = ae.ErrorCode, + Message = ae.Message, + })); return; } if (e is FoxnounsError fce) { - logger.Error( - fce.Inner ?? fce, - "Exception in {ClassName} ({Path})", - typeName, - ctx.Request.Path - ); + logger.Error(fce.Inner ?? fce, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path); } else { logger.Error(e, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path); } - SentryId errorId = sentry.CaptureException( - e, - scope => - { - User? user = ctx.GetUser(); - if (user != null) - { - scope.User = new SentryUser - { - Id = user.Id.ToString(), - Username = user.Username, - }; - } - } - ); - ctx.Response.StatusCode = (int)HttpStatusCode.InternalServerError; ctx.Response.Headers.RequestId = ctx.TraceIdentifier; ctx.Response.ContentType = "application/json; charset=utf-8"; - await ctx.Response.WriteAsync( - JsonConvert.SerializeObject( - new HttpApiError - { - Status = (int)HttpStatusCode.InternalServerError, - Code = ErrorCode.InternalServerError, - ErrorId = errorId.ToString(), - Message = "Internal server error", - } - ) - ); + await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError + { + Status = (int)HttpStatusCode.InternalServerError, + Code = ErrorCode.InternalServerError, + Message = "Internal server error", + })); } } } @@ -153,14 +77,11 @@ public record HttpApiError { public required int Status { get; init; } - [JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] + [JsonConverter(typeof(StringEnumConverter))] public required ErrorCode Code { get; init; } - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public string? ErrorId { get; init; } - public required string Message { get; init; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string[]? Scopes { get; init; } -} +} \ No newline at end of file diff --git a/Foxnouns.Backend/Middleware/LimitMiddleware.cs b/Foxnouns.Backend/Middleware/LimitMiddleware.cs deleted file mode 100644 index 6092041..0000000 --- a/Foxnouns.Backend/Middleware/LimitMiddleware.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database.Models; - -namespace Foxnouns.Backend.Middleware; - -public class LimitMiddleware : IMiddleware -{ - public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) - { - Endpoint? endpoint = ctx.GetEndpoint(); - LimitAttribute? attribute = endpoint?.Metadata.GetMetadata(); - - Token? token = ctx.GetToken(); - - if (attribute == null) - { - // Check for authorize attribute - // If it exists, and the user is deleted, throw an error. - if ( - endpoint?.Metadata.GetMetadata() != null - && token?.User.Deleted == true - ) - { - throw new ApiError.Forbidden("Deleted users cannot access this endpoint."); - } - - await next(ctx); - return; - } - - if (token?.User.Deleted == true && !attribute.UsableByDeletedUsers) - throw new ApiError.Forbidden("Deleted users cannot access this endpoint."); - - if (attribute.RequireAdmin && token?.User.Role != UserRole.Admin) - throw new ApiError.Forbidden("This endpoint can only be used by admins."); - - if ( - attribute.RequireModerator - && token?.User.Role is not (UserRole.Admin or UserRole.Moderator) - ) - { - throw new ApiError.Forbidden("This endpoint can only be used by moderators."); - } - - await next(ctx); - } -} - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class LimitAttribute : Attribute -{ - public bool UsableByDeletedUsers { get; init; } - public bool RequireAdmin { get; init; } - public bool RequireModerator { get; init; } -} diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index b266248..4eca8cb 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -1,148 +1,62 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Text.Json; -using System.Text.Json.Serialization; using Foxnouns.Backend; +using Foxnouns.Backend.Database; +using Serilog; using Foxnouns.Backend.Extensions; -using Foxnouns.Backend.Services; -using Foxnouns.Backend.Utils; -using Foxnouns.Backend.Utils.OpenApi; -using Hangfire; -using Hangfire.Redis.StackExchange; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using Prometheus; -using Sentry.Extensibility; -using Serilog; -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); +var builder = WebApplication.CreateBuilder(args); -Config config = builder.AddConfiguration(); +var config = builder.AddConfiguration(); builder.AddSerilog(); -// Read version information from .version in the repository root -await BuildInfo.ReadBuildInfo(); - -builder - .WebHost.UseSentry(opts => - { - opts.Dsn = config.Logging.SentryUrl ?? ""; - opts.TracesSampleRate = config.Logging.SentryTracesSampleRate; - opts.MaxRequestBodySize = RequestSize.Small; - }) - .ConfigureKestrel(opts => - { - // Requests are limited to a maximum of 2 MB. - // No valid request body will ever come close to this limit, - // but the limit is slightly higher to prevent valid requests from being rejected. - opts.Limits.MaxRequestBodySize = 2 * 1024 * 1024; - }) - .UseUrls(config.Address); - -builder - .Services.AddControllers() - .AddJsonOptions(options => - { - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; - options.JsonSerializerOptions.Converters.Add( - new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper) - ); - }) +builder.Services + .AddControllers() .AddNewtonsoftJson(options => - { - options.SerializerSettings.ContractResolver = new PatchRequestContractResolver + options.SerializerSettings.ContractResolver = new DefaultContractResolver { - NamingStrategy = new SnakeCaseNamingStrategy(), - }; - options.SerializerSettings.DateParseHandling = DateParseHandling.None; - }) + NamingStrategy = new SnakeCaseNamingStrategy() + }) .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult( - new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() + new ApiError.BadRequest("Bad request", actionContext.ModelState).ToJson() ); }); -builder - .Services.AddHangfire( - (services, c) => - { - c.UseRedisStorage( - services.GetRequiredService().Multiplexer, - new RedisStorageOptions { Prefix = "foxnouns_net:" } - ); - } - ) - .AddHangfireServer(); - -builder.Services.AddOpenApi( - "v2", - options => - { - options.AddSchemaTransformer(); - options.AddSchemaTransformer(); - options.AddDocumentTransformer(new DocumentTransformer(config)); - } -); - // Set the default converter to snake case as we use it in a couple places. -JsonConvert.DefaultSettings = () => - new JsonSerializerSettings +JsonConvert.DefaultSettings = () => new JsonSerializerSettings +{ + ContractResolver = new DefaultContractResolver { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy(), - }, - }; + NamingStrategy = new SnakeCaseNamingStrategy() + } +}; -builder.AddServices(config).AddCustomMiddleware(); +builder.Services + .AddDbContext() + .AddCustomServices() + .AddCustomMiddleware() + .AddEndpointsApiExplorer() + .AddSwaggerGen(); -WebApplication app = builder.Build(); +var app = builder.Build(); await app.Initialize(args); app.UseSerilogRequestLogging(); app.UseRouting(); - -// Not all environments will want tracing (from experience, it's expensive to use in production, even with a low sample rate), -// so it's locked behind a config option. -if (config.Logging.SentryTracing) - app.UseSentryTracing(); +app.UseSwagger(); +app.UseSwaggerUI(); app.UseCors(); app.UseCustomMiddleware(); app.MapControllers(); -app.UseHangfireDashboard(); -// TODO: I can't figure out why this doesn't work yet -// TODO: Manually write API docs in the meantime -// app.MapOpenApi("/api-docs/openapi/{documentName}.json"); -// app.MapScalarApiReference( -// "/api-docs/", -// options => -// { -// options.Title = "pronouns.cc API"; -// options.OpenApiRoutePattern = "/api-docs/openapi/{documentName}.json"; -// } -// ); - -// Make sure metrics are updated whenever Prometheus scrapes them -Metrics.DefaultRegistry.AddBeforeCollectCallback(async ct => - await app.Services.GetRequiredService().CollectMetricsAsync(ct) -); +app.Urls.Clear(); +app.Urls.Add(config.Address); app.Run(); -Log.CloseAndFlush(); + +Log.CloseAndFlush(); \ No newline at end of file diff --git a/Foxnouns.Backend/Properties/launchSettings.json b/Foxnouns.Backend/Properties/launchSettings.json deleted file mode 100644 index b9e2ace..0000000 --- a/Foxnouns.Backend/Properties/launchSettings.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/launchsettings.json", - "profiles": { - "Development": { - "commandName": "Project", - "dotnetRunMessages": true, - "hotReloadEnabled": false, - "launchBrowser": false, - "externalUrlConfiguration": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Production": { - "commandName": "Project", - "dotnetRunMessages": true, - "hotReloadEnabled": false, - "launchBrowser": false, - "externalUrlConfiguration": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Production" - } - } - } -} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs deleted file mode 100644 index e3e3edb..0000000 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Security.Cryptography; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using NodaTime; -using XidNet; - -namespace Foxnouns.Backend.Services.Auth; - -public class AuthService( - IClock clock, - ILogger logger, - DatabaseContext db, - ISnowflakeGenerator snowflakeGenerator, - UserRendererService userRenderer, - ValidationService validationService -) -{ - private readonly ILogger _logger = logger.ForContext(); - private readonly PasswordHasher _passwordHasher = new(); - - /// - /// Creates a new user with the given email address and password. - /// This method does not save the resulting user, the caller must still call . - /// - public async Task CreateUserWithPasswordAsync( - string username, - string email, - string password, - CancellationToken ct = default - ) - { - // Validate username and whether it's not taken - ValidationUtils.Validate( - [ - ("username", validationService.ValidateUsername(username)), - ("password", ValidationUtils.ValidatePassword(password)), - ] - ); - if (await db.Users.AnyAsync(u => u.Username == username, ct)) - throw new ApiError.BadRequest("Username is already taken", "username", username); - - var user = new User - { - Id = snowflakeGenerator.GenerateSnowflake(), - Username = username, - AuthMethods = - { - new AuthMethod - { - Id = snowflakeGenerator.GenerateSnowflake(), - AuthType = AuthType.Email, - RemoteId = email, - }, - }, - LastActive = clock.GetCurrentInstant(), - Sid = null!, - LegacyId = Xid.NewXid().ToString(), - }; - - db.Add(user); - user.Password = await HashPasswordAsync(user, password, ct); - - return user; - } - - /// - /// Creates a new user with the given username and remote authentication method. - /// To create a user with email authentication, use - /// This method does not save the resulting user, the caller must still call . - /// - public async Task CreateUserWithRemoteAuthAsync( - string username, - AuthType authType, - string remoteId, - string remoteUsername, - FediverseApplication? instance = null, - CancellationToken ct = default - ) - { - AssertValidAuthType(authType, instance); - - // Validate username and whether it's not taken - ValidationUtils.Validate([("username", validationService.ValidateUsername(username))]); - if (await db.Users.AnyAsync(u => u.Username == username, ct)) - throw new ApiError.BadRequest("Username is already taken", "username", username); - - var user = new User - { - Id = snowflakeGenerator.GenerateSnowflake(), - Username = username, - AuthMethods = - { - new AuthMethod - { - Id = snowflakeGenerator.GenerateSnowflake(), - AuthType = authType, - RemoteId = remoteId, - RemoteUsername = remoteUsername, - FediverseApplication = instance, - }, - }, - LastActive = clock.GetCurrentInstant(), - Sid = null!, - LegacyId = Xid.NewXid().ToString(), - }; - - db.Add(user); - return user; - } - - /// - /// Authenticates a user with email and password. - /// - /// The user's email address - /// The user's password, in plain text - /// Cancellation token - /// A tuple of the authenticated user and whether multi-factor authentication is required - /// Thrown if the email address is not associated with any user - /// or if the password is incorrect - public async Task<(User, EmailAuthenticationResult)> AuthenticateUserAsync( - string email, - string password, - CancellationToken ct = default - ) - { - User? user = await db.Users.FirstOrDefaultAsync( - u => u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email), - ct - ); - if (user == null) - { - throw new ApiError.NotFound( - "No user with that email address found, or password is incorrect", - ErrorCode.UserNotFound - ); - } - - PasswordVerificationResult pwResult = await VerifyHashedPasswordAsync(user, password, ct); - if (pwResult == PasswordVerificationResult.Failed) // TODO: this seems to fail on some valid passwords? - { - throw new ApiError.NotFound( - "No user with that email address found, or password is incorrect", - ErrorCode.UserNotFound - ); - } - - if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) - { - user.Password = await HashPasswordAsync(user, password, ct); - await db.SaveChangesAsync(ct); - } - - return (user, EmailAuthenticationResult.AuthSuccessful); - } - - public enum EmailAuthenticationResult - { - AuthSuccessful, - MfaRequired, - } - - /// - /// Validates a user's password outside an authentication context, for when a password is required for changing - /// a setting, such as adding a new email address or changing passwords. - /// - public async Task ValidatePasswordAsync( - User user, - string password, - CancellationToken ct = default - ) - { - if (user.Password == null) - { - throw new FoxnounsError("Password for user supplied to ValidatePasswordAsync was null"); - } - - PasswordVerificationResult pwResult = await VerifyHashedPasswordAsync(user, password, ct); - return pwResult - is PasswordVerificationResult.SuccessRehashNeeded - or PasswordVerificationResult.Success; - } - - /// - /// Sets or updates a password for the given user. This method does not save the updated password automatically. - /// - public async Task SetUserPasswordAsync( - User user, - string password, - CancellationToken ct = default - ) - { - user.Password = await HashPasswordAsync(user, password, ct); - db.Update(user); - } - - /// - /// Authenticates a user with a remote authentication provider. - /// - /// The remote authentication provider type - /// The remote user ID - /// The Fediverse instance, if authType is Fediverse. - /// Will throw an exception if passed with another authType. - /// Cancellation token. - /// A user object, or null if the remote account isn't linked to any user. - /// Thrown if instance is passed when not required, - /// or not passed when required - public async Task AuthenticateUserAsync( - AuthType authType, - string remoteId, - FediverseApplication? instance = null, - CancellationToken ct = default - ) - { - AssertValidAuthType(authType, instance); - - return await db.Users.FirstOrDefaultAsync( - u => - u.AuthMethods.Any(a => - a.AuthType == authType - && a.RemoteId == remoteId - && a.FediverseApplication == instance - ), - ct - ); - } - - public async Task AddAuthMethodAsync( - Snowflake userId, - AuthType authType, - string remoteId, - string? remoteUsername = null, - FediverseApplication? app = null, - CancellationToken ct = default - ) - { - AssertValidAuthType(authType, app); - - // This is already checked when generating an add account state, but we check it here too just in case. - int currentCount = await db - .AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType) - .CountAsync(ct); - if (currentCount >= AuthUtils.MaxAuthMethodsPerType) - { - throw new ApiError.BadRequest( - $"Too many linked accounts of this type, maximum of {AuthUtils.MaxAuthMethodsPerType} per account." - ); - } - - var authMethod = new AuthMethod - { - Id = snowflakeGenerator.GenerateSnowflake(), - AuthType = authType, - RemoteId = remoteId, - FediverseApplicationId = app?.Id, - RemoteUsername = remoteUsername, - UserId = userId, - }; - - db.Add(authMethod); - await db.SaveChangesAsync(ct); - return authMethod; - } - - public (string, Token) GenerateToken( - User user, - Application application, - string[] scopes, - Instant expires - ) - { - if (!AuthUtils.ValidateScopes(application, scopes)) - { - throw new ApiError.BadRequest( - "Invalid scopes requested for this token", - "scopes", - scopes - ); - } - - (string? token, byte[]? hash) = GenerateToken(); - return ( - token, - new Token - { - Id = snowflakeGenerator.GenerateSnowflake(), - Hash = hash, - Application = application, - User = user, - ExpiresAt = expires, - Scopes = scopes, - } - ); - } - - /// - /// Generates a token for the given user and adds it to the database, returning a fully formed auth response for the user. - /// This method is always called at the end of an endpoint method, so the resulting token - /// (and user, if this is a registration request) is also saved to the database. - /// - public async Task GenerateUserTokenAsync( - User user, - CancellationToken ct = default - ) - { - Application frontendApp = await db.GetFrontendApplicationAsync(ct); - - (string? tokenStr, Token? token) = GenerateToken( - user, - frontendApp, - ["*"], - clock.GetCurrentInstant() + Duration.FromDays(365) - ); - db.Add(token); - - _logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); - - await db.SaveChangesAsync(ct); - - return new CallbackResponse( - true, - null, - null, - await userRenderer.RenderUserAsync(user, user, renderMembers: false, ct: ct), - tokenStr, - token.ExpiresAt - ); - } - - private Task HashPasswordAsync( - User user, - string password, - CancellationToken ct = default - ) => Task.Run(() => _passwordHasher.HashPassword(user, password), ct); - - private Task VerifyHashedPasswordAsync( - User user, - string providedPassword, - CancellationToken ct = default - ) => - Task.Run( - () => _passwordHasher.VerifyHashedPassword(user, user.Password!, providedPassword), - ct - ); - - private static (string, byte[]) GenerateToken() - { - string token = AuthUtils.RandomUrlUnsafeToken(); - byte[] hash = SHA512.HashData(Convert.FromBase64String(token)); - - return (token, hash); - } - - private static void AssertValidAuthType(AuthType authType, FediverseApplication? instance) - { - if (authType == AuthType.Fediverse && instance == null) - throw new FoxnounsError("Fediverse authentication requires an instance."); - if (authType != AuthType.Fediverse && instance != null) - throw new FoxnounsError("Non-Fediverse authentication does not require an instance."); - } -} diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs deleted file mode 100644 index 225461c..0000000 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Diagnostics.CodeAnalysis; -using System.Net; -using System.Web; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; -using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; - -namespace Foxnouns.Backend.Services.Auth; - -public partial class FediverseAuthService -{ - private string MastodonRedirectUri(string instance) => - $"{config.BaseUrl}/auth/callback/mastodon/{instance}"; - - private async Task CreateMastodonApplicationAsync( - string instance, - Snowflake? existingAppId = null - ) - { - HttpResponseMessage resp = await client.PostAsJsonAsync( - $"https://{instance}/api/v1/apps", - new CreateMastodonApplicationRequest( - $"pronouns.cc (+{config.BaseUrl})", - MastodonRedirectUri(instance), - "read read:accounts", - config.BaseUrl - ) - ); - resp.EnsureSuccessStatusCode(); - - PartialMastodonApplication? mastodonApp = - await resp.Content.ReadFromJsonAsync(); - if (mastodonApp == null) - { - throw new FoxnounsError( - $"Application created on Mastodon-compatible instance {instance} was null" - ); - } - - FediverseApplication app; - - if (existingAppId == null) - { - app = new FediverseApplication - { - Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(), - ClientId = mastodonApp.ClientId, - ClientSecret = mastodonApp.ClientSecret, - Domain = instance, - InstanceType = FediverseInstanceType.MastodonApi, - }; - - db.Add(app); - } - else - { - app = - await db.FediverseApplications.FindAsync(existingAppId) - ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null"); - - app.ClientId = mastodonApp.ClientId; - app.ClientSecret = mastodonApp.ClientSecret; - app.InstanceType = FediverseInstanceType.MastodonApi; - } - - await db.SaveChangesAsync(); - - return app; - } - - private async Task GetMastodonUserAsync( - FediverseApplication app, - string code, - string? state = null - ) - { - if (state != null) - await keyCacheService.ValidateAuthStateAsync(state); - - HttpResponseMessage tokenResp = await client.PostAsync( - MastodonTokenUri(app.Domain), - new FormUrlEncodedContent( - new Dictionary - { - { "grant_type", "authorization_code" }, - { "code", code }, - { "scope", "read:accounts" }, - { "client_id", app.ClientId }, - { "client_secret", app.ClientSecret }, - { "redirect_uri", MastodonRedirectUri(app.Domain) }, - } - ) - ); - if (tokenResp.StatusCode == HttpStatusCode.Unauthorized) - { - throw new FoxnounsError($"Application for instance {app.Domain} was invalid"); - } - - tokenResp.EnsureSuccessStatusCode(); - string? token = ( - await tokenResp.Content.ReadFromJsonAsync() - )?.AccessToken; - if (token == null) - { - throw new FoxnounsError($"Token response from instance {app.Domain} was invalid"); - } - - var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentUserUri(app.Domain)); - req.Headers.Add("Authorization", $"Bearer {token}"); - - HttpResponseMessage currentUserResp = await client.SendAsync(req); - currentUserResp.EnsureSuccessStatusCode(); - FediverseUser? user = await currentUserResp.Content.ReadFromJsonAsync(); - if (user == null) - { - throw new FoxnounsError($"User response from instance {app.Domain} was invalid"); - } - - return user; - } - - private record MastodonTokenResponse([property: J("access_token")] string AccessToken); - - private async Task GenerateMastodonAuthUrlAsync( - FediverseApplication app, - bool forceRefresh, - string? state = null - ) - { - if (forceRefresh) - { - _logger.Information( - "An app credentials refresh was requested for {ApplicationId}, creating a new application", - app.Id - ); - app = await CreateMastodonApplicationAsync(app.Domain, app.Id); - } - - state ??= HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync()); - - return $"https://{app.Domain}/oauth/authorize?response_type=code" - + $"&client_id={app.ClientId}" - + $"&scope={HttpUtility.UrlEncode("read:accounts")}" - + $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}" - + $"&state={state}"; - } - - private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token"; - - private static string MastodonCurrentUserUri(string instance) => - $"https://{instance}/api/v1/accounts/verify_credentials"; - - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - private record PartialMastodonApplication( - [property: J("name")] string Name, - [property: J("client_id")] string ClientId, - [property: J("client_secret")] string ClientSecret - ); - - private record CreateMastodonApplicationRequest( - [property: J("client_name")] string ClientName, - [property: J("redirect_uris")] string RedirectUris, - [property: J("scopes")] string Scopes, - [property: J("website")] string Website - ); -} diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs deleted file mode 100644 index bf6c4b4..0000000 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Net; -using System.Text.Json.Serialization; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; - -namespace Foxnouns.Backend.Services.Auth; - -public partial class FediverseAuthService -{ - private static string MisskeyAppUri(string instance) => $"https://{instance}/api/app/create"; - - private static string MisskeyTokenUri(string instance) => - $"https://{instance}/api/auth/session/userkey"; - - private static string MisskeyGenerateSessionUri(string instance) => - $"https://{instance}/api/auth/session/generate"; - - private async Task CreateMisskeyApplicationAsync( - string instance, - Snowflake? existingAppId = null - ) - { - HttpResponseMessage resp = await client.PostAsJsonAsync( - MisskeyAppUri(instance), - new CreateMisskeyApplicationRequest( - $"pronouns.cc (+{config.BaseUrl})", - $"pronouns.cc on {config.BaseUrl}", - ["read:account"], - MastodonRedirectUri(instance) - ) - ); - resp.EnsureSuccessStatusCode(); - - PartialMisskeyApplication? misskeyApp = - await resp.Content.ReadFromJsonAsync(); - if (misskeyApp == null) - { - throw new FoxnounsError( - $"Application created on Misskey-compatible instance {instance} was null" - ); - } - - FediverseApplication app; - - if (existingAppId == null) - { - app = new FediverseApplication - { - Id = existingAppId ?? snowflakeGenerator.GenerateSnowflake(), - ClientId = misskeyApp.Id, - ClientSecret = misskeyApp.Secret, - Domain = instance, - InstanceType = FediverseInstanceType.MisskeyApi, - }; - - db.Add(app); - } - else - { - app = - await db.FediverseApplications.FindAsync(existingAppId) - ?? throw new FoxnounsError($"Existing app with ID {existingAppId} was null"); - - app.ClientId = misskeyApp.Id; - app.ClientSecret = misskeyApp.Secret; - app.InstanceType = FediverseInstanceType.MisskeyApi; - } - - await db.SaveChangesAsync(); - - return app; - } - - private record GetMisskeySessionUserKeyRequest( - [property: JsonPropertyName("appSecret")] string Secret, - [property: JsonPropertyName("token")] string Token - ); - - private record GetMisskeySessionUserKeyResponse( - [property: JsonPropertyName("user")] FediverseUser User - ); - - private async Task GetMisskeyUserAsync(FediverseApplication app, string code) - { - HttpResponseMessage resp = await client.PostAsJsonAsync( - MisskeyTokenUri(app.Domain), - new GetMisskeySessionUserKeyRequest(app.ClientSecret, code) - ); - if (resp.StatusCode == HttpStatusCode.Unauthorized) - { - throw new FoxnounsError($"Application for instance {app.Domain} was invalid"); - } - - resp.EnsureSuccessStatusCode(); - GetMisskeySessionUserKeyResponse? userResp = - await resp.Content.ReadFromJsonAsync(); - if (userResp == null) - { - throw new FoxnounsError($"User response from instance {app.Domain} was invalid"); - } - - return userResp.User; - } - - private async Task GenerateMisskeyAuthUrlAsync( - FediverseApplication app, - bool forceRefresh - ) - { - if (forceRefresh) - { - _logger.Information( - "An app credentials refresh was requested for {ApplicationId}, creating a new application", - app.Id - ); - app = await CreateMisskeyApplicationAsync(app.Domain, app.Id); - } - - HttpResponseMessage resp = await client.PostAsJsonAsync( - MisskeyGenerateSessionUri(app.Domain), - new CreateMisskeySessionUriRequest(app.ClientSecret) - ); - resp.EnsureSuccessStatusCode(); - - CreateMisskeySessionUriResponse? misskeyResp = - await resp.Content.ReadFromJsonAsync(); - if (misskeyResp == null) - throw new FoxnounsError($"Session create response for app {app.Id} was null"); - - return misskeyResp.Url; - } - - private record CreateMisskeySessionUriRequest( - [property: JsonPropertyName("appSecret")] string Secret - ); - - private record CreateMisskeySessionUriResponse( - [property: JsonPropertyName("token")] string Token, - [property: JsonPropertyName("url")] string Url - ); - - private record CreateMisskeyApplicationRequest( - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("description")] string Description, - [property: JsonPropertyName("permission")] string[] Permissions, - [property: JsonPropertyName("callbackUrl")] string CallbackUrl - ); - - private record PartialMisskeyApplication( - [property: JsonPropertyName("id")] string Id, - [property: JsonPropertyName("secret")] string Secret - ); -} diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs deleted file mode 100644 index 3ffc28d..0000000 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; - -namespace Foxnouns.Backend.Services.Auth; - -public partial class FediverseAuthService( - ILogger logger, - Config config, - DatabaseContext db, - HttpClient client, - KeyCacheService keyCacheService, - ISnowflakeGenerator snowflakeGenerator -) -{ - private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0"; - private readonly ILogger _logger = logger.ForContext(); - - public async Task GenerateAuthUrlAsync( - string instance, - bool forceRefresh, - string? state = null - ) - { - FediverseApplication app = await GetApplicationAsync(instance); - return await GenerateAuthUrlAsync(app, forceRefresh || app.ForceRefresh, state); - } - - // thank you, gargron and syuilo, for agreeing on a name for *once* in your lives, - // and having both mastodon and misskey use "username" in the self user response - public record FediverseUser( - [property: J("id")] string Id, - [property: J("username")] string Username - ); - - public async Task GetApplicationAsync(string instance) - { - FediverseApplication? app = await db.FediverseApplications.FirstOrDefaultAsync(a => - a.Domain == instance - ); - if (app != null) - return app; - - _logger.Debug("No application for fediverse instance {Instance}, creating it", instance); - - string softwareName = await GetSoftwareNameAsync(instance); - - if (IsMastodonCompatible(softwareName)) - return await CreateMastodonApplicationAsync(instance); - if (IsMisskeyCompatible(softwareName)) - return await CreateMisskeyApplicationAsync(instance); - - throw new ApiError.BadRequest($"{softwareName} is not a supported instance type, sorry."); - } - - private async Task GetSoftwareNameAsync(string instance) - { - _logger.Debug("Requesting software name for fediverse instance {Instance}", instance); - - HttpResponseMessage wellKnownResp = await client.GetAsync( - new Uri($"https://{instance}/.well-known/nodeinfo") - ); - wellKnownResp.EnsureSuccessStatusCode(); - - WellKnownResponse? wellKnown = - await wellKnownResp.Content.ReadFromJsonAsync(); - string? nodeInfoUrl = wellKnown?.Links.FirstOrDefault(l => l.Rel == NodeInfoRel)?.Href; - if (nodeInfoUrl == null) - { - throw new FoxnounsError( - $".well-known/nodeinfo response for instance {instance} was invalid, no nodeinfo link" - ); - } - - HttpResponseMessage nodeInfoResp = await client.GetAsync(nodeInfoUrl); - nodeInfoResp.EnsureSuccessStatusCode(); - - PartialNodeInfo? nodeInfo = await nodeInfoResp.Content.ReadFromJsonAsync(); - return nodeInfo?.Software.Name - ?? throw new FoxnounsError( - $"Nodeinfo response for instance {instance} was invalid, no software name" - ); - } - - private async Task GenerateAuthUrlAsync( - FediverseApplication app, - bool forceRefresh, - string? state = null - ) => - app.InstanceType switch - { - FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync( - app, - forceRefresh, - state - ), - FediverseInstanceType.MisskeyApi => await GenerateMisskeyAuthUrlAsync( - app, - forceRefresh - ), - _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), - }; - - public async Task GetRemoteFediverseUserAsync( - FediverseApplication app, - string code, - string? state = null - ) => - app.InstanceType switch - { - FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state), - FediverseInstanceType.MisskeyApi => await GetMisskeyUserAsync(app, code), - _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), - }; - - private static readonly string[] MastodonSoftwareNames = - [ - "mastodon", - "hometown", - "akkoma", - "pleroma", - "iceshrimp.net", - "iceshrimp", - "gotosocial", - "pixelfed", - ]; - - private static readonly string[] MisskeySoftwareNames = - [ - "misskey", - "foundkey", - "calckey", - "firefish", - "sharkey", - ]; - - private static bool IsMastodonCompatible(string softwareName) => - MastodonSoftwareNames.Any(n => - string.Equals(softwareName, n, StringComparison.InvariantCultureIgnoreCase) - ); - - private static bool IsMisskeyCompatible(string softwareName) => - MisskeySoftwareNames.Any(n => - string.Equals(softwareName, n, StringComparison.InvariantCultureIgnoreCase) - ); - - private record WellKnownResponse([property: J("links")] WellKnownLink[] Links); - - private record WellKnownLink( - [property: J("rel")] string Rel, - [property: J("href")] string Href - ); - - private record PartialNodeInfo([property: J("software")] NodeInfoSoftware Software); - - private record NodeInfoSoftware([property: J("name")] string Name); -} diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs deleted file mode 100644 index d884fda..0000000 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Text.Json.Serialization; - -namespace Foxnouns.Backend.Services.Auth; - -public partial class RemoteAuthService -{ - private readonly Uri _discordTokenUri = new("https://discord.com/api/oauth2/token"); - private readonly Uri _discordUserUri = new("https://discord.com/api/v10/users/@me"); - - public async Task RequestDiscordTokenAsync( - string code, - CancellationToken ct = default - ) - { - var redirectUri = $"{config.BaseUrl}/auth/callback/discord"; - HttpResponseMessage resp = await client.PostAsync( - _discordTokenUri, - new FormUrlEncodedContent( - new Dictionary - { - { "client_id", config.DiscordAuth.ClientId! }, - { "client_secret", config.DiscordAuth.ClientSecret! }, - { "grant_type", "authorization_code" }, - { "code", code }, - { "redirect_uri", redirectUri }, - } - ), - ct - ); - if (!resp.IsSuccessStatusCode) - { - string respBody = await resp.Content.ReadAsStringAsync(ct); - _logger.Error( - "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", - (int)resp.StatusCode, - respBody - ); - throw new FoxnounsError("Invalid Discord OAuth response"); - } - - OauthTokenResponse? token = await resp.Content.ReadFromJsonAsync(ct); - if (token == null) - throw new FoxnounsError("Discord token response was null"); - - var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri); - req.Headers.Add("Authorization", $"{token.TokenType} {token.AccessToken}"); - - HttpResponseMessage resp2 = await client.SendAsync(req, ct); - resp2.EnsureSuccessStatusCode(); - DiscordUserResponse? user = await resp2.Content.ReadFromJsonAsync(ct); - if (user == null) - throw new FoxnounsError("Discord user response was null"); - - return new RemoteUser(user.Id, user.Username); - } - - // ReSharper disable once ClassNeverInstantiated.Local - private record DiscordUserResponse( - [property: JsonPropertyName("id")] string Id, - [property: JsonPropertyName("username")] string Username - ); -} diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs deleted file mode 100644 index 938ba32..0000000 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace Foxnouns.Backend.Services.Auth; - -public partial class RemoteAuthService -{ - private readonly Uri _googleTokenUri = new("https://oauth2.googleapis.com/token"); - - public async Task RequestGoogleTokenAsync( - string code, - CancellationToken ct = default - ) - { - var redirectUri = $"{config.BaseUrl}/auth/callback/google"; - HttpResponseMessage resp = await client.PostAsync( - _googleTokenUri, - new FormUrlEncodedContent( - new Dictionary - { - { "client_id", config.GoogleAuth.ClientId! }, - { "client_secret", config.GoogleAuth.ClientSecret! }, - { "grant_type", "authorization_code" }, - { "scope", "openid https://www.googleapis.com/auth/userinfo.email" }, - { "code", code }, - { "redirect_uri", redirectUri }, - } - ), - ct - ); - if (!resp.IsSuccessStatusCode) - { - string respBody = await resp.Content.ReadAsStringAsync(ct); - _logger.Error( - "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", - (int)resp.StatusCode, - respBody - ); - throw new FoxnounsError("Invalid Google OAuth response"); - } - - GoogleTokenResponse? token = await resp.Content.ReadFromJsonAsync(ct); - if (token == null) - throw new FoxnounsError("Google token response was null"); - - byte[] rawIdToken = Convert.FromBase64String(token.IdToken.Split(".")[1]); - GoogleUser? user = JsonSerializer.Deserialize( - Encoding.UTF8.GetString(rawIdToken) - ); - if (user == null) - throw new FoxnounsError("Google user was null"); - - return new RemoteUser(user.Id, user.Email); - } - - // ReSharper disable once ClassNeverInstantiated.Local - private record GoogleTokenResponse([property: JsonPropertyName("id_token")] string IdToken); - - private record GoogleUser( - [property: JsonPropertyName("sub")] string Id, - [property: JsonPropertyName("email")] string Email - ); -} diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs deleted file mode 100644 index 45b9161..0000000 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Text.Json.Serialization; - -// ReSharper disable ClassNeverInstantiated.Local - -namespace Foxnouns.Backend.Services.Auth; - -public partial class RemoteAuthService -{ - private readonly Uri _tumblrTokenUri = new("https://api.tumblr.com/v2/oauth2/token"); - private readonly Uri _tumblrUserUri = new("https://api.tumblr.com/v2/user/info"); - - public async Task RequestTumblrTokenAsync( - string code, - CancellationToken ct = default - ) - { - var redirectUri = $"{config.BaseUrl}/auth/callback/tumblr"; - HttpResponseMessage resp = await client.PostAsync( - _tumblrTokenUri, - new FormUrlEncodedContent( - new Dictionary - { - { "client_id", config.TumblrAuth.ClientId! }, - { "client_secret", config.TumblrAuth.ClientSecret! }, - { "grant_type", "authorization_code" }, - { "code", code }, - { "scope", "basic" }, - { "redirect_uri", redirectUri }, - } - ), - ct - ); - if (!resp.IsSuccessStatusCode) - { - string respBody = await resp.Content.ReadAsStringAsync(ct); - _logger.Error( - "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", - (int)resp.StatusCode, - respBody - ); - throw new FoxnounsError("Invalid Tumblr OAuth response"); - } - - OauthTokenResponse? token = await resp.Content.ReadFromJsonAsync(ct); - if (token == null) - throw new FoxnounsError("Tumblr token response was null"); - - var req = new HttpRequestMessage(HttpMethod.Get, _tumblrUserUri); - req.Headers.Add("Authorization", $"Bearer {token.AccessToken}"); - - HttpResponseMessage resp2 = await client.SendAsync(req, ct); - if (!resp2.IsSuccessStatusCode) - { - string respBody = await resp2.Content.ReadAsStringAsync(ct); - _logger.Error( - "Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", - (int)resp2.StatusCode, - respBody - ); - throw new FoxnounsError("Invalid Tumblr user response"); - } - - TumblrData? data = await resp2.Content.ReadFromJsonAsync(ct); - if (data == null) - throw new FoxnounsError("Tumblr user response was null"); - - TumblrBlog? blog = data.Response.User.Blogs.FirstOrDefault(b => b.Primary); - if (blog == null) - throw new FoxnounsError("Tumblr user doesn't have a primary blog"); - - return new RemoteUser(blog.Uuid, blog.Name); - } - - // tumblr why - private record TumblrData( - [property: JsonPropertyName("meta")] TumblrMeta Meta, - [property: JsonPropertyName("response")] TumblrResponse Response - ); - - private record TumblrMeta( - [property: JsonPropertyName("status")] int Status, - [property: JsonPropertyName("msg")] string Message - ); - - private record TumblrResponse([property: JsonPropertyName("user")] TumblrUser User); - - private record TumblrUser([property: JsonPropertyName("blogs")] TumblrBlog[] Blogs); - - private record TumblrBlog( - [property: JsonPropertyName("name")] string Name, - [property: JsonPropertyName("primary")] bool Primary, - [property: JsonPropertyName("uuid")] string Uuid - ); -} diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs deleted file mode 100644 index 93b006b..0000000 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; -using System.Web; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; -using Foxnouns.Backend.Utils; -using Humanizer; -using Microsoft.EntityFrameworkCore; - -namespace Foxnouns.Backend.Services.Auth; - -public partial class RemoteAuthService( - HttpClient client, - Config config, - ILogger logger, - DatabaseContext db, - KeyCacheService keyCacheService -) -{ - private readonly ILogger _logger = logger.ForContext(); - - public record RemoteUser(string Id, string Username); - - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - private record OauthTokenResponse( - [property: JsonPropertyName("access_token")] string AccessToken, - [property: JsonPropertyName("token_type")] string TokenType - ); - - /// - /// Validates whether a user can still add a new account of the given AuthType, and throws an error if they can't. - /// - /// The user to check. - /// The auth type to check. - /// The optional fediverse instance to generate a state for. - /// A state for the given auth type and user ID. - /// The given user can't add another account of this type. - /// This exception should not be caught by controller code. - public async Task ValidateAddAccountRequestAsync( - Snowflake userId, - AuthType authType, - string? instance = null - ) - { - int existingAccounts = await db - .AuthMethods.Where(m => m.UserId == userId && m.AuthType == authType) - .CountAsync(); - if (existingAccounts > AuthUtils.MaxAuthMethodsPerType) - { - throw new ApiError.BadRequest( - $"Too many linked {authType.Humanize()} accounts, maximum of {AuthUtils.MaxAuthMethodsPerType} per account." - ); - } - - return HttpUtility.UrlEncode( - await keyCacheService.GenerateAddExtraAccountStateAsync(authType, userId, instance) - ); - } - - /// - /// Checks whether the given state is correct for the given user/auth type combination. - /// - /// The state doesn't match. - /// This exception should not be caught by controller code. - public async Task ValidateAddAccountStateAsync( - string state, - Snowflake userId, - AuthType authType, - string? instance = null - ) - { - AddExtraAccountState? accountState = await keyCacheService.GetAddExtraAccountStateAsync( - state - ); - if ( - accountState == null - || accountState.AuthType != authType - || accountState.UserId != userId - || (instance != null && accountState.Instance != instance) - ) - { - throw new ApiError.BadRequest("Invalid state", "state", state); - } - } -} diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs new file mode 100644 index 0000000..87494ed --- /dev/null +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -0,0 +1,57 @@ +using System.Security.Cryptography; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Identity; +using NodaTime; + +namespace Foxnouns.Backend.Services; + +public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) +{ + private readonly PasswordHasher _passwordHasher = new(); + + /// + /// Creates a new user with the given email address and password. + /// This method does not save the resulting user, the caller must still call . + /// + public async Task CreateUserWithPasswordAsync(string username, string email, string password) + { + var user = new User + { + Id = snowflakeGenerator.GenerateSnowflake(), + Username = username, + AuthMethods = { new AuthMethod { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } } + }; + + db.Add(user); + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password)); + + return user; + } + + public (string, Token) GenerateToken(User user, Application application, string[] scopes, Instant expires) + { + if (!OauthUtils.ValidateScopes(application, scopes)) + throw new ApiError.BadRequest("Invalid scopes requested for this token"); + + var (token, hash) = GenerateToken(); + return (token, new Token + { + Id = snowflakeGenerator.GenerateSnowflake(), + Hash = hash, + Application = application, + User = user, + ExpiresAt = expires, + Scopes = scopes + }); + } + + private static (string, byte[]) GenerateToken() + { + var token = OauthUtils.RandomToken(48); + var hash = SHA512.HashData(Convert.FromBase64String(token)); + + return (token, hash); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/Caching/NoticeCacheService.cs b/Foxnouns.Backend/Services/Caching/NoticeCacheService.cs deleted file mode 100644 index 2a0b1f9..0000000 --- a/Foxnouns.Backend/Services/Caching/NoticeCacheService.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Services.Caching; - -public class NoticeCacheService(IServiceProvider serviceProvider, IClock clock, ILogger logger) - : SingletonCacheService(serviceProvider, clock, logger) -{ - public override Duration MaxAge { get; init; } = Duration.FromMinutes(5); - - public override Func< - DatabaseContext, - CancellationToken, - Task - > FetchFunc { get; init; } = - async (db, ct) => - await db - .Notices.Where(n => - n.StartTime < clock.GetCurrentInstant() && n.EndTime > clock.GetCurrentInstant() - ) - .OrderByDescending(n => n.Id) - .FirstOrDefaultAsync(ct); -} diff --git a/Foxnouns.Backend/Services/Caching/SingletonCacheService.cs b/Foxnouns.Backend/Services/Caching/SingletonCacheService.cs deleted file mode 100644 index 87b19a7..0000000 --- a/Foxnouns.Backend/Services/Caching/SingletonCacheService.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using NodaTime; - -namespace Foxnouns.Backend.Services.Caching; - -public abstract class SingletonCacheService( - IServiceProvider serviceProvider, - IClock clock, - ILogger logger -) - where T : class -{ - private T? _item; - private Instant _lastUpdated = Instant.MinValue; - private readonly SemaphoreSlim _semaphore = new(1, 1); - private readonly ILogger _logger = logger.ForContext>(); - - public virtual Duration MaxAge { get; init; } = Duration.FromMinutes(5); - - public virtual Func> FetchFunc { get; init; } = - (_, __) => Task.FromResult(null); - - public async Task GetAsync(CancellationToken ct = default) - { - await _semaphore.WaitAsync(ct); - try - { - if (_lastUpdated > clock.GetCurrentInstant() - MaxAge) - { - return _item; - } - - _logger.Debug("Cached item of type {Type} is expired, fetching it.", typeof(T)); - - await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope(); - await using DatabaseContext db = - scope.ServiceProvider.GetRequiredService(); - - T? item = await FetchFunc(db, ct); - _item = item; - _lastUpdated = clock.GetCurrentInstant(); - return item; - } - finally - { - _semaphore.Release(); - } - } -} diff --git a/Foxnouns.Backend/Services/DataCleanupService.cs b/Foxnouns.Backend/Services/DataCleanupService.cs deleted file mode 100644 index ee60bb8..0000000 --- a/Foxnouns.Backend/Services/DataCleanupService.cs +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Diagnostics; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; -using Microsoft.EntityFrameworkCore; -using NodaTime; -using NodaTime.Extensions; - -namespace Foxnouns.Backend.Services; - -public class DataCleanupService( - DatabaseContext db, - IClock clock, - ILogger logger, - ObjectStorageService objectStorageService -) -{ - private readonly ILogger _logger = logger.ForContext(); - - public async Task InvokeAsync(CancellationToken ct = default) - { - _logger.Debug("Cleaning up expired users"); - await CleanUsersAsync(ct); - - _logger.Debug("Cleaning up expired data exports"); - await CleanExportsAsync(ct); - } - - private async Task CleanUsersAsync(CancellationToken ct = default) - { - Instant selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter; - Instant suspendExpires = clock.GetCurrentInstant() - User.DeleteSuspendedAfter; - List users = await db - .Users.Include(u => u.Members) - .Include(u => u.DataExports) - .Where(u => - u.Deleted - && ( - (u.DeletedBy != null && u.DeletedAt < suspendExpires) - || (u.DeletedBy == null && u.DeletedAt < selfDeleteExpires) - ) - ) - .OrderBy(u => u.Id) - .AsSplitQuery() - .ToListAsync(ct); - if (users.Count == 0) - return; - - _logger.Debug( - "Deleting {Count} users that have been deleted for over 30 days or suspended for over 180 days", - users.Count - ); - - var sw = new Stopwatch(); - - await Task.WhenAll(users.Select(u => CleanUserAsync(u, ct))); - - await db.SaveChangesAsync(ct); - _logger.Information( - "Deleted {Count} users, their members, and their exports in {Time}", - users.Count, - sw.ElapsedDuration() - ); - } - - private Task CleanUserAsync(User user, CancellationToken ct = default) - { - var tasks = new List(); - - if (user.Avatar != null) - tasks.Add(objectStorageService.DeleteUserAvatarAsync(user.Id, user.Avatar, ct)); - - tasks.AddRange( - user.Members.Select(member => - objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar!, ct) - ) - ); - - tasks.AddRange( - user.DataExports.Select(export => - objectStorageService.RemoveObjectAsync( - ExportPath(export.UserId, export.Filename), - ct - ) - ) - ); - - db.Remove(user); - return Task.WhenAll(tasks); - } - - private async Task CleanExportsAsync(CancellationToken ct = default) - { - var minExpiredId = Snowflake.FromInstant(clock.GetCurrentInstant() - DataExport.Expiration); - List exports = await db - .DataExports.Where(d => d.Id < minExpiredId) - .ToListAsync(ct); - if (exports.Count == 0) - return; - - _logger.Debug("Deleting {Count} expired exports", exports.Count); - - foreach (DataExport? export in exports) - { - _logger.Debug("Deleting export {ExportId}", export.Id); - await objectStorageService.RemoveObjectAsync( - ExportPath(export.UserId, export.Filename), - ct - ); - db.Remove(export); - } - - await db.SaveChangesAsync(ct); - } - - private static string ExportPath(Snowflake userId, string b64) => - $"data-exports/{userId}/{b64}/data-export.zip"; -} diff --git a/Foxnouns.Backend/Services/EmailRateLimiter.cs b/Foxnouns.Backend/Services/EmailRateLimiter.cs deleted file mode 100644 index cc2dbb4..0000000 --- a/Foxnouns.Backend/Services/EmailRateLimiter.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Collections.Concurrent; -using System.Threading.RateLimiting; -using NodaTime; -using NodaTime.Extensions; - -namespace Foxnouns.Backend.Services; - -public class EmailRateLimiter -{ - private readonly ConcurrentDictionary _limiters = new(); - - private readonly FixedWindowRateLimiterOptions _limiterOptions = new() - { - Window = TimeSpan.FromHours(2), - PermitLimit = 3, - }; - - private RateLimiter GetLimiter(string bucket) => - _limiters.GetOrAdd(bucket, _ => new FixedWindowRateLimiter(_limiterOptions)); - - public bool IsLimited(string bucket, out Duration retryAfter) - { - RateLimiter limiter = GetLimiter(bucket); - RateLimitLease lease = limiter.AttemptAcquire(); - - if (!lease.IsAcquired) - { - retryAfter = lease.TryGetMetadata(MetadataName.RetryAfter, out TimeSpan timeSpan) - ? timeSpan.ToDuration() - : default; - } - else - { - retryAfter = Duration.Zero; - } - - return !lease.IsAcquired; - } -} diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs deleted file mode 100644 index 5c59bce..0000000 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; -using NodaTime; -using StackExchange.Redis; - -namespace Foxnouns.Backend.Services; - -public class KeyCacheService(Config config) -{ - public ConnectionMultiplexer Multiplexer { get; } = - ConnectionMultiplexer.Connect(config.Database.Redis); - - public async Task SetKeyAsync(string key, string value, Duration expireAfter) => - await Multiplexer - .GetDatabase() - .StringSetAsync(key, value, expiry: expireAfter.ToTimeSpan()); - - public async Task GetKeyAsync(string key, bool delete = false) => - delete - ? await Multiplexer.GetDatabase().StringGetDeleteAsync(key) - : await Multiplexer.GetDatabase().StringGetAsync(key); - - public async Task DeleteKeyAsync(string key) => - await Multiplexer.GetDatabase().KeyDeleteAsync(key); - - public async Task SetKeyAsync(string key, T obj, Duration expiresAt) - where T : class - { - string value = JsonConvert.SerializeObject(obj); - await SetKeyAsync(key, value, expiresAt); - } - - public async Task GetKeyAsync(string key, bool delete = false) - where T : class - { - string? value = await GetKeyAsync(key, delete); - return value == null ? default : JsonConvert.DeserializeObject(value); - } -} diff --git a/Foxnouns.Backend/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs deleted file mode 100644 index 83458d6..0000000 --- a/Foxnouns.Backend/Services/MailService.cs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Coravel.Mailer.Mail; -using Coravel.Mailer.Mail.Interfaces; -using Coravel.Queuing.Interfaces; -using Foxnouns.Backend.Mailables; - -namespace Foxnouns.Backend.Services; - -public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config config) -{ - private readonly ILogger _logger = logger.ForContext(); - - public void QueueAccountCreationEmail(string to, string code) - { - queue.QueueAsyncTask(async () => - { - await SendEmailAsync( - to, - new AccountCreationMailable( - config, - new AccountCreationMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - } - ) - ); - }); - } - - public void QueueAddEmailAddressEmail(string to, string code, string username) - { - _logger.Debug("Sending add email address email to {ToEmail}", to); - queue.QueueAsyncTask(async () => - { - await SendEmailAsync( - to, - new AddEmailMailable( - config, - new AddEmailMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - Username = username, - } - ) - ); - }); - } - - public void QueueResetPasswordEmail(string to, string code) - { - _logger.Debug("Sending add email address email to {ToEmail}", to); - queue.QueueAsyncTask(async () => - { - await SendEmailAsync( - to, - new ResetPasswordMailable( - config, - new ResetPasswordMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - } - ) - ); - }); - } - - public void QueuePasswordChangedEmail(string to) - { - _logger.Debug("Sending add email address email to {ToEmail}", to); - queue.QueueAsyncTask(async () => - { - await SendEmailAsync( - to, - new PasswordChangedMailable( - config, - new PasswordChangedMailableView { BaseUrl = config.BaseUrl, To = to } - ) - ); - }); - } - - private async Task SendEmailAsync(string to, Mailable mailable) - { - try - { - await mailer.SendAsync(mailable); - } - catch (Exception exc) - { - _logger.Error(exc, "Sending email"); - } - } -} diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index b0efc8d..73d1998 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -1,104 +1,18 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Utils; -using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Services; -public class MemberRendererService(DatabaseContext db, Config config) +public class MemberRendererService(DatabaseContext db) { - public async Task> RenderUserMembersAsync(User user, Token? token) - { - bool canReadHiddenMembers = - token != null && token.UserId == user.Id && token.HasScope("member.read"); - bool renderUnlisted = - token != null && token.UserId == user.Id && token.HasScope("user.read_hidden"); - bool canReadMemberList = !user.ListHidden || canReadHiddenMembers; + public PartialMember RenderPartialMember(Member member) => new(member.Id, member.Name, + member.DisplayName, member.Bio, member.Names, member.Pronouns); - IEnumerable members = canReadMemberList - ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync() - : []; - if (!canReadHiddenMembers) - members = members.Where(m => !m.Unlisted); - return members.Select(m => RenderPartialMember(m, renderUnlisted)); - } - - public MemberResponse RenderMember( - Member member, - Token? token = null, - string? overrideSid = null - ) - { - bool renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); - - return new MemberResponse( - member.Id, - overrideSid ?? member.Sid, - member.Name, - member.DisplayName ?? member.Name, - member.Bio, - AvatarUrlFor(member), - member.Links, - member.Names, - member.Pronouns, - member.Fields, - member.ProfileFlags.Select(f => RenderPrideFlag(f.PrideFlag)), - RenderPartialUser(member.User), - renderUnlisted ? member.Unlisted : null - ); - } - - private PartialUser RenderPartialUser(User user) => - new( - user.Id, - user.Sid, - user.Username, - user.DisplayName, - AvatarUrlFor(user), - user.CustomPreferences - ); - - public PartialMember RenderPartialMember(Member member, bool renderUnlisted = false) => - new( - member.Id, - member.Sid, - member.Name, - member.DisplayName ?? member.Name, - member.Bio, - AvatarUrlFor(member), - member.Names, - member.Pronouns, - renderUnlisted ? member.Unlisted : null - ); - - public string? AvatarUrlFor(Member member) => - member.Avatar != null - ? $"{config.MediaBaseUrl}/members/{member.Id}/avatars/{member.Avatar}.webp" - : null; - - private string? AvatarUrlFor(User user) => - user.Avatar != null - ? $"{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 PrideFlagResponse RenderPrideFlag(PrideFlag flag) => - new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); -} + public record PartialMember( + Snowflake Id, + string Name, + string? DisplayName, + string? Bio, + IEnumerable Names, + IEnumerable Pronouns); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/MetricsCollectionService.cs b/Foxnouns.Backend/Services/MetricsCollectionService.cs deleted file mode 100644 index 8de0264..0000000 --- a/Foxnouns.Backend/Services/MetricsCollectionService.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Diagnostics; -using Foxnouns.Backend.Database; -using Microsoft.EntityFrameworkCore; -using NodaTime; -using Prometheus; -using ITimer = Prometheus.ITimer; - -namespace Foxnouns.Backend.Services; - -public class MetricsCollectionService(ILogger logger, IServiceProvider services, IClock clock) -{ - private readonly ILogger _logger = logger.ForContext(); - - private static readonly Duration Month = Duration.FromDays(30); - private static readonly Duration Week = Duration.FromDays(7); - private static readonly Duration Day = Duration.FromDays(1); - - public async Task CollectMetricsAsync(CancellationToken ct = default) - { - ITimer timer = FoxnounsMetrics.MetricsCollectionTime.NewTimer(); - Instant now = clock.GetCurrentInstant(); - - await using AsyncServiceScope scope = services.CreateAsyncScope(); - // ReSharper disable once SuggestVarOrType_SimpleTypes - await using var db = scope.ServiceProvider.GetRequiredService(); - - List users = await db - .Users.Where(u => !u.Deleted) - .Select(u => u.LastActive) - .ToListAsync(ct); - FoxnounsMetrics.UsersCount.Set(users.Count); - FoxnounsMetrics.UsersActiveMonthCount.Set(users.Count(i => i > now - Month)); - FoxnounsMetrics.UsersActiveWeekCount.Set(users.Count(i => i > now - Week)); - FoxnounsMetrics.UsersActiveDayCount.Set(users.Count(i => i > now - Day)); - - int memberCount = await db - .Members.Include(m => m.User) - .Where(m => !m.Unlisted && !m.User.ListHidden && !m.User.Deleted) - .CountAsync(ct); - FoxnounsMetrics.MemberCount.Set(memberCount); - - var process = Process.GetCurrentProcess(); - FoxnounsMetrics.ProcessPhysicalMemory.Set(process.WorkingSet64); - FoxnounsMetrics.ProcessVirtualMemory.Set(process.VirtualMemorySize64); - FoxnounsMetrics.ProcessPrivateMemory.Set(process.PrivateMemorySize64); - FoxnounsMetrics.ProcessThreads.Set(process.Threads.Count); - FoxnounsMetrics.ProcessHandles.Set(process.HandleCount); - - _logger.Information( - "Collected metrics in {DurationMilliseconds} ms", - timer.ObserveDuration().TotalMilliseconds - ); - } -} - -public class BackgroundMetricsCollectionService( - ILogger logger, - MetricsCollectionService metricsCollectionService -) : BackgroundService -{ - private readonly ILogger _logger = logger.ForContext(); - - protected override async Task ExecuteAsync(CancellationToken ct) - { - _logger.Information("Metrics are disabled, periodically collecting metrics manually"); - - using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); - while (await timer.WaitForNextTickAsync(ct)) - { - _logger.Debug("Collecting metrics manually"); - await metricsCollectionService.CollectMetricsAsync(ct); - } - } -} diff --git a/Foxnouns.Backend/Services/ModerationRendererService.cs b/Foxnouns.Backend/Services/ModerationRendererService.cs deleted file mode 100644 index c1d259a..0000000 --- a/Foxnouns.Backend/Services/ModerationRendererService.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Foxnouns.Backend.Services; - -public class ModerationRendererService( - UserRendererService userRenderer, - MemberRendererService memberRenderer -) -{ - public ReportResponse RenderReport(Report report) - { - return new ReportResponse( - report.Id, - userRenderer.RenderPartialUser(report.Reporter), - userRenderer.RenderPartialUser(report.TargetUser), - report.TargetMemberId != null - ? memberRenderer.RenderPartialMember(report.TargetMember!) - : null, - report.Status, - report.Reason, - report.Context, - report.TargetType, - report.TargetSnapshot != null - ? JsonConvert.DeserializeObject(report.TargetSnapshot) - : null - ); - } - - public AuditLogResponse RenderAuditLogEntry(AuditLogEntry entry) - { - PartialReport? report = null; - if (entry.Report != null) - { - report = new PartialReport( - entry.Report.Id, - entry.Report.ReporterId, - entry.Report.TargetUserId, - entry.Report.TargetMemberId, - entry.Report.Reason, - entry.Report.Context, - entry.Report.TargetType - ); - } - - return new AuditLogResponse( - Id: entry.Id, - Moderator: ToEntity(entry.ModeratorId, entry.ModeratorUsername)!, - TargetUser: ToEntity(entry.TargetUserId, entry.TargetUsername), - TargetMember: ToEntity(entry.TargetMemberId, entry.TargetMemberName), - Report: report, - Type: entry.Type, - Reason: entry.Reason, - ClearedFields: entry.ClearedFields - ); - } - - public NotificationResponse RenderNotification(Notification notification) => - new( - notification.Id, - notification.Type, - notification.Message, - notification.LocalizationKey, - notification.LocalizationParams, - notification.AcknowledgedAt != null - ); - - private static AuditLogEntity? ToEntity(Snowflake? id, string? username) => - id != null && username != null ? new AuditLogEntity(id.Value, username) : null; -} diff --git a/Foxnouns.Backend/Services/ModerationService.cs b/Foxnouns.Backend/Services/ModerationService.cs deleted file mode 100644 index 30d99ed..0000000 --- a/Foxnouns.Backend/Services/ModerationService.cs +++ /dev/null @@ -1,346 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Coravel.Queuing.Interfaces; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Jobs; -using Humanizer; -using Microsoft.EntityFrameworkCore; -using NodaTime; - -namespace Foxnouns.Backend.Services; - -public class ModerationService( - ILogger logger, - DatabaseContext db, - ISnowflakeGenerator snowflakeGenerator, - IClock clock -) -{ - private readonly ILogger _logger = logger.ForContext(); - - public async Task IgnoreReportAsync( - User moderator, - Report report, - string? reason = null - ) - { - _logger.Information( - "Moderator {ModeratorId} is ignoring report {ReportId} on user {TargetId}", - moderator.Id, - report.Id, - report.TargetUserId - ); - - var entry = new AuditLogEntry - { - Id = snowflakeGenerator.GenerateSnowflake(), - ModeratorId = moderator.Id, - ModeratorUsername = moderator.Username, - ReportId = report.Id, - Type = AuditLogEntryType.IgnoreReport, - Reason = reason, - }; - db.AuditLog.Add(entry); - - report.Status = ReportStatus.Closed; - db.Update(report); - - await db.SaveChangesAsync(); - return entry; - } - - public async Task QuerySensitiveDataAsync( - User moderator, - User target, - string reason - ) - { - _logger.Information( - "Moderator {ModeratorId} is querying sensitive data for {TargetId}", - moderator.Id, - target.Id - ); - - var entry = new AuditLogEntry - { - Id = snowflakeGenerator.GenerateSnowflake(), - ModeratorId = moderator.Id, - ModeratorUsername = moderator.Username, - TargetUserId = target.Id, - TargetUsername = target.Username, - Type = AuditLogEntryType.QuerySensitiveUserData, - Reason = reason, - }; - db.AuditLog.Add(entry); - - await db.SaveChangesAsync(); - return entry; - } - - public async Task ShowSensitiveDataAsync( - User moderator, - User target, - CancellationToken ct = default - ) - { - Snowflake cutoff = snowflakeGenerator.GenerateSnowflake( - clock.GetCurrentInstant() - Duration.FromDays(1) - ); - - return await db.AuditLog.AnyAsync( - e => - e.ModeratorId == moderator.Id - && e.TargetUserId == target.Id - && e.Type == AuditLogEntryType.QuerySensitiveUserData - && e.Id > cutoff, - ct - ); - } - - public async Task ExecuteSuspensionAsync( - User moderator, - User target, - Report? report, - string reason, - bool clearProfile - ) - { - _logger.Information( - "Moderator {ModeratorId} is suspending user {TargetId}", - moderator.Id, - target.Id - ); - var entry = new AuditLogEntry - { - Id = snowflakeGenerator.GenerateSnowflake(), - ModeratorId = moderator.Id, - ModeratorUsername = moderator.Username, - TargetUserId = target.Id, - TargetUsername = target.Username, - ReportId = report?.Id, - Type = AuditLogEntryType.SuspendUser, - Reason = reason, - }; - db.AuditLog.Add(entry); - - db.Notifications.Add( - new Notification - { - Id = snowflakeGenerator.GenerateSnowflake(), - TargetId = target.Id, - Type = NotificationType.Warning, - Message = null, - LocalizationKey = "notification.suspension", - LocalizationParams = { { "reason", reason } }, - } - ); - - target.Deleted = true; - target.DeletedAt = clock.GetCurrentInstant(); - target.DeletedBy = moderator.Id; - - if (report != null) - { - report.Status = ReportStatus.Closed; - db.Update(report); - } - - if (!clearProfile) - { - db.Update(target); - await db.SaveChangesAsync(); - return entry; - } - - _logger.Information("Clearing profile of user {TargetId}", target.Id); - - target.Username = $"deleted-user-{target.Id}"; - target.DisplayName = null; - target.Bio = null; - target.MemberTitle = null; - target.Links = []; - target.Timezone = null; - target.Names = []; - target.Pronouns = []; - target.Fields = []; - target.CustomPreferences = []; - target.ProfileFlags = []; - - UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(target.Id, null)); - - // TODO: also clear member profiles? - - db.Update(target); - await db.SaveChangesAsync(); - return entry; - } - - public async Task ExecuteWarningAsync( - User moderator, - User targetUser, - Member? targetMember, - Report? report, - string reason, - FieldsToClear[]? fieldsToClear - ) - { - _logger.Information( - "Moderator {ModeratorId} is warning user {TargetId} (member {TargetMemberId})", - moderator.Id, - targetUser.Id, - targetMember?.Id - ); - - string[]? fields = fieldsToClear?.Select(f => f.Humanize(LetterCasing.LowerCase)).ToArray(); - - var entry = new AuditLogEntry - { - Id = snowflakeGenerator.GenerateSnowflake(), - ModeratorId = moderator.Id, - ModeratorUsername = moderator.Username, - TargetUserId = targetUser.Id, - TargetUsername = targetUser.Username, - TargetMemberId = targetMember?.Id, - TargetMemberName = targetMember?.Name, - ReportId = report?.Id, - Type = - fields != null - ? AuditLogEntryType.WarnUserAndClearProfile - : AuditLogEntryType.WarnUser, - Reason = reason, - ClearedFields = fields, - }; - db.AuditLog.Add(entry); - - db.Notifications.Add( - new Notification - { - Id = snowflakeGenerator.GenerateSnowflake(), - TargetId = targetUser.Id, - Type = NotificationType.Warning, - Message = null, - LocalizationKey = - fieldsToClear != null - ? "notification.warning-cleared-fields" - : "notification.warning", - LocalizationParams = - { - { "reason", reason }, - { - "clearedFields", - string.Join( - "\n", - fieldsToClear?.Select(f => f.Humanize(LetterCasing.LowerCase)) ?? [] - ) - }, - }, - } - ); - - if (targetMember != null && fieldsToClear != null) - { - foreach (FieldsToClear field in fieldsToClear) - { - switch (field) - { - case FieldsToClear.DisplayName: - targetMember.DisplayName = null; - break; - case FieldsToClear.Avatar: - MemberAvatarUpdateJob.Enqueue( - new AvatarUpdatePayload(targetMember.Id, null) - ); - break; - case FieldsToClear.Bio: - targetMember.Bio = null; - break; - case FieldsToClear.Links: - targetMember.Links = []; - break; - case FieldsToClear.Names: - targetMember.Names = []; - break; - case FieldsToClear.Pronouns: - targetMember.Pronouns = []; - break; - case FieldsToClear.Fields: - targetMember.Fields = []; - break; - case FieldsToClear.Flags: - targetMember.ProfileFlags = []; - break; - // custom preferences can't be cleared on member-scoped warnings - case FieldsToClear.CustomPreferences: - default: - break; - } - } - - db.Update(targetMember); - } - else if (fieldsToClear != null) - { - foreach (FieldsToClear field in fieldsToClear) - { - switch (field) - { - case FieldsToClear.DisplayName: - targetUser.DisplayName = null; - break; - case FieldsToClear.Avatar: - UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(targetUser.Id, null)); - break; - case FieldsToClear.Bio: - targetUser.Bio = null; - break; - case FieldsToClear.Links: - targetUser.Links = []; - break; - case FieldsToClear.Names: - targetUser.Names = []; - break; - case FieldsToClear.Pronouns: - targetUser.Pronouns = []; - break; - case FieldsToClear.Fields: - targetUser.Fields = []; - break; - case FieldsToClear.Flags: - targetUser.ProfileFlags = []; - break; - case FieldsToClear.CustomPreferences: - targetUser.CustomPreferences = []; - break; - default: - break; - } - } - - db.Update(targetUser); - } - - if (report != null) - { - report.Status = ReportStatus.Closed; - db.Update(report); - } - - await db.SaveChangesAsync(); - - return entry; - } -} diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs deleted file mode 100644 index 5ced7fb..0000000 --- a/Foxnouns.Backend/Services/ObjectStorageService.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Minio; -using Minio.DataModel.Args; -using Minio.Exceptions; - -namespace Foxnouns.Backend.Services; - -public class ObjectStorageService(ILogger logger, Config config, IMinioClient minioClient) -{ - private readonly ILogger _logger = logger.ForContext(); - - public async Task RemoveObjectAsync(string path, CancellationToken ct = default) - { - _logger.Debug("Deleting object at path {Path}", path); - try - { - await minioClient.RemoveObjectAsync( - new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path), - ct - ); - } - catch (InvalidObjectNameException) - { - // ignore non-existent objects - } - } - - public async Task PutObjectAsync( - string path, - Stream data, - string contentType, - CancellationToken ct = default - ) - { - _logger.Debug( - "Putting object at path {Path} with length {Length} and content type {ContentType}", - path, - data.Length, - contentType - ); - - await minioClient.PutObjectAsync( - new PutObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(path) - .WithObjectSize(data.Length) - .WithStreamData(data) - .WithContentType(contentType), - ct - ); - } -} diff --git a/Foxnouns.Backend/Services/PeriodicTasksService.cs b/Foxnouns.Backend/Services/PeriodicTasksService.cs deleted file mode 100644 index e5efd28..0000000 --- a/Foxnouns.Backend/Services/PeriodicTasksService.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -namespace Foxnouns.Backend.Services; - -public class PeriodicTasksService(ILogger logger, IServiceProvider services) : BackgroundService -{ - private readonly ILogger _logger = logger.ForContext(); - - protected override async Task ExecuteAsync(CancellationToken ct) - { - using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1)); - while (await timer.WaitForNextTickAsync(ct)) - await RunPeriodicTasksAsync(ct); - } - - private async Task RunPeriodicTasksAsync(CancellationToken ct) - { - _logger.Debug("Running periodic tasks"); - - await using AsyncServiceScope scope = services.CreateAsyncScope(); - - // The type is literally written on the same line, we can just use `var` - // ReSharper disable SuggestVarOrType_SimpleTypes - var dataCleanupService = scope.ServiceProvider.GetRequiredService(); - // ReSharper restore SuggestVarOrType_SimpleTypes - - await dataCleanupService.InvokeAsync(ct); - } -} diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 0c1fc1b..0f22021 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -1,167 +1,36 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto; -using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; namespace Foxnouns.Backend.Services; -public class UserRendererService( - DatabaseContext db, - MemberRendererService memberRenderer, - Config config -) +public class UserRendererService(DatabaseContext db, MemberRendererService memberRendererService) { - public async Task RenderUserAsync( - User user, - User? selfUser = null, - Token? token = null, - bool renderMembers = true, - bool renderAuthMethods = false, - string? overrideSid = null, - bool renderSettings = false, - CancellationToken ct = default - ) => - await RenderUserInnerAsync( - user, - selfUser != null && selfUser.Id == user.Id, - token?.Scopes ?? [], - renderMembers, - renderAuthMethods, - overrideSid, - renderSettings, - ct - ); - - public async Task RenderUserInnerAsync( - User user, - bool isSelfUser, - string[] scopes, - bool renderMembers = true, - bool renderAuthMethods = false, - string? overrideSid = null, - bool renderSettings = false, - CancellationToken ct = default - ) + public async Task RenderUserAsync(User user, User? selfUser = null, bool renderMembers = true) { - scopes = scopes.ExpandScopes(); - bool tokenCanReadHiddenMembers = scopes.Contains("member.read") && isSelfUser; - bool tokenHidden = scopes.Contains("user.read_hidden") && isSelfUser; - bool tokenPrivileged = scopes.Contains("user.read_privileged") && isSelfUser; + renderMembers = renderMembers && (!user.ListHidden || selfUser?.Id == user.Id); - renderMembers = renderMembers && (!user.ListHidden || tokenCanReadHiddenMembers); - renderAuthMethods = renderAuthMethods && tokenPrivileged; - renderSettings = renderSettings && tokenHidden; - - IEnumerable members = renderMembers - ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct) - : []; - // Unless the user is requesting their own members AND the token can read hidden members, we filter out unlisted members. - if (!(isSelfUser && tokenCanReadHiddenMembers)) - members = members.Where(m => !m.Unlisted); - - List flags = await db - .UserFlags.Where(f => f.UserId == user.Id) - .OrderBy(f => f.Id) - .ToListAsync(ct); - - List authMethods = renderAuthMethods - ? await db - .AuthMethods.Where(a => a.UserId == user.Id) - .Include(a => a.FediverseApplication) - .ToListAsync(ct) - : []; - - int? utcOffset = null; - if ( - user.Timezone != null - && TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz) - ) - { - utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds; - } + var members = renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; return new UserResponse( - user.Id, - overrideSid ?? user.Sid, - user.Username, - user.DisplayName, - user.Bio, - user.MemberTitle, - AvatarUrlFor(user), - user.Links, - user.Names, - user.Pronouns, - user.Fields, - user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value))) - .ToDictionary(), - flags.Select(f => RenderPrideFlag(f.PrideFlag)), - utcOffset, - user.Role, - renderMembers - ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) - : null, - renderAuthMethods ? authMethods.Select(RenderAuthMethod) : null, - tokenHidden ? user.ListHidden : null, - tokenHidden ? user.LastActive : null, - tokenHidden ? user.LastSidReroll : null, - tokenHidden ? user.Timezone ?? "" : null, - tokenHidden ? user is { Deleted: true, DeletedBy: not null } : null, - tokenHidden ? user.Deleted : null, - renderSettings ? user.Settings : null - ); + user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, user.Avatar, user.Links, user.Names, + user.Pronouns, user.Fields, + renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null); } - public static AuthMethodResponse RenderAuthMethod(AuthMethod a) => - new( - a.Id, - a.AuthType, - a.RemoteId, - a.FediverseApplication != null - ? $"@{a.RemoteUsername}@{a.FediverseApplication.Domain}" - : a.RemoteUsername - ); - - public static CustomPreferenceResponse RenderCustomPreference(User.CustomPreference pref) => - new(pref.Icon, pref.Tooltip, pref.Muted, pref.Favourite, pref.Size); - - public static Dictionary RenderCustomPreferences( - User user - ) => - user.CustomPreferences.Select(x => (x.Key, RenderCustomPreference(x.Value))).ToDictionary(); - - public PartialUser RenderPartialUser(User user) => - new( - user.Id, - user.Sid, - user.Username, - user.DisplayName, - AvatarUrlFor(user), - user.CustomPreferences - ); - - public string? AvatarUrlFor(User user) => - user.Avatar != null - ? $"{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 PrideFlagResponse RenderPrideFlag(PrideFlag flag) => - new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); -} + public record UserResponse( + Snowflake Id, + string Username, + string? DisplayName, + string? Bio, + string? MemberTitle, + string? AvatarUrl, + string[] Links, + IEnumerable Names, + IEnumerable Pronouns, + IEnumerable Fields, + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + IEnumerable? Members + ); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/V1/MembersV1Service.cs b/Foxnouns.Backend/Services/V1/MembersV1Service.cs deleted file mode 100644 index 632226c..0000000 --- a/Foxnouns.Backend/Services/V1/MembersV1Service.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto.V1; -using Microsoft.EntityFrameworkCore; -using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry; -using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag; - -namespace Foxnouns.Backend.Services.V1; - -public class MembersV1Service(DatabaseContext db, UsersV1Service usersV1Service) -{ - public async Task ResolveMemberAsync(string id, CancellationToken ct = default) - { - Member? member; - if (Snowflake.TryParse(id, out Snowflake? sf)) - { - member = await db - .Members.Include(m => m.User) - .FirstOrDefaultAsync(m => m.Id == sf && !m.User.Deleted, ct); - if (member != null) - return member; - } - - member = await db - .Members.Include(m => m.User) - .FirstOrDefaultAsync(m => m.LegacyId == id && !m.User.Deleted, ct); - if (member != null) - return member; - - throw new ApiError.NotFound("No member with that ID found.", ErrorCode.MemberNotFound); - } - - public async Task ResolveMemberAsync( - string userRef, - string memberRef, - Token? token, - CancellationToken ct = default - ) - { - User user = await usersV1Service.ResolveUserAsync(userRef, token, ct); - - Member? member; - if (Snowflake.TryParse(memberRef, out Snowflake? sf)) - { - member = await db - .Members.Include(m => m.User) - .FirstOrDefaultAsync(m => m.Id == sf && m.UserId == user.Id, ct); - if (member != null) - return member; - } - - member = await db - .Members.Include(m => m.User) - .FirstOrDefaultAsync(m => m.LegacyId == memberRef && m.UserId == user.Id, ct); - if (member != null) - return member; - - member = await db - .Members.Include(m => m.User) - .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == user.Id, ct); - if (member != null) - return member; - - throw new ApiError.NotFound( - "No member with that ID or name found.", - ErrorCode.MemberNotFound - ); - } - - public async Task RenderMemberAsync( - Member m, - Token? token = default, - User? user = null, - bool renderFlags = true, - CancellationToken ct = default - ) - { - user ??= m.User; - bool renderUnlisted = m.UserId == token?.UserId; - - List flags = renderFlags - ? await db.MemberFlags.Where(f => f.MemberId == m.Id).OrderBy(f => f.Id).ToListAsync(ct) - : []; - - return new MemberResponse( - m.LegacyId, - m.Id, - m.Sid, - m.Name, - m.DisplayName, - m.Bio, - m.Avatar, - m.Links, - Names: FieldEntry.FromEntries(m.Names, user.CustomPreferences), - Pronouns: PronounEntry.FromPronouns(m.Pronouns, user.CustomPreferences), - Fields: ProfileField.FromFields(m.Fields, user.CustomPreferences), - Flags: flags - .Where(f => f.PrideFlag.Hash != null) - .Select(f => new PrideFlag( - f.PrideFlag.LegacyId, - f.PrideFlag.Id, - f.PrideFlag.Hash!, - f.PrideFlag.Name, - f.PrideFlag.Description - )) - .ToArray(), - User: UsersV1Service.RenderPartialUser(user), - Unlisted: renderUnlisted ? m.Unlisted : null - ); - } -} diff --git a/Foxnouns.Backend/Services/V1/UsersV1Service.cs b/Foxnouns.Backend/Services/V1/UsersV1Service.cs deleted file mode 100644 index 1f2ad79..0000000 --- a/Foxnouns.Backend/Services/V1/UsersV1Service.cs +++ /dev/null @@ -1,247 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Dto.V1; -using Microsoft.EntityFrameworkCore; -using FieldEntry = Foxnouns.Backend.Dto.V1.FieldEntry; -using PrideFlag = Foxnouns.Backend.Dto.V1.PrideFlag; - -namespace Foxnouns.Backend.Services.V1; - -public class UsersV1Service(DatabaseContext db) -{ - public async Task ResolveUserAsync( - string userRef, - Token? token, - CancellationToken ct = default - ) - { - if (userRef == "@me") - { - if (token == null) - { - throw new ApiError.Unauthorized( - "This endpoint requires an authenticated user.", - ErrorCode.AuthenticationRequired - ); - } - - return await db.Users.FirstAsync(u => u.Id == token.UserId, ct); - } - - User? user; - if (Snowflake.TryParse(userRef, out Snowflake? sf)) - { - user = await db.Users.FirstOrDefaultAsync(u => u.Id == sf && !u.Deleted, ct); - if (user != null) - return user; - } - - user = await db.Users.FirstOrDefaultAsync(u => u.LegacyId == userRef && !u.Deleted, ct); - if (user != null) - return user; - - user = await db.Users.FirstOrDefaultAsync(u => u.Username == userRef && !u.Deleted, ct); - if (user != null) - return user; - - throw new ApiError.NotFound( - "No user with that ID or username found.", - ErrorCode.UserNotFound - ); - } - - public async Task RenderUserAsync( - User user, - Token? token = null, - bool renderMembers = true, - bool renderFlags = true, - CancellationToken ct = default - ) - { - bool isSelfUser = user.Id == token?.UserId; - renderMembers = renderMembers && (isSelfUser || !user.ListHidden); - - // Only fetch members if we're rendering members (duh) - List members = renderMembers - ? await db.Members.Where(m => m.UserId == user.Id).OrderBy(m => m.Name).ToListAsync(ct) - : []; - - List flags = renderFlags - ? await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct) - : []; - - int? utcOffset = null; - if ( - user.Timezone != null - && TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz) - ) - { - utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds; - } - - return new UserResponse( - user.LegacyId, - user.Id, - user.Sid, - user.Username, - user.DisplayName, - user.Bio, - user.MemberTitle, - user.Avatar, - user.Links, - Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences), - Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences), - Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences), - Flags: flags - .Where(f => f.PrideFlag.Hash != null) - .Select(f => new PrideFlag( - f.PrideFlag.LegacyId, - f.PrideFlag.Id, - f.PrideFlag.Hash!, - f.PrideFlag.Name, - f.PrideFlag.Description - )) - .ToArray(), - Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(), - utcOffset, - CustomPreferences: RenderCustomPreferences(user.CustomPreferences) - ); - } - - public async Task RenderCurrentUserAsync( - User user, - CancellationToken ct = default - ) - { - List members = await db - .Members.Where(m => m.UserId == user.Id) - .OrderBy(m => m.Name) - .ToListAsync(ct); - - List flags = await db - .UserFlags.Where(f => f.UserId == user.Id) - .OrderBy(f => f.Id) - .ToListAsync(ct); - - int? utcOffset = null; - if ( - user.Timezone != null - && TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out TimeZoneInfo? tz) - ) - { - utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds; - } - - List authMethods = await db - .AuthMethods.Include(a => a.FediverseApplication) - .Where(a => a.UserId == user.Id) - .OrderBy(a => a.Id) - .ToListAsync(ct); - - AuthMethod? discord = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Discord); - AuthMethod? google = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Google); - AuthMethod? tumblr = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Tumblr); - AuthMethod? fediverse = authMethods.FirstOrDefault(a => a.AuthType is AuthType.Fediverse); - - return new CurrentUserResponse( - user.LegacyId, - user.Id, - user.Sid, - user.Username, - user.DisplayName, - user.Bio, - user.MemberTitle, - user.Avatar, - user.Links, - Names: FieldEntry.FromEntries(user.Names, user.CustomPreferences), - Pronouns: PronounEntry.FromPronouns(user.Pronouns, user.CustomPreferences), - Fields: ProfileField.FromFields(user.Fields, user.CustomPreferences), - Flags: flags - .Where(f => f.PrideFlag.Hash != null) - .Select(f => new PrideFlag( - f.PrideFlag.LegacyId, - f.PrideFlag.Id, - f.PrideFlag.Hash!, - f.PrideFlag.Name, - f.PrideFlag.Description - )) - .ToArray(), - Members: members.Select(m => RenderPartialMember(m, user.CustomPreferences)).ToArray(), - utcOffset, - CustomPreferences: RenderCustomPreferences(user.CustomPreferences), - user.Id.Time, - user.Timezone, - user.Role is UserRole.Admin, - user.ListHidden, - user.LastSidReroll, - discord?.RemoteId, - discord?.RemoteUsername, - google?.RemoteId, - google?.RemoteUsername, - tumblr?.RemoteId, - tumblr?.RemoteUsername, - fediverse?.RemoteId, - fediverse?.RemoteUsername, - fediverse?.FediverseApplication?.Domain - ); - } - - private static Dictionary RenderCustomPreferences( - Dictionary customPreferences - ) => - customPreferences - .Select(x => - ( - x.Value.LegacyId, - new CustomPreference( - x.Value.Icon, - x.Value.Tooltip, - x.Value.Size, - x.Value.Muted, - x.Value.Favourite - ) - ) - ) - .ToDictionary(); - - private static PartialMember RenderPartialMember( - Member m, - Dictionary customPreferences - ) => - new( - m.LegacyId, - m.Id, - m.Sid, - m.Name, - m.DisplayName, - m.Bio, - m.Avatar, - m.Links, - Names: FieldEntry.FromEntries(m.Names, customPreferences), - Pronouns: PronounEntry.FromPronouns(m.Pronouns, customPreferences) - ); - - public static PartialUser RenderPartialUser(User user) => - new( - user.LegacyId, - user.Id, - user.Username, - user.DisplayName, - user.Avatar, - CustomPreferences: RenderCustomPreferences(user.CustomPreferences) - ); -} diff --git a/Foxnouns.Backend/Services/V1/V1Utils.cs b/Foxnouns.Backend/Services/V1/V1Utils.cs deleted file mode 100644 index 2e52316..0000000 --- a/Foxnouns.Backend/Services/V1/V1Utils.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; - -namespace Foxnouns.Backend.Services.V1; - -public static class V1Utils -{ - public static string TranslateStatus( - string status, - Dictionary customPreferences - ) - { - if (!Snowflake.TryParse(status, out Snowflake? sf)) - return status; - - return customPreferences.TryGetValue(sf.Value, out User.CustomPreference? cf) - ? cf.LegacyId.ToString() - : "unknown"; - } -} diff --git a/Foxnouns.Backend/Services/ValidationService.Fields.cs b/Foxnouns.Backend/Services/ValidationService.Fields.cs deleted file mode 100644 index e2cbff3..0000000 --- a/Foxnouns.Backend/Services/ValidationService.Fields.cs +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; - -namespace Foxnouns.Backend.Services; - -public partial class ValidationService -{ - public static readonly string[] DefaultStatusOptions = - [ - "favourite", - "okay", - "jokingly", - "friends_only", - "avoid", - ]; - - public IEnumerable<(string, ValidationError?)> ValidateFields( - List? fields, - IReadOnlyDictionary customPreferences - ) - { - if (fields == null) - return []; - - var errors = new List<(string, ValidationError?)>(); - if (fields.Count > _limits.MaxFields) - { - errors.Add( - ( - "fields", - ValidationError.LengthError( - "Too many fields", - 0, - _limits.MaxFields, - fields.Count - ) - ) - ); - } - - // No overwhelming this function, thank you - if (fields.Count > _limits.MaxFields + 50) - return errors; - - foreach ((Field? field, int index) in fields.Select((field, index) => (field, index))) - { - if (field.Name.Length > _limits.MaxFieldNameLength) - { - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too long", - 1, - _limits.MaxFieldNameLength, - field.Name.Length - ) - ) - ); - } - else if (field.Name.Length < 1) - { - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too short", - 1, - _limits.MaxFieldNameLength, - field.Name.Length - ) - ) - ); - } - - errors = errors - .Concat( - ValidateFieldEntries( - field.Entries, - customPreferences, - $"fields.{index}.entries" - ) - ) - .ToList(); - } - - return errors; - } - - public IEnumerable<(string, ValidationError?)> ValidateFieldEntries( - FieldEntry[]? entries, - IReadOnlyDictionary customPreferences, - string errorPrefix = "fields" - ) - { - if (entries == null || entries.Length == 0) - return []; - var errors = new List<(string, ValidationError?)>(); - - if (entries.Length > _limits.MaxFieldEntries) - { - errors.Add( - ( - errorPrefix, - ValidationError.LengthError( - "Field has too many entries", - 0, - _limits.MaxFieldEntries, - entries.Length - ) - ) - ); - } - - // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > _limits.MaxFieldEntries + 50) - return errors; - - string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray(); - - foreach ( - (FieldEntry? entry, int entryIdx) in entries.Select( - (entry, entryIdx) => (entry, entryIdx) - ) - ) - { - if (entry.Value.Length > _limits.MaxFieldEntryTextLength) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too long", - 1, - _limits.MaxFieldEntryTextLength, - entry.Value.Length - ) - ) - ); - } - else if (entry.Value.Length < 1) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too short", - 1, - _limits.MaxFieldEntryTextLength, - entry.Value.Length - ) - ) - ); - } - - if ( - !DefaultStatusOptions.Contains(entry.Status) - && !customPreferenceIds.Contains(entry.Status) - ) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.status", - ValidationError.GenericValidationError("Invalid status", entry.Status) - ) - ); - } - } - - return errors; - } - - public IEnumerable<(string, ValidationError?)> ValidatePronouns( - Pronoun[]? entries, - IReadOnlyDictionary customPreferences, - string errorPrefix = "pronouns" - ) - { - if (entries == null || entries.Length == 0) - return []; - var errors = new List<(string, ValidationError?)>(); - - if (entries.Length > _limits.MaxFieldEntries) - { - errors.Add( - ( - errorPrefix, - ValidationError.LengthError( - "Too many pronouns", - 0, - _limits.MaxFieldEntries, - entries.Length - ) - ) - ); - } - - // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > _limits.MaxFieldEntries + 50) - return errors; - - string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray(); - - foreach ( - (Pronoun? entry, int entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx)) - ) - { - if (entry.Value.Length > _limits.MaxFieldEntryTextLength) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too long", - 1, - _limits.MaxFieldEntryTextLength, - entry.Value.Length - ) - ) - ); - } - else if (entry.Value.Length < 1) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too short", - 1, - _limits.MaxFieldEntryTextLength, - entry.Value.Length - ) - ) - ); - } - - if (entry.DisplayText != null) - { - if (entry.DisplayText.Length > _limits.MaxFieldEntryTextLength) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too long", - 1, - _limits.MaxFieldEntryTextLength, - entry.Value.Length - ) - ) - ); - } - else if (entry.DisplayText.Length < 1) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too short", - 1, - _limits.MaxFieldEntryTextLength, - entry.Value.Length - ) - ) - ); - } - } - - if ( - !DefaultStatusOptions.Contains(entry.Status) - && !customPreferenceIds.Contains(entry.Status) - ) - { - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.status", - ValidationError.GenericValidationError("Invalid status", entry.Status) - ) - ); - } - } - - return errors; - } -} diff --git a/Foxnouns.Backend/Services/ValidationService.Strings.cs b/Foxnouns.Backend/Services/ValidationService.Strings.cs deleted file mode 100644 index 9244ed4..0000000 --- a/Foxnouns.Backend/Services/ValidationService.Strings.cs +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Text.RegularExpressions; - -namespace Foxnouns.Backend.Services; - -public partial class ValidationService -{ - private static readonly string[] InvalidUsernames = - [ - "..", - "admin", - "administrator", - "mod", - "moderator", - "api", - "page", - "pronouns", - "settings", - "pronouns.cc", - "pronounscc", - "null", - ]; - - private static readonly string[] InvalidMemberNames = - [ - // these break routing outright - ".", - "..", - // TODO: remove this? i'm not sure if /@[username]/edit will redirect to settings - "edit", - // this breaks the frontend, somehow - "null", - ]; - - public ValidationError? ValidateUsername(string username) - { - if (!UsernameRegex().IsMatch(username)) - { - if (username.Length < 2) - { - return ValidationError.LengthError( - "Username is too short", - 2, - _limits.MaxUsernameLength, - username.Length - ); - } - - if (username.Length > _limits.MaxUsernameLength) - { - return ValidationError.LengthError( - "Username is too long", - 2, - _limits.MaxUsernameLength, - username.Length - ); - } - - return ValidationError.GenericValidationError( - "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", - username - ); - } - - if ( - InvalidUsernames.Any(u => - string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase) - ) - ) - { - return ValidationError.GenericValidationError("Username is not allowed", username); - } - - return null; - } - - public ValidationError? ValidateMemberName(string memberName) - { - if (!MemberRegex().IsMatch(memberName)) - { - if (memberName.Length < 1) - { - return ValidationError.LengthError( - "Name is too short", - 1, - _limits.MaxMemberNameLength, - memberName.Length - ); - } - - if (memberName.Length > _limits.MaxMemberNameLength) - { - return ValidationError.LengthError( - "Name is too long", - 1, - _limits.MaxMemberNameLength, - memberName.Length - ); - } - - return ValidationError.GenericValidationError( - "Member name cannot contain any of the following: " - + " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , " - + "and cannot be one or two periods", - memberName - ); - } - - if ( - InvalidMemberNames.Any(u => - string.Equals(u, memberName, StringComparison.InvariantCultureIgnoreCase) - ) - ) - { - return ValidationError.GenericValidationError("Name is not allowed", memberName); - } - - return null; - } - - public ValidationError? ValidateDisplayName(string? displayName) - { - if (displayName?.Length == 0) - { - return ValidationError.LengthError( - "Display name is too short", - 1, - _limits.MaxDisplayNameLength, - displayName.Length - ); - } - - if (displayName?.Length > _limits.MaxDisplayNameLength) - { - return ValidationError.LengthError( - "Display name is too long", - 1, - _limits.MaxDisplayNameLength, - displayName.Length - ); - } - - return null; - } - - public IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) - { - if (links == null) - return []; - if (links.Length > _limits.MaxLinks) - { - return - [ - ( - "links", - ValidationError.LengthError("Too many links", 0, _limits.MaxLinks, links.Length) - ), - ]; - } - - var errors = new List<(string, ValidationError?)>(); - foreach ((string link, int idx) in links.Select((l, i) => (l, i))) - { - if (link.Length == 0) - { - errors.Add( - ( - $"links.{idx}", - ValidationError.LengthError( - "Link cannot be empty", - 1, - _limits.MaxLinkLength, - 0 - ) - ) - ); - } - else if (link.Length > _limits.MaxLinkLength) - { - errors.Add( - ( - $"links.{idx}", - ValidationError.LengthError( - "Link is too long", - 1, - _limits.MaxLinkLength, - link.Length - ) - ) - ); - } - } - - return errors; - } - - public ValidationError? ValidateBio(string? bio) - { - if (bio?.Length == 0) - { - return ValidationError.LengthError( - "Bio is too short", - 1, - _limits.MaxBioLength, - bio.Length - ); - } - - if (bio?.Length > _limits.MaxBioLength) - { - return ValidationError.LengthError( - "Bio is too long", - 1, - _limits.MaxBioLength, - bio.Length - ); - } - - return null; - } - - public ValidationError? ValidateAvatar(string? avatar) - { - if (avatar?.Length == 0) - { - return ValidationError.GenericValidationError("Avatar cannot be empty", null); - } - - if (avatar?.Length > _limits.MaxAvatarLength) - { - return ValidationError.GenericValidationError("Avatar is too large", null); - } - - return null; - } - - [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-US")] - private static partial Regex UsernameRegex(); - - [GeneratedRegex( - """^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", - RegexOptions.IgnoreCase, - "en-US" - )] - private static partial Regex MemberRegex(); -} diff --git a/Foxnouns.Backend/Services/ValidationService.cs b/Foxnouns.Backend/Services/ValidationService.cs deleted file mode 100644 index 989f469..0000000 --- a/Foxnouns.Backend/Services/ValidationService.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Foxnouns.Backend.Services; - -public partial class ValidationService(Config config) -{ - private readonly Config.LimitsConfig _limits = config.Limits; -} diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs deleted file mode 100644 index d57eb73..0000000 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Security.Cryptography; -using Foxnouns.Backend.Database.Models; - -namespace Foxnouns.Backend.Utils; - -public static class AuthUtils -{ - public const string ClientCredentials = "client_credentials"; - public const string AuthorizationCode = "authorization_code"; - private static readonly string[] ForbiddenSchemes = - [ - "javascript", - "file", - "data", - "mailto", - "tel", - ]; - - public static readonly string[] UserScopes = - [ - "user.read_hidden", - "user.read_privileged", - "user.update", - "user.read_flags", - "user.create_flags", - "user.update_flags", - "user.moderation", - ]; - - public static readonly string[] MemberScopes = - [ - "member.read", - "member.update", - "member.create", - ]; - - /// - /// All scopes endpoints can be secured by. This does *not* include the catch-all token scopes. - /// - public static readonly string[] Scopes = ["identify", .. UserScopes, .. MemberScopes]; - - /// - /// All scopes that can be granted to applications and tokens. Includes the catch-all token scopes, - /// except for "*" which is only granted to the frontend. - /// - public static readonly string[] ApplicationScopes = [.. Scopes, "user", "member"]; - - public static string[] ExpandScopes(this string[] scopes) - { - if (scopes.Contains("*")) - return ["*", .. Scopes]; - List expandedScopes = ["identify"]; - if (scopes.Contains("user")) - expandedScopes.AddRange(UserScopes); - if (scopes.Contains("member")) - expandedScopes.AddRange(MemberScopes); - - return expandedScopes.ToArray(); - } - - public static bool HasScope(this Token? token, string scope) => - token?.Scopes.ExpandScopes().Contains(scope) == true; - - private static string[] ExpandAppScopes(this string[] scopes) - { - var expandedScopes = scopes.ExpandScopes().ToList(); - if (scopes.Contains("user")) - expandedScopes.Add("user"); - if (scopes.Contains("member")) - expandedScopes.Add("member"); - return expandedScopes.ToArray(); - } - - public static bool ValidateScopes(Application application, string[] scopes) - { - string[] expandedScopes = scopes.ExpandScopes(); - string[] appScopes = application.Scopes.ExpandAppScopes(); - return !expandedScopes.Except(appScopes).Any(); - } - - public static bool ValidateRedirectUri(string uri) - { - try - { - string scheme = new Uri(uri).Scheme; - return !ForbiddenSchemes.Contains(scheme); - } - catch - { - return false; - } - } - - public static bool TryFromBase64String(string b64, out byte[] bytes) - { - try - { - bytes = Convert.FromBase64String(b64); - return true; - } - catch - { - bytes = []; - return false; - } - } - - public static bool TryParseToken(string? input, out byte[] rawToken) - { - rawToken = []; - - if (string.IsNullOrWhiteSpace(input)) - return false; - if (input.StartsWith("bearer ", StringComparison.InvariantCultureIgnoreCase)) - input = input["bearer ".Length..]; - - return TryFromBase64String(input, out rawToken); - } - - public static string RandomUrlUnsafeToken(int bytes = 48) => - Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); - - public static string RandomToken(int bytes = 48) => - RandomUrlUnsafeToken(bytes) - // Make the token URL-safe - .Replace('+', '-') - .Replace('/', '_'); - - public const int MaxAuthMethodsPerType = 3; // Maximum of 3 Discord accounts, 3 emails, etc -} diff --git a/Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs b/Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs deleted file mode 100644 index 6f0b2be..0000000 --- a/Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs +++ /dev/null @@ -1,2060 +0,0 @@ -// - -namespace Foxnouns.Backend.Utils; - -public static partial class BootstrapIcons -{ - private static readonly HashSet Icons = - [ - "123", - "alarm-fill", - "alarm", - "align-bottom", - "align-center", - "align-end", - "align-middle", - "align-start", - "align-top", - "alt", - "app-indicator", - "app", - "archive-fill", - "archive", - "arrow-90deg-down", - "arrow-90deg-left", - "arrow-90deg-right", - "arrow-90deg-up", - "arrow-bar-down", - "arrow-bar-left", - "arrow-bar-right", - "arrow-bar-up", - "arrow-clockwise", - "arrow-counterclockwise", - "arrow-down-circle-fill", - "arrow-down-circle", - "arrow-down-left-circle-fill", - "arrow-down-left-circle", - "arrow-down-left-square-fill", - "arrow-down-left-square", - "arrow-down-left", - "arrow-down-right-circle-fill", - "arrow-down-right-circle", - "arrow-down-right-square-fill", - "arrow-down-right-square", - "arrow-down-right", - "arrow-down-short", - "arrow-down-square-fill", - "arrow-down-square", - "arrow-down-up", - "arrow-down", - "arrow-left-circle-fill", - "arrow-left-circle", - "arrow-left-right", - "arrow-left-short", - "arrow-left-square-fill", - "arrow-left-square", - "arrow-left", - "arrow-repeat", - "arrow-return-left", - "arrow-return-right", - "arrow-right-circle-fill", - "arrow-right-circle", - "arrow-right-short", - "arrow-right-square-fill", - "arrow-right-square", - "arrow-right", - "arrow-up-circle-fill", - "arrow-up-circle", - "arrow-up-left-circle-fill", - "arrow-up-left-circle", - "arrow-up-left-square-fill", - "arrow-up-left-square", - "arrow-up-left", - "arrow-up-right-circle-fill", - "arrow-up-right-circle", - "arrow-up-right-square-fill", - "arrow-up-right-square", - "arrow-up-right", - "arrow-up-short", - "arrow-up-square-fill", - "arrow-up-square", - "arrow-up", - "arrows-angle-contract", - "arrows-angle-expand", - "arrows-collapse", - "arrows-expand", - "arrows-fullscreen", - "arrows-move", - "aspect-ratio-fill", - "aspect-ratio", - "asterisk", - "at", - "award-fill", - "award", - "back", - "backspace-fill", - "backspace-reverse-fill", - "backspace-reverse", - "backspace", - "badge-3d-fill", - "badge-3d", - "badge-4k-fill", - "badge-4k", - "badge-8k-fill", - "badge-8k", - "badge-ad-fill", - "badge-ad", - "badge-ar-fill", - "badge-ar", - "badge-cc-fill", - "badge-cc", - "badge-hd-fill", - "badge-hd", - "badge-tm-fill", - "badge-tm", - "badge-vo-fill", - "badge-vo", - "badge-vr-fill", - "badge-vr", - "badge-wc-fill", - "badge-wc", - "bag-check-fill", - "bag-check", - "bag-dash-fill", - "bag-dash", - "bag-fill", - "bag-plus-fill", - "bag-plus", - "bag-x-fill", - "bag-x", - "bag", - "bar-chart-fill", - "bar-chart-line-fill", - "bar-chart-line", - "bar-chart-steps", - "bar-chart", - "basket-fill", - "basket", - "basket2-fill", - "basket2", - "basket3-fill", - "basket3", - "battery-charging", - "battery-full", - "battery-half", - "battery", - "bell-fill", - "bell", - "bezier", - "bezier2", - "bicycle", - "binoculars-fill", - "binoculars", - "blockquote-left", - "blockquote-right", - "book-fill", - "book-half", - "book", - "bookmark-check-fill", - "bookmark-check", - "bookmark-dash-fill", - "bookmark-dash", - "bookmark-fill", - "bookmark-heart-fill", - "bookmark-heart", - "bookmark-plus-fill", - "bookmark-plus", - "bookmark-star-fill", - "bookmark-star", - "bookmark-x-fill", - "bookmark-x", - "bookmark", - "bookmarks-fill", - "bookmarks", - "bookshelf", - "bootstrap-fill", - "bootstrap-reboot", - "bootstrap", - "border-all", - "border-bottom", - "border-center", - "border-inner", - "border-left", - "border-middle", - "border-outer", - "border-right", - "border-style", - "border-top", - "border-width", - "border", - "bounding-box-circles", - "bounding-box", - "box-arrow-down-left", - "box-arrow-down-right", - "box-arrow-down", - "box-arrow-in-down-left", - "box-arrow-in-down-right", - "box-arrow-in-down", - "box-arrow-in-left", - "box-arrow-in-right", - "box-arrow-in-up-left", - "box-arrow-in-up-right", - "box-arrow-in-up", - "box-arrow-left", - "box-arrow-right", - "box-arrow-up-left", - "box-arrow-up-right", - "box-arrow-up", - "box-seam", - "box", - "braces", - "bricks", - "briefcase-fill", - "briefcase", - "brightness-alt-high-fill", - "brightness-alt-high", - "brightness-alt-low-fill", - "brightness-alt-low", - "brightness-high-fill", - "brightness-high", - "brightness-low-fill", - "brightness-low", - "broadcast-pin", - "broadcast", - "brush-fill", - "brush", - "bucket-fill", - "bucket", - "bug-fill", - "bug", - "building", - "bullseye", - "calculator-fill", - "calculator", - "calendar-check-fill", - "calendar-check", - "calendar-date-fill", - "calendar-date", - "calendar-day-fill", - "calendar-day", - "calendar-event-fill", - "calendar-event", - "calendar-fill", - "calendar-minus-fill", - "calendar-minus", - "calendar-month-fill", - "calendar-month", - "calendar-plus-fill", - "calendar-plus", - "calendar-range-fill", - "calendar-range", - "calendar-week-fill", - "calendar-week", - "calendar-x-fill", - "calendar-x", - "calendar", - "calendar2-check-fill", - "calendar2-check", - "calendar2-date-fill", - "calendar2-date", - "calendar2-day-fill", - "calendar2-day", - "calendar2-event-fill", - "calendar2-event", - "calendar2-fill", - "calendar2-minus-fill", - "calendar2-minus", - "calendar2-month-fill", - "calendar2-month", - "calendar2-plus-fill", - "calendar2-plus", - "calendar2-range-fill", - "calendar2-range", - "calendar2-week-fill", - "calendar2-week", - "calendar2-x-fill", - "calendar2-x", - "calendar2", - "calendar3-event-fill", - "calendar3-event", - "calendar3-fill", - "calendar3-range-fill", - "calendar3-range", - "calendar3-week-fill", - "calendar3-week", - "calendar3", - "calendar4-event", - "calendar4-range", - "calendar4-week", - "calendar4", - "camera-fill", - "camera-reels-fill", - "camera-reels", - "camera-video-fill", - "camera-video-off-fill", - "camera-video-off", - "camera-video", - "camera", - "camera2", - "capslock-fill", - "capslock", - "card-checklist", - "card-heading", - "card-image", - "card-list", - "card-text", - "caret-down-fill", - "caret-down-square-fill", - "caret-down-square", - "caret-down", - "caret-left-fill", - "caret-left-square-fill", - "caret-left-square", - "caret-left", - "caret-right-fill", - "caret-right-square-fill", - "caret-right-square", - "caret-right", - "caret-up-fill", - "caret-up-square-fill", - "caret-up-square", - "caret-up", - "cart-check-fill", - "cart-check", - "cart-dash-fill", - "cart-dash", - "cart-fill", - "cart-plus-fill", - "cart-plus", - "cart-x-fill", - "cart-x", - "cart", - "cart2", - "cart3", - "cart4", - "cash-stack", - "cash", - "cast", - "chat-dots-fill", - "chat-dots", - "chat-fill", - "chat-left-dots-fill", - "chat-left-dots", - "chat-left-fill", - "chat-left-quote-fill", - "chat-left-quote", - "chat-left-text-fill", - "chat-left-text", - "chat-left", - "chat-quote-fill", - "chat-quote", - "chat-right-dots-fill", - "chat-right-dots", - "chat-right-fill", - "chat-right-quote-fill", - "chat-right-quote", - "chat-right-text-fill", - "chat-right-text", - "chat-right", - "chat-square-dots-fill", - "chat-square-dots", - "chat-square-fill", - "chat-square-quote-fill", - "chat-square-quote", - "chat-square-text-fill", - "chat-square-text", - "chat-square", - "chat-text-fill", - "chat-text", - "chat", - "check-all", - "check-circle-fill", - "check-circle", - "check-square-fill", - "check-square", - "check", - "check2-all", - "check2-circle", - "check2-square", - "check2", - "chevron-bar-contract", - "chevron-bar-down", - "chevron-bar-expand", - "chevron-bar-left", - "chevron-bar-right", - "chevron-bar-up", - "chevron-compact-down", - "chevron-compact-left", - "chevron-compact-right", - "chevron-compact-up", - "chevron-contract", - "chevron-double-down", - "chevron-double-left", - "chevron-double-right", - "chevron-double-up", - "chevron-down", - "chevron-expand", - "chevron-left", - "chevron-right", - "chevron-up", - "circle-fill", - "circle-half", - "circle-square", - "circle", - "clipboard-check", - "clipboard-data", - "clipboard-minus", - "clipboard-plus", - "clipboard-x", - "clipboard", - "clock-fill", - "clock-history", - "clock", - "cloud-arrow-down-fill", - "cloud-arrow-down", - "cloud-arrow-up-fill", - "cloud-arrow-up", - "cloud-check-fill", - "cloud-check", - "cloud-download-fill", - "cloud-download", - "cloud-drizzle-fill", - "cloud-drizzle", - "cloud-fill", - "cloud-fog-fill", - "cloud-fog", - "cloud-fog2-fill", - "cloud-fog2", - "cloud-hail-fill", - "cloud-hail", - "cloud-haze-fill", - "cloud-haze", - "cloud-haze2-fill", - "cloud-lightning-fill", - "cloud-lightning-rain-fill", - "cloud-lightning-rain", - "cloud-lightning", - "cloud-minus-fill", - "cloud-minus", - "cloud-moon-fill", - "cloud-moon", - "cloud-plus-fill", - "cloud-plus", - "cloud-rain-fill", - "cloud-rain-heavy-fill", - "cloud-rain-heavy", - "cloud-rain", - "cloud-slash-fill", - "cloud-slash", - "cloud-sleet-fill", - "cloud-sleet", - "cloud-snow-fill", - "cloud-snow", - "cloud-sun-fill", - "cloud-sun", - "cloud-upload-fill", - "cloud-upload", - "cloud", - "clouds-fill", - "clouds", - "cloudy-fill", - "cloudy", - "code-slash", - "code-square", - "code", - "collection-fill", - "collection-play-fill", - "collection-play", - "collection", - "columns-gap", - "columns", - "command", - "compass-fill", - "compass", - "cone-striped", - "cone", - "controller", - "cpu-fill", - "cpu", - "credit-card-2-back-fill", - "credit-card-2-back", - "credit-card-2-front-fill", - "credit-card-2-front", - "credit-card-fill", - "credit-card", - "crop", - "cup-fill", - "cup-straw", - "cup", - "cursor-fill", - "cursor-text", - "cursor", - "dash-circle-dotted", - "dash-circle-fill", - "dash-circle", - "dash-square-dotted", - "dash-square-fill", - "dash-square", - "dash", - "diagram-2-fill", - "diagram-2", - "diagram-3-fill", - "diagram-3", - "diamond-fill", - "diamond-half", - "diamond", - "dice-1-fill", - "dice-1", - "dice-2-fill", - "dice-2", - "dice-3-fill", - "dice-3", - "dice-4-fill", - "dice-4", - "dice-5-fill", - "dice-5", - "dice-6-fill", - "dice-6", - "disc-fill", - "disc", - "discord", - "display-fill", - "display", - "distribute-horizontal", - "distribute-vertical", - "door-closed-fill", - "door-closed", - "door-open-fill", - "door-open", - "dot", - "download", - "droplet-fill", - "droplet-half", - "droplet", - "earbuds", - "easel-fill", - "easel", - "egg-fill", - "egg-fried", - "egg", - "eject-fill", - "eject", - "emoji-angry-fill", - "emoji-angry", - "emoji-dizzy-fill", - "emoji-dizzy", - "emoji-expressionless-fill", - "emoji-expressionless", - "emoji-frown-fill", - "emoji-frown", - "emoji-heart-eyes-fill", - "emoji-heart-eyes", - "emoji-laughing-fill", - "emoji-laughing", - "emoji-neutral-fill", - "emoji-neutral", - "emoji-smile-fill", - "emoji-smile-upside-down-fill", - "emoji-smile-upside-down", - "emoji-smile", - "emoji-sunglasses-fill", - "emoji-sunglasses", - "emoji-wink-fill", - "emoji-wink", - "envelope-fill", - "envelope-open-fill", - "envelope-open", - "envelope", - "eraser-fill", - "eraser", - "exclamation-circle-fill", - "exclamation-circle", - "exclamation-diamond-fill", - "exclamation-diamond", - "exclamation-octagon-fill", - "exclamation-octagon", - "exclamation-square-fill", - "exclamation-square", - "exclamation-triangle-fill", - "exclamation-triangle", - "exclamation", - "exclude", - "eye-fill", - "eye-slash-fill", - "eye-slash", - "eye", - "eyedropper", - "eyeglasses", - "facebook", - "file-arrow-down-fill", - "file-arrow-down", - "file-arrow-up-fill", - "file-arrow-up", - "file-bar-graph-fill", - "file-bar-graph", - "file-binary-fill", - "file-binary", - "file-break-fill", - "file-break", - "file-check-fill", - "file-check", - "file-code-fill", - "file-code", - "file-diff-fill", - "file-diff", - "file-earmark-arrow-down-fill", - "file-earmark-arrow-down", - "file-earmark-arrow-up-fill", - "file-earmark-arrow-up", - "file-earmark-bar-graph-fill", - "file-earmark-bar-graph", - "file-earmark-binary-fill", - "file-earmark-binary", - "file-earmark-break-fill", - "file-earmark-break", - "file-earmark-check-fill", - "file-earmark-check", - "file-earmark-code-fill", - "file-earmark-code", - "file-earmark-diff-fill", - "file-earmark-diff", - "file-earmark-easel-fill", - "file-earmark-easel", - "file-earmark-excel-fill", - "file-earmark-excel", - "file-earmark-fill", - "file-earmark-font-fill", - "file-earmark-font", - "file-earmark-image-fill", - "file-earmark-image", - "file-earmark-lock-fill", - "file-earmark-lock", - "file-earmark-lock2-fill", - "file-earmark-lock2", - "file-earmark-medical-fill", - "file-earmark-medical", - "file-earmark-minus-fill", - "file-earmark-minus", - "file-earmark-music-fill", - "file-earmark-music", - "file-earmark-person-fill", - "file-earmark-person", - "file-earmark-play-fill", - "file-earmark-play", - "file-earmark-plus-fill", - "file-earmark-plus", - "file-earmark-post-fill", - "file-earmark-post", - "file-earmark-ppt-fill", - "file-earmark-ppt", - "file-earmark-richtext-fill", - "file-earmark-richtext", - "file-earmark-ruled-fill", - "file-earmark-ruled", - "file-earmark-slides-fill", - "file-earmark-slides", - "file-earmark-spreadsheet-fill", - "file-earmark-spreadsheet", - "file-earmark-text-fill", - "file-earmark-text", - "file-earmark-word-fill", - "file-earmark-word", - "file-earmark-x-fill", - "file-earmark-x", - "file-earmark-zip-fill", - "file-earmark-zip", - "file-earmark", - "file-easel-fill", - "file-easel", - "file-excel-fill", - "file-excel", - "file-fill", - "file-font-fill", - "file-font", - "file-image-fill", - "file-image", - "file-lock-fill", - "file-lock", - "file-lock2-fill", - "file-lock2", - "file-medical-fill", - "file-medical", - "file-minus-fill", - "file-minus", - "file-music-fill", - "file-music", - "file-person-fill", - "file-person", - "file-play-fill", - "file-play", - "file-plus-fill", - "file-plus", - "file-post-fill", - "file-post", - "file-ppt-fill", - "file-ppt", - "file-richtext-fill", - "file-richtext", - "file-ruled-fill", - "file-ruled", - "file-slides-fill", - "file-slides", - "file-spreadsheet-fill", - "file-spreadsheet", - "file-text-fill", - "file-text", - "file-word-fill", - "file-word", - "file-x-fill", - "file-x", - "file-zip-fill", - "file-zip", - "file", - "files-alt", - "files", - "film", - "filter-circle-fill", - "filter-circle", - "filter-left", - "filter-right", - "filter-square-fill", - "filter-square", - "filter", - "flag-fill", - "flag", - "flower1", - "flower2", - "flower3", - "folder-check", - "folder-fill", - "folder-minus", - "folder-plus", - "folder-symlink-fill", - "folder-symlink", - "folder-x", - "folder", - "folder2-open", - "folder2", - "fonts", - "forward-fill", - "forward", - "front", - "fullscreen-exit", - "fullscreen", - "funnel-fill", - "funnel", - "gear-fill", - "gear-wide-connected", - "gear-wide", - "gear", - "gem", - "geo-alt-fill", - "geo-alt", - "geo-fill", - "geo", - "gift-fill", - "gift", - "github", - "globe", - "globe2", - "google", - "graph-down", - "graph-up", - "grid-1x2-fill", - "grid-1x2", - "grid-3x2-gap-fill", - "grid-3x2-gap", - "grid-3x2", - "grid-3x3-gap-fill", - "grid-3x3-gap", - "grid-3x3", - "grid-fill", - "grid", - "grip-horizontal", - "grip-vertical", - "hammer", - "hand-index-fill", - "hand-index-thumb-fill", - "hand-index-thumb", - "hand-index", - "hand-thumbs-down-fill", - "hand-thumbs-down", - "hand-thumbs-up-fill", - "hand-thumbs-up", - "handbag-fill", - "handbag", - "hash", - "hdd-fill", - "hdd-network-fill", - "hdd-network", - "hdd-rack-fill", - "hdd-rack", - "hdd-stack-fill", - "hdd-stack", - "hdd", - "headphones", - "headset", - "heart-fill", - "heart-half", - "heart", - "heptagon-fill", - "heptagon-half", - "heptagon", - "hexagon-fill", - "hexagon-half", - "hexagon", - "hourglass-bottom", - "hourglass-split", - "hourglass-top", - "hourglass", - "house-door-fill", - "house-door", - "house-fill", - "house", - "hr", - "hurricane", - "image-alt", - "image-fill", - "image", - "images", - "inbox-fill", - "inbox", - "inboxes-fill", - "inboxes", - "info-circle-fill", - "info-circle", - "info-square-fill", - "info-square", - "info", - "input-cursor-text", - "input-cursor", - "instagram", - "intersect", - "journal-album", - "journal-arrow-down", - "journal-arrow-up", - "journal-bookmark-fill", - "journal-bookmark", - "journal-check", - "journal-code", - "journal-medical", - "journal-minus", - "journal-plus", - "journal-richtext", - "journal-text", - "journal-x", - "journal", - "journals", - "joystick", - "justify-left", - "justify-right", - "justify", - "kanban-fill", - "kanban", - "key-fill", - "key", - "keyboard-fill", - "keyboard", - "ladder", - "lamp-fill", - "lamp", - "laptop-fill", - "laptop", - "layer-backward", - "layer-forward", - "layers-fill", - "layers-half", - "layers", - "layout-sidebar-inset-reverse", - "layout-sidebar-inset", - "layout-sidebar-reverse", - "layout-sidebar", - "layout-split", - "layout-text-sidebar-reverse", - "layout-text-sidebar", - "layout-text-window-reverse", - "layout-text-window", - "layout-three-columns", - "layout-wtf", - "life-preserver", - "lightbulb-fill", - "lightbulb-off-fill", - "lightbulb-off", - "lightbulb", - "lightning-charge-fill", - "lightning-charge", - "lightning-fill", - "lightning", - "link-45deg", - "link", - "linkedin", - "list-check", - "list-nested", - "list-ol", - "list-stars", - "list-task", - "list-ul", - "list", - "lock-fill", - "lock", - "mailbox", - "mailbox2", - "map-fill", - "map", - "markdown-fill", - "markdown", - "mask", - "megaphone-fill", - "megaphone", - "menu-app-fill", - "menu-app", - "menu-button-fill", - "menu-button-wide-fill", - "menu-button-wide", - "menu-button", - "menu-down", - "menu-up", - "mic-fill", - "mic-mute-fill", - "mic-mute", - "mic", - "minecart-loaded", - "minecart", - "moisture", - "moon-fill", - "moon-stars-fill", - "moon-stars", - "moon", - "mouse-fill", - "mouse", - "mouse2-fill", - "mouse2", - "mouse3-fill", - "mouse3", - "music-note-beamed", - "music-note-list", - "music-note", - "music-player-fill", - "music-player", - "newspaper", - "node-minus-fill", - "node-minus", - "node-plus-fill", - "node-plus", - "nut-fill", - "nut", - "octagon-fill", - "octagon-half", - "octagon", - "option", - "outlet", - "paint-bucket", - "palette-fill", - "palette", - "palette2", - "paperclip", - "paragraph", - "patch-check-fill", - "patch-check", - "patch-exclamation-fill", - "patch-exclamation", - "patch-minus-fill", - "patch-minus", - "patch-plus-fill", - "patch-plus", - "patch-question-fill", - "patch-question", - "pause-btn-fill", - "pause-btn", - "pause-circle-fill", - "pause-circle", - "pause-fill", - "pause", - "peace-fill", - "peace", - "pen-fill", - "pen", - "pencil-fill", - "pencil-square", - "pencil", - "pentagon-fill", - "pentagon-half", - "pentagon", - "people-fill", - "people", - "percent", - "person-badge-fill", - "person-badge", - "person-bounding-box", - "person-check-fill", - "person-check", - "person-circle", - "person-dash-fill", - "person-dash", - "person-fill", - "person-lines-fill", - "person-plus-fill", - "person-plus", - "person-square", - "person-x-fill", - "person-x", - "person", - "phone-fill", - "phone-landscape-fill", - "phone-landscape", - "phone-vibrate-fill", - "phone-vibrate", - "phone", - "pie-chart-fill", - "pie-chart", - "pin-angle-fill", - "pin-angle", - "pin-fill", - "pin", - "pip-fill", - "pip", - "play-btn-fill", - "play-btn", - "play-circle-fill", - "play-circle", - "play-fill", - "play", - "plug-fill", - "plug", - "plus-circle-dotted", - "plus-circle-fill", - "plus-circle", - "plus-square-dotted", - "plus-square-fill", - "plus-square", - "plus", - "power", - "printer-fill", - "printer", - "puzzle-fill", - "puzzle", - "question-circle-fill", - "question-circle", - "question-diamond-fill", - "question-diamond", - "question-octagon-fill", - "question-octagon", - "question-square-fill", - "question-square", - "question", - "rainbow", - "receipt-cutoff", - "receipt", - "reception-0", - "reception-1", - "reception-2", - "reception-3", - "reception-4", - "record-btn-fill", - "record-btn", - "record-circle-fill", - "record-circle", - "record-fill", - "record", - "record2-fill", - "record2", - "reply-all-fill", - "reply-all", - "reply-fill", - "reply", - "rss-fill", - "rss", - "rulers", - "save-fill", - "save", - "save2-fill", - "save2", - "scissors", - "screwdriver", - "search", - "segmented-nav", - "server", - "share-fill", - "share", - "shield-check", - "shield-exclamation", - "shield-fill-check", - "shield-fill-exclamation", - "shield-fill-minus", - "shield-fill-plus", - "shield-fill-x", - "shield-fill", - "shield-lock-fill", - "shield-lock", - "shield-minus", - "shield-plus", - "shield-shaded", - "shield-slash-fill", - "shield-slash", - "shield-x", - "shield", - "shift-fill", - "shift", - "shop-window", - "shop", - "shuffle", - "signpost-2-fill", - "signpost-2", - "signpost-fill", - "signpost-split-fill", - "signpost-split", - "signpost", - "sim-fill", - "sim", - "skip-backward-btn-fill", - "skip-backward-btn", - "skip-backward-circle-fill", - "skip-backward-circle", - "skip-backward-fill", - "skip-backward", - "skip-end-btn-fill", - "skip-end-btn", - "skip-end-circle-fill", - "skip-end-circle", - "skip-end-fill", - "skip-end", - "skip-forward-btn-fill", - "skip-forward-btn", - "skip-forward-circle-fill", - "skip-forward-circle", - "skip-forward-fill", - "skip-forward", - "skip-start-btn-fill", - "skip-start-btn", - "skip-start-circle-fill", - "skip-start-circle", - "skip-start-fill", - "skip-start", - "slack", - "slash-circle-fill", - "slash-circle", - "slash-square-fill", - "slash-square", - "slash", - "sliders", - "smartwatch", - "snow", - "snow2", - "snow3", - "sort-alpha-down-alt", - "sort-alpha-down", - "sort-alpha-up-alt", - "sort-alpha-up", - "sort-down-alt", - "sort-down", - "sort-numeric-down-alt", - "sort-numeric-down", - "sort-numeric-up-alt", - "sort-numeric-up", - "sort-up-alt", - "sort-up", - "soundwave", - "speaker-fill", - "speaker", - "speedometer", - "speedometer2", - "spellcheck", - "square-fill", - "square-half", - "square", - "stack", - "star-fill", - "star-half", - "star", - "stars", - "stickies-fill", - "stickies", - "sticky-fill", - "sticky", - "stop-btn-fill", - "stop-btn", - "stop-circle-fill", - "stop-circle", - "stop-fill", - "stop", - "stoplights-fill", - "stoplights", - "stopwatch-fill", - "stopwatch", - "subtract", - "suit-club-fill", - "suit-club", - "suit-diamond-fill", - "suit-diamond", - "suit-heart-fill", - "suit-heart", - "suit-spade-fill", - "suit-spade", - "sun-fill", - "sun", - "sunglasses", - "sunrise-fill", - "sunrise", - "sunset-fill", - "sunset", - "symmetry-horizontal", - "symmetry-vertical", - "table", - "tablet-fill", - "tablet-landscape-fill", - "tablet-landscape", - "tablet", - "tag-fill", - "tag", - "tags-fill", - "tags", - "telegram", - "telephone-fill", - "telephone-forward-fill", - "telephone-forward", - "telephone-inbound-fill", - "telephone-inbound", - "telephone-minus-fill", - "telephone-minus", - "telephone-outbound-fill", - "telephone-outbound", - "telephone-plus-fill", - "telephone-plus", - "telephone-x-fill", - "telephone-x", - "telephone", - "terminal-fill", - "terminal", - "text-center", - "text-indent-left", - "text-indent-right", - "text-left", - "text-paragraph", - "text-right", - "textarea-resize", - "textarea-t", - "textarea", - "thermometer-half", - "thermometer-high", - "thermometer-low", - "thermometer-snow", - "thermometer-sun", - "thermometer", - "three-dots-vertical", - "three-dots", - "toggle-off", - "toggle-on", - "toggle2-off", - "toggle2-on", - "toggles", - "toggles2", - "tools", - "tornado", - "trash-fill", - "trash", - "trash2-fill", - "trash2", - "tree-fill", - "tree", - "triangle-fill", - "triangle-half", - "triangle", - "trophy-fill", - "trophy", - "tropical-storm", - "truck-flatbed", - "truck", - "tsunami", - "tv-fill", - "tv", - "twitch", - "twitter", - "type-bold", - "type-h1", - "type-h2", - "type-h3", - "type-italic", - "type-strikethrough", - "type-underline", - "type", - "ui-checks-grid", - "ui-checks", - "ui-radios-grid", - "ui-radios", - "umbrella-fill", - "umbrella", - "union", - "unlock-fill", - "unlock", - "upc-scan", - "upc", - "upload", - "vector-pen", - "view-list", - "view-stacked", - "vinyl-fill", - "vinyl", - "voicemail", - "volume-down-fill", - "volume-down", - "volume-mute-fill", - "volume-mute", - "volume-off-fill", - "volume-off", - "volume-up-fill", - "volume-up", - "vr", - "wallet-fill", - "wallet", - "wallet2", - "watch", - "water", - "whatsapp", - "wifi-1", - "wifi-2", - "wifi-off", - "wifi", - "wind", - "window-dock", - "window-sidebar", - "window", - "wrench", - "x-circle-fill", - "x-circle", - "x-diamond-fill", - "x-diamond", - "x-octagon-fill", - "x-octagon", - "x-square-fill", - "x-square", - "x", - "youtube", - "zoom-in", - "zoom-out", - "bank", - "bank2", - "bell-slash-fill", - "bell-slash", - "cash-coin", - "check-lg", - "coin", - "currency-bitcoin", - "currency-dollar", - "currency-euro", - "currency-exchange", - "currency-pound", - "currency-yen", - "dash-lg", - "exclamation-lg", - "file-earmark-pdf-fill", - "file-earmark-pdf", - "file-pdf-fill", - "file-pdf", - "gender-ambiguous", - "gender-female", - "gender-male", - "gender-trans", - "headset-vr", - "info-lg", - "mastodon", - "messenger", - "piggy-bank-fill", - "piggy-bank", - "pin-map-fill", - "pin-map", - "plus-lg", - "question-lg", - "recycle", - "reddit", - "safe-fill", - "safe2-fill", - "safe2", - "sd-card-fill", - "sd-card", - "skype", - "slash-lg", - "translate", - "x-lg", - "safe", - "apple", - "microsoft", - "windows", - "behance", - "dribbble", - "line", - "medium", - "paypal", - "pinterest", - "signal", - "snapchat", - "spotify", - "stack-overflow", - "strava", - "wordpress", - "vimeo", - "activity", - "easel2-fill", - "easel2", - "easel3-fill", - "easel3", - "fan", - "fingerprint", - "graph-down-arrow", - "graph-up-arrow", - "hypnotize", - "magic", - "person-rolodex", - "person-video", - "person-video2", - "person-video3", - "person-workspace", - "radioactive", - "webcam-fill", - "webcam", - "yin-yang", - "bandaid-fill", - "bandaid", - "bluetooth", - "body-text", - "boombox", - "boxes", - "dpad-fill", - "dpad", - "ear-fill", - "ear", - "envelope-check-fill", - "envelope-check", - "envelope-dash-fill", - "envelope-dash", - "envelope-exclamation-fill", - "envelope-exclamation", - "envelope-plus-fill", - "envelope-plus", - "envelope-slash-fill", - "envelope-slash", - "envelope-x-fill", - "envelope-x", - "explicit-fill", - "explicit", - "git", - "infinity", - "list-columns-reverse", - "list-columns", - "meta", - "nintendo-switch", - "pc-display-horizontal", - "pc-display", - "pc-horizontal", - "pc", - "playstation", - "plus-slash-minus", - "projector-fill", - "projector", - "qr-code-scan", - "qr-code", - "quora", - "quote", - "robot", - "send-check-fill", - "send-check", - "send-dash-fill", - "send-dash", - "send-exclamation-fill", - "send-exclamation", - "send-fill", - "send-plus-fill", - "send-plus", - "send-slash-fill", - "send-slash", - "send-x-fill", - "send-x", - "send", - "steam", - "terminal-dash", - "terminal-plus", - "terminal-split", - "ticket-detailed-fill", - "ticket-detailed", - "ticket-fill", - "ticket-perforated-fill", - "ticket-perforated", - "ticket", - "tiktok", - "window-dash", - "window-desktop", - "window-fullscreen", - "window-plus", - "window-split", - "window-stack", - "window-x", - "xbox", - "ethernet", - "hdmi-fill", - "hdmi", - "usb-c-fill", - "usb-c", - "usb-fill", - "usb-plug-fill", - "usb-plug", - "usb-symbol", - "usb", - "boombox-fill", - "displayport", - "gpu-card", - "memory", - "modem-fill", - "modem", - "motherboard-fill", - "motherboard", - "optical-audio-fill", - "optical-audio", - "pci-card", - "router-fill", - "router", - "thunderbolt-fill", - "thunderbolt", - "usb-drive-fill", - "usb-drive", - "usb-micro-fill", - "usb-micro", - "usb-mini-fill", - "usb-mini", - "cloud-haze2", - "device-hdd-fill", - "device-hdd", - "device-ssd-fill", - "device-ssd", - "displayport-fill", - "mortarboard-fill", - "mortarboard", - "terminal-x", - "arrow-through-heart-fill", - "arrow-through-heart", - "badge-sd-fill", - "badge-sd", - "bag-heart-fill", - "bag-heart", - "balloon-fill", - "balloon-heart-fill", - "balloon-heart", - "balloon", - "box2-fill", - "box2-heart-fill", - "box2-heart", - "box2", - "braces-asterisk", - "calendar-heart-fill", - "calendar-heart", - "calendar2-heart-fill", - "calendar2-heart", - "chat-heart-fill", - "chat-heart", - "chat-left-heart-fill", - "chat-left-heart", - "chat-right-heart-fill", - "chat-right-heart", - "chat-square-heart-fill", - "chat-square-heart", - "clipboard-check-fill", - "clipboard-data-fill", - "clipboard-fill", - "clipboard-heart-fill", - "clipboard-heart", - "clipboard-minus-fill", - "clipboard-plus-fill", - "clipboard-pulse", - "clipboard-x-fill", - "clipboard2-check-fill", - "clipboard2-check", - "clipboard2-data-fill", - "clipboard2-data", - "clipboard2-fill", - "clipboard2-heart-fill", - "clipboard2-heart", - "clipboard2-minus-fill", - "clipboard2-minus", - "clipboard2-plus-fill", - "clipboard2-plus", - "clipboard2-pulse-fill", - "clipboard2-pulse", - "clipboard2-x-fill", - "clipboard2-x", - "clipboard2", - "emoji-kiss-fill", - "emoji-kiss", - "envelope-heart-fill", - "envelope-heart", - "envelope-open-heart-fill", - "envelope-open-heart", - "envelope-paper-fill", - "envelope-paper-heart-fill", - "envelope-paper-heart", - "envelope-paper", - "filetype-aac", - "filetype-ai", - "filetype-bmp", - "filetype-cs", - "filetype-css", - "filetype-csv", - "filetype-doc", - "filetype-docx", - "filetype-exe", - "filetype-gif", - "filetype-heic", - "filetype-html", - "filetype-java", - "filetype-jpg", - "filetype-js", - "filetype-jsx", - "filetype-key", - "filetype-m4p", - "filetype-md", - "filetype-mdx", - "filetype-mov", - "filetype-mp3", - "filetype-mp4", - "filetype-otf", - "filetype-pdf", - "filetype-php", - "filetype-png", - "filetype-ppt", - "filetype-psd", - "filetype-py", - "filetype-raw", - "filetype-rb", - "filetype-sass", - "filetype-scss", - "filetype-sh", - "filetype-svg", - "filetype-tiff", - "filetype-tsx", - "filetype-ttf", - "filetype-txt", - "filetype-wav", - "filetype-woff", - "filetype-xls", - "filetype-xml", - "filetype-yml", - "heart-arrow", - "heart-pulse-fill", - "heart-pulse", - "heartbreak-fill", - "heartbreak", - "hearts", - "hospital-fill", - "hospital", - "house-heart-fill", - "house-heart", - "incognito", - "magnet-fill", - "magnet", - "person-heart", - "person-hearts", - "phone-flip", - "plugin", - "postage-fill", - "postage-heart-fill", - "postage-heart", - "postage", - "postcard-fill", - "postcard-heart-fill", - "postcard-heart", - "postcard", - "search-heart-fill", - "search-heart", - "sliders2-vertical", - "sliders2", - "trash3-fill", - "trash3", - "valentine", - "valentine2", - "wrench-adjustable-circle-fill", - "wrench-adjustable-circle", - "wrench-adjustable", - "filetype-json", - "filetype-pptx", - "filetype-xlsx", - "1-circle-fill", - "1-circle", - "1-square-fill", - "1-square", - "2-circle-fill", - "2-circle", - "2-square-fill", - "2-square", - "3-circle-fill", - "3-circle", - "3-square-fill", - "3-square", - "4-circle-fill", - "4-circle", - "4-square-fill", - "4-square", - "5-circle-fill", - "5-circle", - "5-square-fill", - "5-square", - "6-circle-fill", - "6-circle", - "6-square-fill", - "6-square", - "7-circle-fill", - "7-circle", - "7-square-fill", - "7-square", - "8-circle-fill", - "8-circle", - "8-square-fill", - "8-square", - "9-circle-fill", - "9-circle", - "9-square-fill", - "9-square", - "airplane-engines-fill", - "airplane-engines", - "airplane-fill", - "airplane", - "alexa", - "alipay", - "android", - "android2", - "box-fill", - "box-seam-fill", - "browser-chrome", - "browser-edge", - "browser-firefox", - "browser-safari", - "c-circle-fill", - "c-circle", - "c-square-fill", - "c-square", - "capsule-pill", - "capsule", - "car-front-fill", - "car-front", - "cassette-fill", - "cassette", - "cc-circle-fill", - "cc-circle", - "cc-square-fill", - "cc-square", - "cup-hot-fill", - "cup-hot", - "currency-rupee", - "dropbox", - "escape", - "fast-forward-btn-fill", - "fast-forward-btn", - "fast-forward-circle-fill", - "fast-forward-circle", - "fast-forward-fill", - "fast-forward", - "filetype-sql", - "fire", - "google-play", - "h-circle-fill", - "h-circle", - "h-square-fill", - "h-square", - "indent", - "lungs-fill", - "lungs", - "microsoft-teams", - "p-circle-fill", - "p-circle", - "p-square-fill", - "p-square", - "pass-fill", - "pass", - "prescription", - "prescription2", - "r-circle-fill", - "r-circle", - "r-square-fill", - "r-square", - "repeat-1", - "repeat", - "rewind-btn-fill", - "rewind-btn", - "rewind-circle-fill", - "rewind-circle", - "rewind-fill", - "rewind", - "train-freight-front-fill", - "train-freight-front", - "train-front-fill", - "train-front", - "train-lightrail-front-fill", - "train-lightrail-front", - "truck-front-fill", - "truck-front", - "ubuntu", - "unindent", - "unity", - "universal-access-circle", - "universal-access", - "virus", - "virus2", - "wechat", - "yelp", - "sign-stop-fill", - "sign-stop-lights-fill", - "sign-stop-lights", - "sign-stop", - "sign-turn-left-fill", - "sign-turn-left", - "sign-turn-right-fill", - "sign-turn-right", - "sign-turn-slight-left-fill", - "sign-turn-slight-left", - "sign-turn-slight-right-fill", - "sign-turn-slight-right", - "sign-yield-fill", - "sign-yield", - "ev-station-fill", - "ev-station", - "fuel-pump-diesel-fill", - "fuel-pump-diesel", - "fuel-pump-fill", - "fuel-pump", - "0-circle-fill", - "0-circle", - "0-square-fill", - "0-square", - "rocket-fill", - "rocket-takeoff-fill", - "rocket-takeoff", - "rocket", - "stripe", - "subscript", - "superscript", - "trello", - "envelope-at-fill", - "envelope-at", - "regex", - "text-wrap", - "sign-dead-end-fill", - "sign-dead-end", - "sign-do-not-enter-fill", - "sign-do-not-enter", - "sign-intersection-fill", - "sign-intersection-side-fill", - "sign-intersection-side", - "sign-intersection-t-fill", - "sign-intersection-t", - "sign-intersection-y-fill", - "sign-intersection-y", - "sign-intersection", - "sign-merge-left-fill", - "sign-merge-left", - "sign-merge-right-fill", - "sign-merge-right", - "sign-no-left-turn-fill", - "sign-no-left-turn", - "sign-no-parking-fill", - "sign-no-parking", - "sign-no-right-turn-fill", - "sign-no-right-turn", - "sign-railroad-fill", - "sign-railroad", - "building-add", - "building-check", - "building-dash", - "building-down", - "building-exclamation", - "building-fill-add", - "building-fill-check", - "building-fill-dash", - "building-fill-down", - "building-fill-exclamation", - "building-fill-gear", - "building-fill-lock", - "building-fill-slash", - "building-fill-up", - "building-fill-x", - "building-fill", - "building-gear", - "building-lock", - "building-slash", - "building-up", - "building-x", - "buildings-fill", - "buildings", - "bus-front-fill", - "bus-front", - "ev-front-fill", - "ev-front", - "globe-americas", - "globe-asia-australia", - "globe-central-south-asia", - "globe-europe-africa", - "house-add-fill", - "house-add", - "house-check-fill", - "house-check", - "house-dash-fill", - "house-dash", - "house-down-fill", - "house-down", - "house-exclamation-fill", - "house-exclamation", - "house-gear-fill", - "house-gear", - "house-lock-fill", - "house-lock", - "house-slash-fill", - "house-slash", - "house-up-fill", - "house-up", - "house-x-fill", - "house-x", - "person-add", - "person-down", - "person-exclamation", - "person-fill-add", - "person-fill-check", - "person-fill-dash", - "person-fill-down", - "person-fill-exclamation", - "person-fill-gear", - "person-fill-lock", - "person-fill-slash", - "person-fill-up", - "person-fill-x", - "person-gear", - "person-lock", - "person-slash", - "person-up", - "scooter", - "taxi-front-fill", - "taxi-front", - "amd", - "database-add", - "database-check", - "database-dash", - "database-down", - "database-exclamation", - "database-fill-add", - "database-fill-check", - "database-fill-dash", - "database-fill-down", - "database-fill-exclamation", - "database-fill-gear", - "database-fill-lock", - "database-fill-slash", - "database-fill-up", - "database-fill-x", - "database-fill", - "database-gear", - "database-lock", - "database-slash", - "database-up", - "database-x", - "database", - "houses-fill", - "houses", - "nvidia", - "person-vcard-fill", - "person-vcard", - "sina-weibo", - "tencent-qq", - "wikipedia", - "alphabet-uppercase", - "alphabet", - "amazon", - "arrows-collapse-vertical", - "arrows-expand-vertical", - "arrows-vertical", - "arrows", - "ban-fill", - "ban", - "bing", - "cake", - "cake2", - "cookie", - "copy", - "crosshair", - "crosshair2", - "emoji-astonished-fill", - "emoji-astonished", - "emoji-grimace-fill", - "emoji-grimace", - "emoji-grin-fill", - "emoji-grin", - "emoji-surprise-fill", - "emoji-surprise", - "emoji-tear-fill", - "emoji-tear", - "envelope-arrow-down-fill", - "envelope-arrow-down", - "envelope-arrow-up-fill", - "envelope-arrow-up", - "feather", - "feather2", - "floppy-fill", - "floppy", - "floppy2-fill", - "floppy2", - "gitlab", - "highlighter", - "marker-tip", - "nvme-fill", - "nvme", - "opencollective", - "pci-card-network", - "pci-card-sound", - "radar", - "send-arrow-down-fill", - "send-arrow-down", - "send-arrow-up-fill", - "send-arrow-up", - "sim-slash-fill", - "sim-slash", - "sourceforge", - "substack", - "threads-fill", - "threads", - "transparency", - "twitter-x", - "type-h4", - "type-h5", - "type-h6", - "backpack-fill", - "backpack", - "backpack2-fill", - "backpack2", - "backpack3-fill", - "backpack3", - "backpack4-fill", - "backpack4", - "brilliance", - "cake-fill", - "cake2-fill", - "duffle-fill", - "duffle", - "exposure", - "gender-neuter", - "highlights", - "luggage-fill", - "luggage", - "mailbox-flag", - "mailbox2-flag", - "noise-reduction", - "passport-fill", - "passport", - "person-arms-up", - "person-raised-hand", - "person-standing-dress", - "person-standing", - "person-walking", - "person-wheelchair", - "shadows", - "suitcase-fill", - "suitcase-lg-fill", - "suitcase-lg", - "suitcase", - "suitcase2-fill", - "suitcase2", - "vignette", - ]; -} diff --git a/Foxnouns.Backend/Utils/BootstrapIcons.cs b/Foxnouns.Backend/Utils/BootstrapIcons.cs deleted file mode 100644 index 60461b0..0000000 --- a/Foxnouns.Backend/Utils/BootstrapIcons.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -namespace Foxnouns.Backend.Utils; - -public static partial class BootstrapIcons -{ - public static bool IsValid(string icon) => Icons.Contains(icon); -} diff --git a/Foxnouns.Backend/Utils/OauthUtils.cs b/Foxnouns.Backend/Utils/OauthUtils.cs new file mode 100644 index 0000000..4cbc83a --- /dev/null +++ b/Foxnouns.Backend/Utils/OauthUtils.cs @@ -0,0 +1,83 @@ +using System.Security.Cryptography; +using Foxnouns.Backend.Database.Models; + +namespace Foxnouns.Backend.Utils; + +public static class OauthUtils +{ + public const string ClientCredentials = "client_credentials"; + public const string AuthorizationCode = "authorization_code"; + private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"]; + + public static readonly string[] UserScopes = + ["user.read_hidden", "user.read_privileged", "user.update"]; + + public static readonly string[] MemberScopes = ["member.read", "member.update", "member.create"]; + + /// + /// All scopes endpoints can be secured by. This does *not* include the catch-all token scopes. + /// + public static readonly string[] Scopes = ["identify", ..UserScopes, ..MemberScopes]; + + /// + /// All scopes that can be granted to applications and tokens. Includes the catch-all token scopes, + /// except for "*" which is only granted to the frontend. + /// + public static readonly string[] ApplicationScopes = [..Scopes, "user", "member"]; + + public static string[] ExpandScopes(this string[] scopes) + { + if (scopes.Contains("*")) return Scopes; + List expandedScopes = ["identify"]; + if (scopes.Contains("user")) expandedScopes.AddRange(UserScopes); + if (scopes.Contains("member")) expandedScopes.AddRange(MemberScopes); + + return expandedScopes.ToArray(); + } + + private static string[] ExpandAppScopes(this string[] scopes) + { + var expandedScopes = scopes.ExpandScopes().ToList(); + if (scopes.Contains("user")) expandedScopes.Add("user"); + if (scopes.Contains("member")) expandedScopes.Add("member"); + return expandedScopes.ToArray(); + } + + public static bool ValidateScopes(Application application, string[] scopes) + { + var expandedScopes = scopes.ExpandScopes(); + var appScopes = application.Scopes.ExpandAppScopes(); + return !expandedScopes.Except(appScopes).Any(); + } + + public static bool ValidateRedirectUri(string uri) + { + try + { + var scheme = new Uri(uri).Scheme; + return !ForbiddenSchemes.Contains(scheme); + } + catch + { + return false; + } + } + + + public static bool TryFromBase64String(string b64, out byte[] bytes) + { + try + { + bytes = Convert.FromBase64String(b64); + return true; + } + catch + { + bytes = []; + return false; + } + } + + public static string RandomToken(int bytes = 48) => + Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs b/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs deleted file mode 100644 index 9835b50..0000000 --- a/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Database; -using Microsoft.AspNetCore.OpenApi; -using Microsoft.OpenApi.Any; -using Microsoft.OpenApi.Models; -using Newtonsoft.Json.Serialization; - -namespace Foxnouns.Backend.Utils.OpenApi; - -public class PropertyKeySchemaTransformer : IOpenApiSchemaTransformer -{ - private static readonly DefaultContractResolver SnakeCaseConverter = new() - { - NamingStrategy = new SnakeCaseNamingStrategy(), - }; - - public Task TransformAsync( - OpenApiSchema schema, - OpenApiSchemaTransformerContext context, - CancellationToken cancellationToken - ) - { - Dictionary newProperties = new(); - foreach (KeyValuePair property in schema.Properties) - { - newProperties[SnakeCaseConverter.GetResolvedPropertyName(property.Key)] = - property.Value; - } - - schema.Properties = newProperties; - schema.Required = schema - .Required.Select(SnakeCaseConverter.GetResolvedPropertyName) - .ToHashSet(); - - return Task.CompletedTask; - } -} - -public class ExampleFixingSchemaTransformer : IOpenApiSchemaTransformer -{ - public Task TransformAsync( - OpenApiSchema schema, - OpenApiSchemaTransformerContext context, - CancellationToken cancellationToken - ) - { - if (context.JsonTypeInfo.Type == typeof(Snowflake)) - { - schema.Type = "string"; - schema.Example = new OpenApiString("999999999999999999"); - } - - return Task.CompletedTask; - } -} - -public class DocumentTransformer(Config config) : IOpenApiDocumentTransformer -{ - public Task TransformAsync( - OpenApiDocument document, - OpenApiDocumentTransformerContext context, - CancellationToken cancellationToken - ) - { - document.Info.Title = "pronouns.cc API"; - document.Info.Version = "2.0.0"; - - document.Servers.Clear(); - document.Servers.Add(new OpenApiServer { Url = config.BaseUrl }); - return Task.CompletedTask; - } -} diff --git a/Foxnouns.Backend/Utils/PatchRequest.cs b/Foxnouns.Backend/Utils/PatchRequest.cs deleted file mode 100644 index 2197b92..0000000 --- a/Foxnouns.Backend/Utils/PatchRequest.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace Foxnouns.Backend.Utils; - -/// -/// A base class used for PATCH requests which stores information on whether a key is explicitly set to null or not passed at all. -/// -/// HasProperty() should not be used for properties that cannot be set to null--a null value should be treated -/// as an unset value in those cases. -/// -/// -public abstract class PatchRequest -{ - private readonly HashSet _properties = []; - - public bool HasProperty(string propertyName) => _properties.Contains(propertyName); - - public void SetHasProperty(string propertyName) => _properties.Add(propertyName); -} - -/// -/// A custom contract resolver to reduce the boilerplate needed to use . -/// Based on this StackOverflow answer: https://stackoverflow.com/a/58748036 -/// -public class PatchRequestContractResolver : DefaultContractResolver -{ - protected override JsonProperty CreateProperty( - MemberInfo member, - MemberSerialization memberSerialization - ) - { - JsonProperty prop = base.CreateProperty(member, memberSerialization); - - prop.SetIsSpecified += (o, _) => - { - if (o is not PatchRequest patchRequest) - return; - patchRequest.SetHasProperty(prop.UnderlyingName!); - }; - - return prop; - } -} diff --git a/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs b/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs deleted file mode 100644 index 3269c59..0000000 --- a/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using System.Globalization; -using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Serialization; - -namespace Foxnouns.Backend.Utils; - -/// -/// A custom StringEnumConverter that converts enum members to SCREAMING_SNAKE_CASE, rather than CamelCase as is the default. -/// Newtonsoft.Json doesn't provide a screaming snake case naming strategy, so we just wrap the normal snake case one and convert it to uppercase. -/// -public class ScreamingSnakeCaseEnumConverter() - : StringEnumConverter(new ScreamingSnakeCaseNamingStrategy(), false) -{ - private class ScreamingSnakeCaseNamingStrategy : SnakeCaseNamingStrategy - { - protected override string ResolvePropertyName(string name) => - base.ResolvePropertyName(name).ToUpper(CultureInfo.InvariantCulture); - } -} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs deleted file mode 100644 index 3eed6e2..0000000 --- a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -using Foxnouns.Backend.Dto; - -namespace Foxnouns.Backend.Utils; - -public static partial class ValidationUtils -{ - public const int MaxCustomPreferences = 25; - public const int MaxPreferenceTooltipLength = 128; - - public static List<(string, ValidationError?)> ValidateCustomPreferences( - List preferences - ) - { - var errors = new List<(string, ValidationError?)>(); - - if (preferences.Count > MaxCustomPreferences) - { - errors.Add( - ( - "custom_preferences", - ValidationError.LengthError( - "Too many custom preferences", - 0, - MaxCustomPreferences, - preferences.Count - ) - ) - ); - } - - if (preferences.Count > 50) - return errors; - - foreach ((CustomPreferenceUpdateRequest? p, int i) in preferences.Select((p, i) => (p, i))) - { - if (!BootstrapIcons.IsValid(p.Icon)) - { - errors.Add( - ( - $"custom_preferences.{i}.icon", - ValidationError.DisallowedValueError("Invalid icon name", [], p.Icon) - ) - ); - } - - if (p.Tooltip.Length is 1 or > MaxPreferenceTooltipLength) - { - errors.Add( - ( - $"custom_preferences.{i}.tooltip", - ValidationError.LengthError( - "Tooltip is too short or too long", - 1, - MaxPreferenceTooltipLength, - p.Tooltip.Length - ) - ) - ); - } - } - - return errors; - } -} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs deleted file mode 100644 index 82ee485..0000000 --- a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -namespace Foxnouns.Backend.Utils; - -public static partial class ValidationUtils -{ - public const int MaximumReportContextLength = 512; - - public static ValidationError? ValidateReportContext(string? context) => - context?.Length > MaximumReportContextLength - ? ValidationError.GenericValidationError("Report context is too long", null) - : null; - - public const int MinimumPasswordLength = 12; - public const int MaximumPasswordLength = 1024; - - public static ValidationError? ValidatePassword(string password) => - password.Length switch - { - < MinimumPasswordLength => ValidationError.LengthError( - "Password is too short", - MinimumPasswordLength, - MaximumPasswordLength, - password.Length - ), - > MaximumPasswordLength => ValidationError.LengthError( - "Password is too long", - MinimumPasswordLength, - MaximumPasswordLength, - password.Length - ), - _ => null, - }; -} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs deleted file mode 100644 index a17d331..0000000 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . -namespace Foxnouns.Backend.Utils; - -/// -/// Static methods for validating user input (mostly making sure it's not too short or too long) -/// -public static partial class ValidationUtils -{ - public static void Validate(IEnumerable<(string, ValidationError?)> errors) - { - errors = errors.Where(e => e.Item2 != null).ToList(); - if (!errors.Any()) - return; - - var errorDict = new Dictionary>(); - foreach ((string, ValidationError?) error in errors) - { - if (errorDict.TryGetValue(error.Item1, out IEnumerable? value)) - errorDict[error.Item1] = value.Append(error.Item2!); - errorDict.Add(error.Item1, [error.Item2!]); - } - - throw new ApiError.BadRequest("Error validating input", errorDict); - } -} diff --git a/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml b/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml deleted file mode 100644 index fb85a65..0000000 --- a/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml +++ /dev/null @@ -1,12 +0,0 @@ -@model Foxnouns.Backend.Mailables.AccountCreationMailableView - -

- Please continue creating a new pronouns.cc account by using the following link: -
- @Model.BaseUrl/auth/callback/email/@Model.Code -
- Note that this link will expire in one hour. -

-

- If you didn't mean to create a new account, feel free to ignore this email. -

\ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml b/Foxnouns.Backend/Views/Mail/AddEmail.cshtml deleted file mode 100644 index fcdd2b2..0000000 --- a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml +++ /dev/null @@ -1,12 +0,0 @@ -@model Foxnouns.Backend.Mailables.AddEmailMailableView - -

- Hello @@@Model.Username, please confirm adding this email address to your account by using the following link: -
- @Model.BaseUrl/auth/callback/email/@Model.Code -
- Note that this link will expire in one hour. -

-

- If you didn't mean to link this email address to @@@Model.Username, feel free to ignore this email. -

\ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/Layout.cshtml b/Foxnouns.Backend/Views/Mail/Layout.cshtml deleted file mode 100644 index 6b2a68a..0000000 --- a/Foxnouns.Backend/Views/Mail/Layout.cshtml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - -@RenderBody() - - \ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml b/Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml deleted file mode 100644 index 458dcdf..0000000 --- a/Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@model Foxnouns.Backend.Mailables.PasswordChangedMailableView - -

- Your password has been changed using a "forgot password" link. - If this wasn't you, please a password reset immediately: -
- @Model.BaseUrl/auth/forgot-password -

\ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/ResetPassword.cshtml b/Foxnouns.Backend/Views/Mail/ResetPassword.cshtml deleted file mode 100644 index f141d8b..0000000 --- a/Foxnouns.Backend/Views/Mail/ResetPassword.cshtml +++ /dev/null @@ -1,14 +0,0 @@ -@model Foxnouns.Backend.Mailables.ResetPasswordMailableView - -

- Somebody (hopefully you!) has requested a password reset. - You can use the following link to do this: -
- @Model.BaseUrl/auth/forgot-password/@Model.Code -
- Note that this link will expire in one hour. -

-

- If you weren't expecting this email, you don't have to do anything. - Your password can't be changed without the above link. -

\ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml b/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml deleted file mode 100644 index f13b1c3..0000000 --- a/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml +++ /dev/null @@ -1,2 +0,0 @@ -@using Foxnouns.Backend -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers \ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml b/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml deleted file mode 100644 index 4080127..0000000 --- a/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "~/Views/Mail/Layout.cshtml"; -} \ No newline at end of file diff --git a/Foxnouns.Backend/appSettings.json b/Foxnouns.Backend/appSettings.json deleted file mode 100644 index ae6b417..0000000 --- a/Foxnouns.Backend/appSettings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "Coravel": { - "Queue": { - "ConsummationDelay": 1 - } - } -} \ No newline at end of file diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini deleted file mode 100644 index 4d7c17c..0000000 --- a/Foxnouns.Backend/config.example.ini +++ /dev/null @@ -1,62 +0,0 @@ -; The host the server will listen on -Host = localhost -; The port the server will listen on -Port = 6000 -; The base *external* URL -BaseUrl = https://pronouns.localhost -; The base URL for media, without a trailing slash. This must be publicly accessible. -MediaBaseUrl = https://cdn-staging.pronouns.localhost - -[Logging] -; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal -LogEventLevel = Debug -; The URL to the Seq instance (optional) -SeqLogUrl = http://localhost:5341 -; The Sentry DSN to log to (optional) -SentryUrl = https://examplePublicKey@o0.ingest.sentry.io/0 -; Whether to trace performance with Sentry (optional) -SentryTracing = true -; Percentage of performance traces to send to Sentry (optional). Defaults to 0.0 (no traces at all) -SentryTracesSampleRate = 1.0 -; Whether to log SQL queries. Note that this is very verbose. Defaults to false. -LogQueries = false -; Whether metrics are enabled. If this is set to true, Foxnouns.NET will rely on Prometheus scraping metrics to update stats. -; If set to false, a background service will be used instead. Does not actually disable the /metrics endpoint. -; Defaults to false. -EnableMetrics = true -; The port the /metrics endpoint will listen on. Defaults to 5001. -MetricsPort = 5001 - -[Database] -; The database URL in ADO.NET format. -Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns" -; Whether to enable connection pooling. This should be turned off if using pgbouncer. Defaults to true. -EnablePooling = true -; The timeout for opening new connections. Defaults to 5. -Timeout = 5 -; The maximum number of open connections. Defaults to 50. -MaxPoolSize = 50 - -[Storage] -Endpoint = -AccessKey = -SecretKey = -Bucket = pronounscc - -[Limits] -MaxMemberCount = 5000 - -[EmailAuth] -; The address that emails will be sent from. If not set, email auth is disabled. -From = noreply@accounts.pronouns.cc - -; The Coravel mail driver configuration. Keys should be self-explanatory. -[Coravel:Mail] -Host = localhost -Port = 1025 -Username = smtp-username -Password = smtp-password - -[DiscordAuth] -ClientId = -ClientSecret = diff --git a/Foxnouns.Backend/config.ini b/Foxnouns.Backend/config.ini new file mode 100644 index 0000000..e0e13c4 --- /dev/null +++ b/Foxnouns.Backend/config.ini @@ -0,0 +1,20 @@ +; The host the server will listen on +Host = localhost +; The port the server will listen on +Port = 5000 +; The base *external* URL +BaseUrl = https://pronouns.localhost + +; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal +LogEventLevel = Verbose + +SeqLogUrl = http://localhost:5341 + +[Database] +; The database URL in ADO.NET format. +Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns" + +; The timeout for opening new connections. Defaults to 5. +Timeout = 5 +; The maximum number of open connections. Defaults to 50. +MaxPoolSize = 50 diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json deleted file mode 100644 index 365ee8c..0000000 --- a/Foxnouns.Backend/packages.lock.json +++ /dev/null @@ -1,1070 +0,0 @@ -{ - "version": 1, - "dependencies": { - "net9.0": { - "Coravel": { - "type": "Direct", - "requested": "[6.0.2, )", - "resolved": "6.0.2", - "contentHash": "/XZiRId4Ilar/OqjGKdxkZWfW97ekeT0wgiWNjGdqf8pPxiK508//Zkc0xrKMDOqchFT7B/oqAoQ+Vrx1txpPQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Memory": "3.1.0", - "Microsoft.Extensions.Configuration.Binder": "6.0.0", - "Microsoft.Extensions.DependencyInjection": "6.0.1", - "Microsoft.Extensions.Hosting.Abstractions": "3.1.0", - "Microsoft.Extensions.Logging.Abstractions": "3.1.0" - } - }, - "Coravel.Mailer": { - "type": "Direct", - "requested": "[7.1.0, )", - "resolved": "7.1.0", - "contentHash": "yMbUrwKl5/HbJeX8JkHa8Q3CPTJ3OmPyDSG7sULbXGEhzc2GiYIh7pmVhI1FFeL3VUtFavMDkS8PTwEeCpiwlg==", - "dependencies": { - "MailKit": "4.8.0", - "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.36" - } - }, - "EFCore.NamingConventions": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "heKIYzPdEWx+Ba4xuG6jfEssW9rEi7I0lX38eoN7wo7qgg9uw7nn8UEmDQfwGEYPzSDpetCVANnDr5tqt2Asjg==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.0, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.0, 10.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" - } - }, - "EntityFrameworkCore.Exceptions.PostgreSQL": { - "type": "Direct", - "requested": "[8.1.3, )", - "resolved": "8.1.3", - "contentHash": "hqTsPy2SeupzawK/AeH5/8/K7+KEdZjQbyKVlxBX45ei86eU8D14X9E06uS6MX+J5TdSKY6o+WXGS7G2k8Xvyw==", - "dependencies": { - "EntityFrameworkCore.Exceptions.Common": "8.1.3", - "Npgsql": "8.0.3" - } - }, - "Hangfire": { - "type": "Direct", - "requested": "[1.8.18, )", - "resolved": "1.8.18", - "contentHash": "EY+UqMHTOQAtdjeJf3jlnj8MpENyDPTpA6OHMncucVlkaongZjrx+gCN4bgma7vD3BNHqfQ7irYrfE5p1DOBEQ==", - "dependencies": { - "Hangfire.AspNetCore": "[1.8.18]", - "Hangfire.Core": "[1.8.18]", - "Hangfire.SqlServer": "[1.8.18]" - } - }, - "Hangfire.Core": { - "type": "Direct", - "requested": "[1.8.18, )", - "resolved": "1.8.18", - "contentHash": "oNAkV8QQoYg5+vM2M024NBk49EhTO2BmKDLuQaKNew23RpH9OUGtKDl1KldBdDJrD8TMFzjhWCArol3igd2i2w==", - "dependencies": { - "Newtonsoft.Json": "11.0.1" - } - }, - "Hangfire.Redis.StackExchange": { - "type": "Direct", - "requested": "[1.9.4, )", - "resolved": "1.9.4", - "contentHash": "rB4eGf4+hFhdnrN3//2O39JGuy1ThIKL3oTdVI2F3HqmSaSD9Cixl2xmMAqGJMld39Ke7eoP9sxbxnpVnYW66g==", - "dependencies": { - "Hangfire.Core": "1.8.7", - "Newtonsoft.Json": "13.0.3", - "StackExchange.Redis": "2.7.10" - } - }, - "Humanizer.Core": { - "type": "Direct", - "requested": "[2.14.1, )", - "resolved": "2.14.1", - "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" - }, - "JetBrains.Annotations": { - "type": "Direct", - "requested": "[2024.3.0, )", - "resolved": "2024.3.0", - "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" - }, - "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "cCnaxji6nqIHHLAEhZ6QirXCvwJNi0Q/qCPLkRW5SqMYNuOwoQdGk1KAhW65phBq1VHGt7wLbadpuGPGqfiZuA==", - "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "9.0.2", - "Newtonsoft.Json": "13.0.3", - "Newtonsoft.Json.Bson": "1.0.2" - } - }, - "Microsoft.AspNetCore.OpenApi": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "JUndpjRNdG8GvzBLH/J4hen4ehWaPcshtiQ6+sUs1Bcj3a7dOsmWpDloDlpPeMOVSlhHwUJ3Xld0ClZjsFLgFQ==", - "dependencies": { - "Microsoft.OpenApi": "1.6.17" - } - }, - "Microsoft.EntityFrameworkCore": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "P90ZuybgcpW32y985eOYxSoZ9IiL0UTYQlY0y1Pt1iHAnpZj/dQHREpSpry1RNvk8YjAeoAkWFdem5conqB9zQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.2", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.2", - "Microsoft.Extensions.Caching.Memory": "9.0.2", - "Microsoft.Extensions.Logging": "9.0.2" - } - }, - "Microsoft.EntityFrameworkCore.Design": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "WWRmTxb/yd05cTW+k32lLvIhffxilgYvwKHDxiqe7GRLKeceyMspuf5BRpW65sFF7S2G+Be9JgjUe1ypGqt9tg==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Build.Framework": "17.8.3", - "Microsoft.Build.Locator": "1.7.8", - "Microsoft.CodeAnalysis.CSharp": "4.8.0", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.8.0", - "Microsoft.CodeAnalysis.Workspaces.MSBuild": "4.8.0", - "Microsoft.EntityFrameworkCore.Relational": "9.0.2", - "Microsoft.Extensions.Caching.Memory": "9.0.2", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", - "Microsoft.Extensions.DependencyModel": "9.0.2", - "Microsoft.Extensions.Logging": "9.0.2", - "Mono.TextTemplating": "3.0.0", - "System.Text.Json": "9.0.2" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "AlEfp0DMz8E1h1Exi8LBrUCNmCYcGDfSM4F/uK1D1cYx/R3w0LVvlmjICqxqXTsy7BEZaCf5leRZY2FuPEiFaw==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.Logging.Abstractions": "9.0.2", - "Microsoft.Extensions.Options": "9.0.2", - "Microsoft.Extensions.Primitives": "9.0.2" - } - }, - "Microsoft.Extensions.Http.Resilience": { - "type": "Direct", - "requested": "[9.2.0, )", - "resolved": "9.2.0", - "contentHash": "Km+YyCuk1IaeOsAzPDygtgsUOh3Fi89hpA18si0tFJmpSBf9aKzP9ffV5j7YOoVDvRWirpumXAPQzk1inBsvKw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "9.0.2", - "Microsoft.Extensions.Http.Diagnostics": "9.2.0", - "Microsoft.Extensions.ObjectPool": "9.0.2", - "Microsoft.Extensions.Resilience": "9.2.0" - } - }, - "MimeKit": { - "type": "Direct", - "requested": "[4.10.0, )", - "resolved": "4.10.0", - "contentHash": "GQofI17cH55XSh109hJmHaYMtSFqTX/eUek3UcV7hTnYayAIXZ6eHlv345tfdc+bQ/BrEnYOSZVzx9I3wpvvpg==", - "dependencies": { - "BouncyCastle.Cryptography": "2.5.0", - "System.Formats.Asn1": "8.0.1", - "System.Security.Cryptography.Pkcs": "8.0.1" - } - }, - "Minio": { - "type": "Direct", - "requested": "[6.0.4, )", - "resolved": "6.0.4", - "contentHash": "JckRL95hQ/eDHTQZ/BB7jeR0JyF+bOctMW6uriXHY5YPjCX61hiJGsswGjuDSEViKJEPxtPi3e4IwD/1TJ7PIw==", - "dependencies": { - "CommunityToolkit.HighPerformance": "8.3.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", - "Microsoft.Extensions.Logging": "8.0.0", - "System.IO.Hashing": "8.0.0", - "System.Reactive": "6.0.1" - } - }, - "Newtonsoft.Json": { - "type": "Direct", - "requested": "[13.0.3, )", - "resolved": "13.0.3", - "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" - }, - "NodaTime": { - "type": "Direct", - "requested": "[3.2.1, )", - "resolved": "3.2.1", - "contentHash": "D1aHhUfPQUxU2nfDCVuSLahpp0xCYZTmj/KNH3mSK/tStJYcx9HO9aJ0qbOP3hzjGPV/DXOqY2AHe27Nt4xs4g==" - }, - "Npgsql.EntityFrameworkCore.PostgreSQL": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "mw5vcY2IEc7L+IeGrxpp/J5OSnCcjkjAgJYCm/eD52wpZze8zsSifdqV7zXslSMmfJG2iIUGZyo3KuDtEFKwMQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", - "Npgsql": "9.0.3" - } - }, - "Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": { - "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "QZ80CL3c9xzC83eVMWYWa1RcFZA6HJtpMAKFURlmz+1p0OyysSe8R6f/4sI9vk/nwqF6Fkw3lDgku/xH6HcJYg==", - "dependencies": { - "Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.4", - "Npgsql.NodaTime": "9.0.3" - } - }, - "Npgsql.Json.NET": { - "type": "Direct", - "requested": "[9.0.3, )", - "resolved": "9.0.3", - "contentHash": "lN8p9UKkoXaGUhX3DHg/1W6YeEfbjQiQ7XrJSGREUoDHXOLxDQHJnZ49P/9P2s/pH6HTVgTgT5dijpKoRLN0vQ==", - "dependencies": { - "Newtonsoft.Json": "13.0.3", - "Npgsql": "9.0.3" - } - }, - "prometheus-net": { - "type": "Direct", - "requested": "[8.2.1, )", - "resolved": "8.2.1", - "contentHash": "3wVgdEPOCBF752s2xps5T+VH+c9mJK8S8GKEDg49084P6JZMumTZI5Te6aJ9MQpX0sx7om6JOnBpIi7ZBmmiDQ==", - "dependencies": { - "Microsoft.Extensions.Http": "3.1.0", - "Microsoft.Extensions.ObjectPool": "7.0.0" - } - }, - "prometheus-net.AspNetCore": { - "type": "Direct", - "requested": "[8.2.1, )", - "resolved": "8.2.1", - "contentHash": "/4TfTvbwIDqpaKTiWvEsjUywiHYF9zZvGZF5sK15avoDsUO/WPQbKsF8TiMaesuphdFQPK2z52P0zk6j26V0rQ==", - "dependencies": { - "prometheus-net": "8.2.1" - } - }, - "Roslynator.Analyzers": { - "type": "Direct", - "requested": "[4.13.1, )", - "resolved": "4.13.1", - "contentHash": "KZpLy6ZlCebMk+d/3I5KU2R7AOb4LNJ6tPJqPtvFXmO8bEBHQvCIAvJOnY2tu4C9/aVOROTDYUFADxFqw1gh/g==" - }, - "Scalar.AspNetCore": { - "type": "Direct", - "requested": "[2.0.26, )", - "resolved": "2.0.26", - "contentHash": "0tKBFM7quBq0ifgRWo7eTTVpiTbnwpf/6ygtb/aYVuo0D2gMsYknAJRqEhH8HFBqzntNiYpzHbQSf2b+VAA8sA==" - }, - "Sentry.AspNetCore": { - "type": "Direct", - "requested": "[5.3.0, )", - "resolved": "5.3.0", - "contentHash": "zC2yhwQB0laYWGXLYDCsiKSIqleaEK3fUH9Z5t8Bgvfs2nGX0mHmh9oPqNAAbkVGvni56mhgHHCBxN/kpfkawA==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "9.0.0", - "Sentry.Extensions.Logging": "5.3.0" - } - }, - "Serilog": { - "type": "Direct", - "requested": "[4.2.0, )", - "resolved": "4.2.0", - "contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA==" - }, - "Serilog.AspNetCore": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "JslDajPlBsn3Pww1554flJFTqROvK9zz9jONNQgn0D8Lx2Trw8L0A8/n6zEQK1DAZWXrJwiVLw8cnTR3YFuYsg==", - "dependencies": { - "Serilog": "4.2.0", - "Serilog.Extensions.Hosting": "9.0.0", - "Serilog.Formatting.Compact": "3.0.0", - "Serilog.Settings.Configuration": "9.0.0", - "Serilog.Sinks.Console": "6.0.0", - "Serilog.Sinks.Debug": "3.0.0", - "Serilog.Sinks.File": "6.0.0" - } - }, - "Serilog.Sinks.Console": { - "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "fQGWqVMClCP2yEyTXPIinSr5c+CBGUvBybPxjAGcf7ctDhadFhrQw03Mv8rJ07/wR5PDfFjewf2LimvXCDzpbA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Sinks.Seq": { - "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==", - "dependencies": { - "Serilog": "4.2.0", - "Serilog.Sinks.File": "6.0.0" - } - }, - "SixLabors.ImageSharp": { - "type": "Direct", - "requested": "[3.1.7, )", - "resolved": "3.1.7", - "contentHash": "9fIOOAsyLFid6qKypM2Iy0Z3Q9yoanV8VoYAHtI2sYGMNKzhvRTjgFDHonIiVe+ANtxIxM6SuqUzj0r91nItpA==" - }, - "StackExchange.Redis": { - "type": "Direct", - "requested": "[2.8.31, )", - "resolved": "2.8.31", - "contentHash": "RCHVQa9Zke8k0oBgJn1Yl6BuYy8i6kv+sdMObiH60nOwD6QvWAjxdDwOm+LO78E8WsGiPqgOuItkz98fPS6haQ==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "6.0.0", - "Pipelines.Sockets.Unofficial": "2.2.8" - } - }, - "System.Text.Json": { - "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "4TY2Yokh5Xp8XHFhsY9y84yokS7B0rhkaZCXuRiKppIiKwPVH4lVSFD9EEFzRpXdBM5ZeZXD43tc2vB6njEwwQ==" - }, - "System.Text.RegularExpressions": { - "type": "Direct", - "requested": "[4.3.1, )", - "resolved": "4.3.1", - "contentHash": "N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==", - "dependencies": { - "System.Runtime": "4.3.1" - } - }, - "Yort.Xid.Net": { - "type": "Direct", - "requested": "[2.0.1, )", - "resolved": "2.0.1", - "contentHash": "+3sNX7/RKSKheVuMz9jtWLazD+R4PXpx8va2d9SdDgvKOhETbEb0VYis8K/fD1qm/qOQT57LadToSpzReGMZlw==" - }, - "BouncyCastle.Cryptography": { - "type": "Transitive", - "resolved": "2.5.0", - "contentHash": "rc7vRCq/KD3GtIwSgRtjanGaBwTb9nLenFDZnEcauWlssuuEoxcbMfWA3QWWho6QDMSOSkWjs657McdHzEtEcw==" - }, - "CommunityToolkit.HighPerformance": { - "type": "Transitive", - "resolved": "8.3.0", - "contentHash": "2zc0Wfr9OtEbLqm6J1Jycim/nKmYv+v12CytJ3tZGNzw7n3yjh1vNCMX0kIBaFBk3sw8g0pMR86QJGXGlArC+A==" - }, - "EntityFrameworkCore.Exceptions.Common": { - "type": "Transitive", - "resolved": "8.1.3", - "contentHash": "nweeiVHx4HbDi6+TqendOe0QmN0a9v0AB5FaL83eToqFFztwGIhOqLfveKqJDYSCU51CJShW8kbU1onZLdZZSg==", - "dependencies": { - "Microsoft.EntityFrameworkCore.Relational": "8.0.0" - } - }, - "Hangfire.AspNetCore": { - "type": "Transitive", - "resolved": "1.8.18", - "contentHash": "5D6Do0qgoAnakvh4KnKwhIoUzFU84Z0sCYMB+Sit+ygkpL1P6JGYDcd/9vDBcfr5K3JqBxD4Zh2IK2LOXuuiaw==", - "dependencies": { - "Hangfire.NetCore": "[1.8.18]" - } - }, - "Hangfire.NetCore": { - "type": "Transitive", - "resolved": "1.8.18", - "contentHash": "3KAV9AZ1nqQHC54qR4buNEEKRmQJfq+lODtZxUk5cdi68lV8+9K2f4H1/mIfDlPpgjPFjEfCobNoi2+TIpKySw==", - "dependencies": { - "Hangfire.Core": "[1.8.18]", - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "3.0.0", - "Microsoft.Extensions.Logging.Abstractions": "3.0.0" - } - }, - "Hangfire.SqlServer": { - "type": "Transitive", - "resolved": "1.8.18", - "contentHash": "yBfI2ygYfN/31rOrahfOFHee1mwTrG0ppsmK9awCS0mAr2GEaB9eyYqg/lURgZy8AA8UVJVs5nLHa2hc1pDAVQ==", - "dependencies": { - "Hangfire.Core": "[1.8.18]" - } - }, - "MailKit": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "zZ1UoM4FUnSFUJ9fTl5CEEaejR0DNP6+FDt1OfXnjg4igZntcir1tg/8Ufd6WY5vrpmvToAjluYqjVM24A+5lA==", - "dependencies": { - "MimeKit": "4.8.0", - "System.Formats.Asn1": "8.0.1" - } - }, - "Microsoft.AspNetCore.JsonPatch": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "bZMRhazEBgw9aZ5EBGYt0017CSd+aecsUCnppVjSa1SzWH6C1ieTSQZRAe+H0DzAVzWAoK7HLwKnQUPioopPrA==", - "dependencies": { - "Microsoft.CSharp": "4.7.0", - "Newtonsoft.Json": "13.0.3" - } - }, - "Microsoft.AspNetCore.Mvc.Razor.Extensions": { - "type": "Transitive", - "resolved": "6.0.36", - "contentHash": "KFHRhrGAnd80310lpuWzI7Cf+GidS/h3JaPDFFnSmSGjCxB5vkBv5E+TXclJCJhqPtgNxg+keTC5SF1T9ieG5w==", - "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.36", - "Microsoft.CodeAnalysis.Razor": "6.0.36" - } - }, - "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": { - "type": "Transitive", - "resolved": "6.0.36", - "contentHash": "0OG/wNedsQ9kTMrFuvrUDoJvp6Fxj6BzWgi7AUCluOENxu/0PzbjY9AC5w6mZJ22/AFxn2gFc2m0yOBTfQbiPg==", - "dependencies": { - "Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.36", - "Microsoft.CodeAnalysis.Razor": "6.0.36", - "Microsoft.Extensions.DependencyModel": "6.0.2" - } - }, - "Microsoft.AspNetCore.Razor.Language": { - "type": "Transitive", - "resolved": "6.0.36", - "contentHash": "n5Mg5D0aRrhHJJ6bJcwKqQydIFcgUq0jTlvuynoJjwA2IvAzh8Aqf9cpYagofQbIlIXILkCP6q6FgbngyVtpYA==" - }, - "Microsoft.Bcl.AsyncInterfaces": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "3aeMZ1N0lJoSyzqiP03hqemtb1BijhsJADdobn/4nsMJ8V1H+CrpuduUe4hlRdx+ikBQju1VGjMD1GJ3Sk05Eg==" - }, - "Microsoft.Build.Framework": { - "type": "Transitive", - "resolved": "17.8.3", - "contentHash": "NrQZJW8TlKVPx72yltGb8SVz3P5mNRk9fNiD/ao8jRSk48WqIIdCn99q4IjlVmPcruuQ+yLdjNQLL8Rb4c916g==" - }, - "Microsoft.Build.Locator": { - "type": "Transitive", - "resolved": "1.7.8", - "contentHash": "sPy10x527Ph16S2u0yGME4S6ohBKJ69WfjeGG/bvELYeZVmJdKjxgnlL8cJJJLGV/cZIRqSfB12UDB8ICakOog==" - }, - "Microsoft.CodeAnalysis.Analyzers": { - "type": "Transitive", - "resolved": "3.3.4", - "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" - }, - "Microsoft.CodeAnalysis.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", - "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.4", - "System.Collections.Immutable": "7.0.0", - "System.Reflection.Metadata": "7.0.0", - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "Microsoft.CodeAnalysis.CSharp": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", - "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.CSharp.Workspaces": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "[4.8.0]", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]" - } - }, - "Microsoft.CodeAnalysis.Razor": { - "type": "Transitive", - "resolved": "6.0.36", - "contentHash": "RTLNJglWezr/1IkiWdtDpPYW7X7lwa4ow8E35cHt+sWdWxOnl+ayQqMy1RfbaLp7CLmRmgXSzMMZZU3D4vZi9Q==", - "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.36", - "Microsoft.CodeAnalysis.CSharp": "4.0.0", - "Microsoft.CodeAnalysis.Common": "4.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.Common": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", - "dependencies": { - "Humanizer.Core": "2.14.1", - "Microsoft.Bcl.AsyncInterfaces": "7.0.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "System.Composition": "7.0.0", - "System.IO.Pipelines": "7.0.0", - "System.Threading.Channels": "7.0.0" - } - }, - "Microsoft.CodeAnalysis.Workspaces.MSBuild": { - "type": "Transitive", - "resolved": "4.8.0", - "contentHash": "IEYreI82QZKklp54yPHxZNG9EKSK6nHEkeuf+0Asie9llgS1gp0V1hw7ODG+QyoB7MuAnNQHmeV1Per/ECpv6A==", - "dependencies": { - "Microsoft.Build.Framework": "16.10.0", - "Microsoft.CodeAnalysis.Common": "[4.8.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.8.0]", - "System.Text.Json": "7.0.3" - } - }, - "Microsoft.CSharp": { - "type": "Transitive", - "resolved": "4.7.0", - "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" - }, - "Microsoft.EntityFrameworkCore.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "oVSjNSIYHsk0N66eqAWgDcyo9etEFbUswbz7SmlYR6nGp05byHrJAYM5N8U2aGWJWJI6WvIC2e4TXJgH6GZ6HQ==" - }, - "Microsoft.EntityFrameworkCore.Analyzers": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "w4jzX7XI+L3erVGzbHXpx64A3QaLXxqG3f1vPpGYYZGpxOIHkh7e4iLLD7cq4Ng1vjkwzWl5ZJp0Kj/nHsgFYg==" - }, - "Microsoft.EntityFrameworkCore.Relational": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "r7O4N5uaM95InVSGUj7SMOQWN0f1PBF2Y30ow7Jg+pGX5GJCRVd/1fq83lQ50YMyq+EzyHac5o4CDQA2RsjKJQ==", - "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.2", - "Microsoft.Extensions.Caching.Memory": "9.0.2", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", - "Microsoft.Extensions.Logging": "9.0.2" - } - }, - "Microsoft.Extensions.AmbientMetadata.Application": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "GMCX3zybUB22aAADjYPXrWhhd1HNMkcY5EcFAJnXy/4k5pPpJ6TS4VRl37xfrtosNyzbpO2SI7pd2Q5PvggSdg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.2", - "Microsoft.Extensions.Hosting.Abstractions": "9.0.2", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" - } - }, - "Microsoft.Extensions.Caching.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "a7QhA25n+BzSM5r5d7JznfyluMBGI7z3qyLlFviZ1Eiqv6DdiK27sLZdP/rpYirBM6UYAKxu5TbmfhIy13GN9A==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.2" - } - }, - "Microsoft.Extensions.Compliance.Abstractions": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "Te+N4xphDlGIS90lKJMZyezFiMWKLAtYV2/M8gGJG4thH6xyC7LWhMzgz2+tWMehxwZlBUq2D9DvVpjKBZFTPQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.ObjectPool": "9.0.2" - } - }, - "Microsoft.Extensions.Configuration": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "EBZW+u96tApIvNtjymXEIS44tH0I/jNwABHo4c33AchWOiDWCq2rL3klpnIo+xGrxoVGJzPDISV6hZ+a9C9SzQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", - "Microsoft.Extensions.Primitives": "9.0.2" - } - }, - "Microsoft.Extensions.Configuration.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "I0O/270E/lUNqbBxlRVjxKOMZyYjP88dpEgQTveml+h2lTzAP4vbawLVwjS9SC7lKaU893bwyyNz0IVJYsm9EA==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.2" - } - }, - "Microsoft.Extensions.Configuration.Binder": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "krJ04xR0aPXrOf5dkNASg6aJjsdzexvsMRL6UNOUjiTzqBvRr95sJ1owoKEm89bSONQCfZNhHrAFV9ahDqIPIw==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2" - } - }, - "Microsoft.Extensions.DependencyInjection": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" - } - }, - "Microsoft.Extensions.DependencyInjection.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" - }, - "Microsoft.Extensions.DependencyInjection.AutoActivation": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "WcwfTpl3IcPcaahTVEaJwMUg1eWog1SkIA6jQZZFqMXiMX9/tVkhNB6yzUQmBdGWdlWDDRKpOmK7T7x1Uu05pQ==", - "dependencies": { - "Microsoft.Extensions.Hosting.Abstractions": "9.0.2" - } - }, - "Microsoft.Extensions.DependencyModel": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "3ImbcbS68jy9sKr9Z9ToRbEEX0bvIRdb8zyf5ebtL9Av2CUCGHvaO5wsSXfRfAjr60Vrq0tlmNji9IzAxW6EOw==" - }, - "Microsoft.Extensions.Diagnostics": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "kwFWk6DPaj1Roc0CExRv+TTwjsiERZA730jQIPlwCcS5tMaCAQtaGfwAK0z8CMFpVTiT+MgKXpd/P50qVCuIgg==", - "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.2", - "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" - } - }, - "Microsoft.Extensions.Diagnostics.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "kFwIZEC/37cwKuEm/nXvjF7A/Myz9O7c7P9Csgz6AOiiDE62zdOG5Bu7VkROu1oMYaX0wgijPJ5LqVt6+JKjVg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.Options": "9.0.2" - } - }, - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "et5JevHsLv1w1O1Zhb6LiUfai/nmDRzIHnbrZJdzLsIbbMCKTZpeHuANYIppAD//n12KvgOne05j4cu0GhG9gw==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" - } - }, - "Microsoft.Extensions.FileProviders.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "IcOBmTlr2jySswU+3x8c3ql87FRwTVPQgVKaV5AXzPT5u0VItfNU8SMbESpdSp5STwxT/1R99WYszgHWsVkzhg==", - "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.2" - } - }, - "Microsoft.Extensions.Hosting.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "PvjZW6CMdZbPbOwKsQXYN5VPtIWZQqdTRuBPZiW3skhU3hymB17XSlLVC4uaBbDZU+/3eHG3p80y+MzZxZqR7Q==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.2", - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.2", - "Microsoft.Extensions.Logging.Abstractions": "9.0.2" - } - }, - "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "34+kcwxPZr3Owk9eZx268+gqGNB8G/8Y96gZHomxam0IOH08FhPBjPrLWDtKdVn4+sVUUJnJMpECSTJi4XXCcg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.Diagnostics": "9.0.2", - "Microsoft.Extensions.Logging": "9.0.2", - "Microsoft.Extensions.Logging.Abstractions": "9.0.2", - "Microsoft.Extensions.Options": "9.0.2" - } - }, - "Microsoft.Extensions.Http.Diagnostics": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "Eeup1LuD5hVk5SsKAuX1D7I9sF380MjrNG10IaaauRLOmrRg8rq2TA8PYTXVBXf3MLkZ6m2xpBqRbZdxf8ygkg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0", - "Microsoft.Extensions.Http": "9.0.2", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2", - "Microsoft.Extensions.Telemetry": "9.2.0", - "System.IO.Pipelines": "9.0.2" - } - }, - "Microsoft.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "loV/0UNpt2bD+6kCDzFALVE63CDtqzPeC0LAetkdhiEr/tTNbvOlQ7CBResH7BQBd3cikrwiBfaHdyHMFUlc2g==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.2", - "Microsoft.Extensions.Logging.Abstractions": "9.0.2", - "Microsoft.Extensions.Options": "9.0.2" - } - }, - "Microsoft.Extensions.Logging.Abstractions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "dV9s2Lamc8jSaqhl2BQSPn/AryDIH2sSbQUyLitLXV0ROmsb+SROnn2cH939JFbsNrnf3mIM3GNRKT7P0ldwLg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" - } - }, - "Microsoft.Extensions.Logging.Configuration": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "pnwYZE7U6d3Y6iMVqADOAUUMMBGYAQPsT3fMwVr/V1Wdpe5DuVGFcViZavUthSJ5724NmelIl1cYy+kRfKfRPQ==", - "dependencies": { - "Microsoft.Extensions.Configuration": "9.0.2", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", - "Microsoft.Extensions.Configuration.Binder": "9.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.Logging": "9.0.2", - "Microsoft.Extensions.Logging.Abstractions": "9.0.2", - "Microsoft.Extensions.Options": "9.0.2", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2" - } - }, - "Microsoft.Extensions.ObjectPool": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "nWx7uY6lfkmtpyC2dGc0IxtrZZs/LnLCQHw3YYQucbqWj8a27U/dZ+eh72O3ZiolqLzzLkVzoC+w/M8dZwxRTw==" - }, - "Microsoft.Extensions.Options": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "zr98z+AN8+isdmDmQRuEJ/DAKZGUTHmdv3t0ZzjHvNqvA44nAgkXE9kYtfoN6581iALChhVaSw2Owt+Z2lVbkQ==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.Primitives": "9.0.2" - } - }, - "Microsoft.Extensions.Options.ConfigurationExtensions": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "OPm1NXdMg4Kb4Kz+YHdbBQfekh7MqQZ7liZ5dYUd+IbJakinv9Fl7Ck6Strbgs0a6E76UGbP/jHR532K/7/feQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "9.0.2", - "Microsoft.Extensions.Configuration.Binder": "9.0.2", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", - "Microsoft.Extensions.Options": "9.0.2", - "Microsoft.Extensions.Primitives": "9.0.2" - } - }, - "Microsoft.Extensions.Primitives": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "puBMtKe/wLuYa7H6docBkLlfec+h8L35DXqsDKKJgW0WY5oCwJ3cBJKcDaZchv6knAyqOMfsl6VUbaR++E5LXA==" - }, - "Microsoft.Extensions.Resilience": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "dyaM+Jeznh/i21bOrrRs3xceFfn0571EOjOq95dRXmL1rHDLC4ExhACJ2xipRBP6g1AgRNqmryi+hMrVWWgmlg==", - "dependencies": { - "Microsoft.Extensions.Diagnostics": "9.0.2", - "Microsoft.Extensions.Diagnostics.ExceptionSummarization": "9.2.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.2", - "Microsoft.Extensions.Telemetry.Abstractions": "9.2.0", - "Polly.Extensions": "8.4.2", - "Polly.RateLimiting": "8.4.2" - } - }, - "Microsoft.Extensions.Telemetry": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "4+bw7W4RrAMrND9TxonnSmzJOdXiPxljoda8OPJiReIN607mKCc0t0Mf28sHNsTujO1XQw28wsI0poxeeQxohw==", - "dependencies": { - "Microsoft.Extensions.AmbientMetadata.Application": "9.2.0", - "Microsoft.Extensions.DependencyInjection.AutoActivation": "9.2.0", - "Microsoft.Extensions.Logging.Configuration": "9.0.2", - "Microsoft.Extensions.ObjectPool": "9.0.2", - "Microsoft.Extensions.Telemetry.Abstractions": "9.2.0" - } - }, - "Microsoft.Extensions.Telemetry.Abstractions": { - "type": "Transitive", - "resolved": "9.2.0", - "contentHash": "kEl+5G3RqS20XaEhHh/nOugcjKEK+rgVtMJra1iuwNzdzQXElelf3vu8TugcT7rIZ/T4T76EKW1OX/fmlxz4hw==", - "dependencies": { - "Microsoft.Extensions.Compliance.Abstractions": "9.2.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.2", - "Microsoft.Extensions.ObjectPool": "9.0.2", - "Microsoft.Extensions.Options": "9.0.2" - } - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" - }, - "Microsoft.NETCore.Targets": { - "type": "Transitive", - "resolved": "1.1.3", - "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" - }, - "Microsoft.OpenApi": { - "type": "Transitive", - "resolved": "1.6.17", - "contentHash": "Le+kehlmrlQfuDFUt1zZ2dVwrhFQtKREdKBo+rexOwaCoYP0/qpgT9tLxCsZjsgR5Itk1UKPcbgO+FyaNid/bA==" - }, - "Mono.TextTemplating": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", - "dependencies": { - "System.CodeDom": "6.0.0" - } - }, - "Newtonsoft.Json.Bson": { - "type": "Transitive", - "resolved": "1.0.2", - "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", - "dependencies": { - "Newtonsoft.Json": "12.0.1" - } - }, - "Npgsql": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.2" - } - }, - "Npgsql.NodaTime": { - "type": "Transitive", - "resolved": "9.0.3", - "contentHash": "PMWXCft/iw+5A7eCeMcy6YZXBst6oeisbCkv2JMQVG4SAFa5vQaf6K2voXzUJCqzwOFcCWs+oT42w2uMDFpchw==", - "dependencies": { - "NodaTime": "3.2.0", - "Npgsql": "9.0.3" - } - }, - "Pipelines.Sockets.Unofficial": { - "type": "Transitive", - "resolved": "2.2.8", - "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", - "dependencies": { - "System.IO.Pipelines": "5.0.1" - } - }, - "Polly.Core": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "BpE2I6HBYYA5tF0Vn4eoQOGYTYIK1BlF5EXVgkWGn3mqUUjbXAr13J6fZVbp7Q3epRR8yshacBMlsHMhpOiV3g==" - }, - "Polly.Extensions": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "GZ9vRVmR0jV2JtZavt+pGUsQ1O1cuRKG7R7VOZI6ZDy9y6RNPvRvXK1tuS4ffUrv8L0FTea59oEuQzgS0R7zSA==", - "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Polly.Core": "8.4.2" - } - }, - "Polly.RateLimiting": { - "type": "Transitive", - "resolved": "8.4.2", - "contentHash": "ehTImQ/eUyO07VYW2WvwSmU9rRH200SKJ/3jku9rOkyWE0A2JxNFmAVms8dSn49QLSjmjFRRSgfNyOgr/2PSmA==", - "dependencies": { - "Polly.Core": "8.4.2", - "System.Threading.RateLimiting": "8.0.0" - } - }, - "Sentry": { - "type": "Transitive", - "resolved": "5.3.0", - "contentHash": "zlBIP7YmYxySwcgapLMj1gdxPEz9rwdrOa4Yjub/TzcAaMQXusRH9hY4CE6pu0EIibZ7C7Hhjhr6xOTlyK8gFQ==" - }, - "Sentry.Extensions.Logging": { - "type": "Transitive", - "resolved": "5.3.0", - "contentHash": "DPN6NXvO4LTH21UM2gUFJwSwVa/fuT3B/UZmQyfSfecqViXrZO7WFuKz/h592YUoGNCumyt8x045bxbz6j9btg==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "9.0.0", - "Microsoft.Extensions.Http": "9.0.0", - "Microsoft.Extensions.Logging.Configuration": "9.0.0", - "Sentry": "5.3.0" - } - }, - "Serilog.Extensions.Hosting": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "u2TRxuxbjvTAldQn7uaAwePkWxTHIqlgjelekBtilAGL5sYyF3+65NWctN4UrwwGLsDC7c3Vz3HnOlu+PcoxXg==", - "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Serilog": "4.2.0", - "Serilog.Extensions.Logging": "9.0.0" - } - }, - "Serilog.Extensions.Logging": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "NwSSYqPJeKNzl5AuXVHpGbr6PkZJFlNa14CdIebVjK3k/76kYj/mz5kiTRNVSsSaxM8kAIa1kpy/qyT9E4npRQ==", - "dependencies": { - "Microsoft.Extensions.Logging": "9.0.0", - "Serilog": "4.2.0" - } - }, - "Serilog.Formatting.Compact": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Settings.Configuration": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "4/Et4Cqwa+F88l5SeFeNZ4c4Z6dEAIKbu3MaQb2Zz9F/g27T5a3wvfMcmCOaAiACjfUb4A6wrlTVfyYUZk3RRQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "9.0.0", - "Microsoft.Extensions.DependencyModel": "9.0.0", - "Serilog": "4.2.0" - } - }, - "Serilog.Sinks.Debug": { - "type": "Transitive", - "resolved": "3.0.0", - "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "Serilog.Sinks.File": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "lxjg89Y8gJMmFxVkbZ+qDgjl+T4yC5F7WSLTvA+5q0R04tfKVLRL/EHpYoJ/MEQd2EeCKDuylBIVnAYMotmh2A==", - "dependencies": { - "Serilog": "4.0.0" - } - }, - "System.CodeDom": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" - }, - "System.Collections.Immutable": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" - }, - "System.Composition": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Convention": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0", - "System.Composition.TypedParts": "7.0.0" - } - }, - "System.Composition.AttributedModel": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" - }, - "System.Composition.Convention": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0" - } - }, - "System.Composition.Hosting": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", - "dependencies": { - "System.Composition.Runtime": "7.0.0" - } - }, - "System.Composition.Runtime": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" - }, - "System.Composition.TypedParts": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", - "dependencies": { - "System.Composition.AttributedModel": "7.0.0", - "System.Composition.Hosting": "7.0.0", - "System.Composition.Runtime": "7.0.0" - } - }, - "System.Formats.Asn1": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" - }, - "System.IO.Hashing": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" - }, - "System.IO.Pipelines": { - "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "UIBaK7c/A3FyQxmX/747xw4rCUkm1BhNiVU617U5jweNJssNjLJkPUGhBsrlDG0BpKWCYKsncD+Kqpy4KmvZZQ==" - }, - "System.Reactive": { - "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" - }, - "System.Reflection.Metadata": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", - "dependencies": { - "System.Collections.Immutable": "7.0.0" - } - }, - "System.Runtime": { - "type": "Transitive", - "resolved": "4.3.1", - "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.1", - "Microsoft.NETCore.Targets": "1.1.3" - } - }, - "System.Runtime.CompilerServices.Unsafe": { - "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" - }, - "System.Security.Cryptography.Pkcs": { - "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" - }, - "System.Threading.Channels": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" - }, - "System.Threading.RateLimiting": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7mu9v0QDv66ar3DpGSZHg9NuNcxDaaAcnMULuZlaTpP9+hwXhrxNGsF5GmLkSHxFdb5bBc1TzeujsRgTrPWi+Q==" - } - } - } -} \ No newline at end of file diff --git a/Foxnouns.Backend/static-pages/.gitignore b/Foxnouns.Backend/static-pages/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/Foxnouns.Backend/static-pages/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj b/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj deleted file mode 100644 index ead15ce..0000000 --- a/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - Exe - net9.0 - enable - enable - - - - - - - - - - - - - diff --git a/Foxnouns.DataMigrator/GoDatabase.cs b/Foxnouns.DataMigrator/GoDatabase.cs deleted file mode 100644 index a2f7720..0000000 --- a/Foxnouns.DataMigrator/GoDatabase.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Data; -using Dapper; -using Foxnouns.DataMigrator.Models; -using Newtonsoft.Json; -using Npgsql; - -namespace Foxnouns.DataMigrator; - -public static class GoDatabase -{ - private static NpgsqlDataSource? _dataSource; - - public static async Task GetConnectionAsync() - { - if (_dataSource != null) - return await _dataSource.OpenConnectionAsync(); - - DefaultTypeMap.MatchNamesWithUnderscores = true; - - SqlMapper.RemoveTypeMap(typeof(ulong)); - SqlMapper.AddTypeHandler(new UlongEncodeAsLongHandler()); - SqlMapper.AddTypeHandler(new JsonTypeHandler()); - SqlMapper.AddTypeHandler(new JsonTypeHandler>()); - SqlMapper.AddTypeHandler(new JsonTypeHandler()); - SqlMapper.AddTypeHandler(new UlongListHandler()); - - string dsn = - Environment.GetEnvironmentVariable("GO_DATABASE") - ?? throw new Exception("$GO_DATABASE is not set"); - - var dataSourceBuilder = new NpgsqlDataSourceBuilder(dsn); - dataSourceBuilder.UseJsonNet(); - - _dataSource = dataSourceBuilder.Build(); - - return await _dataSource.OpenConnectionAsync(); - } - - // dapper why - // taken from https://codeberg.org/starshine/catalogger/src/branch/main/Catalogger.Backend/Database/DatabasePool.cs - private class UlongEncodeAsLongHandler : SqlMapper.TypeHandler - { - public override void SetValue(IDbDataParameter parameter, ulong value) => - parameter.Value = (long)value; - - public override ulong Parse(object value) => - // Cast to long to unbox, then to ulong (???) - (ulong)(long)value; - } - - private class UlongListHandler : SqlMapper.TypeHandler> - { - public override void SetValue(IDbDataParameter parameter, List? value) => - parameter.Value = value?.Select(i => (long)i).ToArray(); - - public override List? Parse(object value) => - ((long[])value).Select(i => (ulong)i).ToList(); - } - - private class JsonTypeHandler : SqlMapper.TypeHandler - { - public override void SetValue(IDbDataParameter parameter, T? value) => - parameter.Value = JsonConvert.SerializeObject(value); - - public override T? Parse(object value) - { - var json = (string)value; - return JsonConvert.DeserializeObject(json) ?? default; - } - } -} diff --git a/Foxnouns.DataMigrator/Models/GoMember.cs b/Foxnouns.DataMigrator/Models/GoMember.cs deleted file mode 100644 index d1c52fe..0000000 --- a/Foxnouns.DataMigrator/Models/GoMember.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Foxnouns.Backend.Database; - -namespace Foxnouns.DataMigrator.Models; - -public class GoMember -{ - public required string Id { get; init; } - public required string Name { get; init; } - public string? Bio { get; init; } - public string[]? Links { get; init; } - public string? DisplayName { get; init; } - public GoFieldEntry[] Names { get; init; } = []; - public GoPronounEntry[] Pronouns { get; init; } = []; - public string? Avatar { get; init; } - public required bool Unlisted { get; init; } - public required string Sid { get; init; } - public required Snowflake SnowflakeId { get; init; } -} - -public class GoMemberField -{ - public required string MemberId { get; init; } - public required long Id { get; init; } - public required string Name { get; init; } - public required GoFieldEntry[] Entries { get; init; } -} diff --git a/Foxnouns.DataMigrator/Models/GoUser.cs b/Foxnouns.DataMigrator/Models/GoUser.cs deleted file mode 100644 index 20f5d51..0000000 --- a/Foxnouns.DataMigrator/Models/GoUser.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; - -namespace Foxnouns.DataMigrator.Models; - -public class GoUser -{ - public required string Id { get; init; } - public required string Username { get; init; } - public string? DisplayName { get; init; } - public string? Bio { get; init; } - public string[]? Links { get; init; } - public string? Discord { get; init; } - public string? DiscordUsername { get; init; } - public DateTimeOffset? DeletedAt { get; init; } - public bool? SelfDelete { get; init; } - public string? DeleteReason { get; init; } - public GoFieldEntry[] Names { get; init; } = []; - public GoPronounEntry[] Pronouns { get; init; } = []; - public string? Avatar { get; init; } - public string? Fediverse { get; init; } - public string? FediverseUsername { get; init; } - public int? FediverseAppId { get; init; } - public bool IsAdmin { get; init; } - public string? MemberTitle { get; init; } - public bool ListPrivate { get; init; } - public string? Tumblr { get; init; } - public string? TumblrUsername { get; init; } - public string? Google { get; init; } - public string? GoogleUsername { get; init; } - public Dictionary CustomPreferences { get; init; } = []; - public DateTimeOffset LastActive { get; init; } - public required string Sid { get; init; } - public DateTimeOffset LastSidReroll { get; init; } - public string? Timezone { get; init; } - public Snowflake SnowflakeId { get; init; } -} - -public class GoUserField -{ - public required string UserId { get; init; } - public required long Id { get; init; } - public required string Name { get; init; } - public required GoFieldEntry[] Entries { get; init; } -} - -public class GoPrideFlag -{ - public required string Id { get; init; } - public required string UserId { get; init; } - public required string Hash { get; init; } - public required string Name { get; init; } - public string? Description { get; init; } - public required Snowflake SnowflakeId { get; init; } -} - -public class GoProfileFlag -{ - public string? UserId { get; init; } - public string? MemberId { get; init; } - public required long Id { get; init; } - public required string FlagId { get; init; } -} - -public class GoFediverseApp -{ - public required int Id { get; init; } - public required string Instance { get; init; } - public required string ClientId { get; init; } - public required string ClientSecret { get; init; } - public required string InstanceType { get; init; } - - public FediverseInstanceType TypeToEnum() => - InstanceType switch - { - "sharkey" => FediverseInstanceType.MisskeyApi, - "firefish" => FediverseInstanceType.MisskeyApi, - "pixelfed" => FediverseInstanceType.MastodonApi, - "mastodon" => FediverseInstanceType.MastodonApi, - "pleroma" => FediverseInstanceType.MastodonApi, - "akkoma" => FediverseInstanceType.MastodonApi, - "misskey" => FediverseInstanceType.MastodonApi, - "gotosocial" => FediverseInstanceType.MastodonApi, - "calckey" => FediverseInstanceType.MisskeyApi, - "foundkey" => FediverseInstanceType.MisskeyApi, - // this should never happen but if it does we just fall back to the mastodon api - // basically everything but misskey forks implement it anyway :3 - _ => FediverseInstanceType.MastodonApi, - }; -} - -public record GoFieldEntry(string Value, string Status); - -public record GoPronounEntry(string Pronouns, string? DisplayText, string Status); - -public record GoCustomPreference( - string Icon, - string Tooltip, - string PreferenceSize, - bool Muted, - bool Favourite -); diff --git a/Foxnouns.DataMigrator/Program.cs b/Foxnouns.DataMigrator/Program.cs deleted file mode 100644 index e79977b..0000000 --- a/Foxnouns.DataMigrator/Program.cs +++ /dev/null @@ -1,134 +0,0 @@ -using System.Globalization; -using Foxnouns.Backend; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; -using Foxnouns.DataMigrator.Models; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Newtonsoft.Json; -using Npgsql; -using Serilog; -using Serilog.Sinks.SystemConsole.Themes; - -namespace Foxnouns.DataMigrator; - -internal class Program -{ - public static async Task Main(string[] args) - { - // Create logger and get configuration - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen) - .CreateLogger(); - - var minUserId = new Snowflake(0); - if (args.Length > 0) - minUserId = ulong.Parse(args[0]); - - Log.Information("Starting migration from user ID {MinUserId}", minUserId); - - Config config = - new ConfigurationBuilder() - .AddConfiguration() - .Build() - // Get the configuration as our config class - .Get() ?? new Config(); - - NpgsqlConnection conn = await GoDatabase.GetConnectionAsync(); - // just reuse the design time factory so we don't have to copy this - DatabaseContext context = new DesignTimeDatabaseContextFactory().CreateDbContext(args); - - await context.Database.MigrateAsync(); - - Dictionary appIds; - if (minUserId == new Snowflake(0)) - { - Log.Information("Migrating applications"); - appIds = await MigrateAppsAsync(conn, context); - - string appJson = JsonConvert.SerializeObject(appIds); - await File.WriteAllTextAsync("apps.json", appJson); - } - else - { - Log.Information( - "Not the first migration, reading application IDs from {Filename}", - "apps.json" - ); - - string appJson = await File.ReadAllTextAsync("apps.json"); - appIds = - JsonConvert.DeserializeObject>(appJson) - ?? throw new Exception("invalid apps.json file"); - } - - Log.Information("Migrating users"); - List users = await Queries.GetUsersAsync(conn, minUserId); - List userFields = await Queries.GetUserFieldsAsync(conn); - List memberFields = await Queries.GetMemberFieldsAsync(conn); - List prideFlags = await Queries.GetUserFlagsAsync(conn); - List userFlags = await Queries.GetUserProfileFlagsAsync(conn); - List memberFlags = await Queries.GetMemberProfileFlagsAsync(conn); - Log.Information("Migrating {Count} users", users.Count); - foreach ((GoUser user, int i) in users.Select((user, i) => (user, i))) - { - Log.Debug( - "Migrating user #{Index}/{Count}: {Id}/{SnowflakeId}", - i, - users.Count, - user.Id, - user.SnowflakeId - ); - await new UserMigrator( - conn, - context, - user, - appIds, - userFields, - memberFields, - prideFlags, - userFlags, - memberFlags - ).MigrateAsync(); - } - - await context.SaveChangesAsync(); - Log.Information("Migration complete!"); - Log.Information( - "Migrated {Count} users, last user was {UserId}. Complete? {Complete}", - users.Count, - users.Last().SnowflakeId, - users.Count != 1000 - ); - } - - private static async Task> MigrateAppsAsync( - NpgsqlConnection conn, - DatabaseContext context - ) - { - List goApps = await Queries.GetFediverseAppsAsync(conn); - var appIds = new Dictionary(); - foreach (GoFediverseApp app in goApps) - { - Log.Debug("Migrating application for {Domain}", app.Instance); - Snowflake id = SnowflakeGenerator.Instance.GenerateSnowflake(); - appIds[app.Id] = id; - context.FediverseApplications.Add( - new FediverseApplication - { - Id = id, - Domain = app.Instance.ToLower(CultureInfo.InvariantCulture), - ClientId = app.ClientId, - ClientSecret = app.ClientSecret, - InstanceType = app.TypeToEnum(), - ForceRefresh = true, - } - ); - } - - return appIds; - } -} diff --git a/Foxnouns.DataMigrator/Queries.cs b/Foxnouns.DataMigrator/Queries.cs deleted file mode 100644 index 0bc14a2..0000000 --- a/Foxnouns.DataMigrator/Queries.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Coravel.Mailer.Mail.Helpers; -using Dapper; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.DataMigrator.Models; -using NodaTime.Extensions; -using Npgsql; - -namespace Foxnouns.DataMigrator; - -public static class Queries -{ - public static async Task> GetFediverseAppsAsync(NpgsqlConnection conn) => - (await conn.QueryAsync("select * from fediverse_apps")).ToList(); - - public static async Task> GetUsersAsync(NpgsqlConnection conn, Snowflake minId) => - ( - await conn.QueryAsync( - "select * from users where snowflake_id > @Id order by snowflake_id limit 1000", - new { Id = minId.Value } - ) - ).ToList(); - - public static async Task> GetUserFieldsAsync(NpgsqlConnection conn) => - (await conn.QueryAsync("select * from user_fields order by id")).ToList(); - - public static async Task> GetMemberFieldsAsync(NpgsqlConnection conn) => - (await conn.QueryAsync("select * from member_fields order by id")).ToList(); - - public static async Task> GetUserProfileFlagsAsync(NpgsqlConnection conn) => - (await conn.QueryAsync("select * from user_flags order by id")).ToList(); - - public static async Task> GetMemberProfileFlagsAsync( - NpgsqlConnection conn - ) => (await conn.QueryAsync("select * from member_flags order by id")).ToList(); - - public static async Task> GetUserFlagsAsync(NpgsqlConnection conn) => - (await conn.QueryAsync("select * from pride_flags order by id")).ToList(); -} diff --git a/Foxnouns.DataMigrator/UserMigrator.cs b/Foxnouns.DataMigrator/UserMigrator.cs deleted file mode 100644 index ee46878..0000000 --- a/Foxnouns.DataMigrator/UserMigrator.cs +++ /dev/null @@ -1,265 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Dapper; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Services; -using Foxnouns.DataMigrator.Models; -using NodaTime.Extensions; -using Npgsql; -using Serilog; - -namespace Foxnouns.DataMigrator; - -public class UserMigrator( - NpgsqlConnection conn, - DatabaseContext context, - GoUser goUser, - Dictionary fediverseApplicationIds, - List userFields, - List memberFields, - List prideFlags, - List userFlags, - List memberFlags -) -{ - private readonly Dictionary _preferenceIds = new(); - private readonly Dictionary _flagIds = new(); - private User? _user; - - public async Task MigrateAsync() - { - CreateNewUser(); - MigrateFlags(); - await MigrateMembersAsync(); - } - - [MemberNotNull(nameof(_user))] - private void CreateNewUser() - { - _user = new User - { - Id = goUser.SnowflakeId, - LegacyId = goUser.Id, - Username = goUser.Username, - DisplayName = goUser.DisplayName, - Bio = goUser.Bio, - Links = goUser.Links ?? [], - - Deleted = goUser.DeletedAt != null, - DeletedAt = goUser.DeletedAt?.ToInstant(), - DeletedBy = goUser.SelfDelete == true ? null : goUser.SnowflakeId, - - Names = goUser.Names.Select(ConvertFieldEntry).ToList(), - Pronouns = goUser.Pronouns.Select(ConvertPronoun).ToList(), - Avatar = goUser.Avatar, - - Role = goUser.IsAdmin ? UserRole.Admin : UserRole.User, - MemberTitle = goUser.MemberTitle, - ListHidden = goUser.ListPrivate, - CustomPreferences = ConvertPreferences(), - LastActive = goUser.LastActive.ToInstant(), - Sid = goUser.Sid, - LastSidReroll = goUser.LastSidReroll.ToInstant(), - Timezone = goUser.Timezone, - Fields = userFields - .Where(f => f.UserId == goUser.Id) - .Select(f => new Field - { - Name = f.Name, - Entries = f.Entries.Select(ConvertFieldEntry).ToArray(), - }) - .ToList(), - }; - context.Users.Add(_user); - - // Create the user's auth methods - if (goUser.Discord != null) - { - context.AuthMethods.Add( - new AuthMethod - { - Id = SnowflakeGenerator.Instance.GenerateSnowflake(_user.Id.Time), - UserId = _user.Id, - AuthType = AuthType.Discord, - RemoteId = goUser.Discord, - RemoteUsername = goUser.DiscordUsername ?? "(unknown)", - } - ); - } - - if (goUser.Google != null) - { - context.AuthMethods.Add( - new AuthMethod - { - Id = SnowflakeGenerator.Instance.GenerateSnowflake(_user.Id.Time), - UserId = _user.Id, - AuthType = AuthType.Google, - RemoteId = goUser.Google, - RemoteUsername = goUser.GoogleUsername ?? "(unknown)", - } - ); - } - - if (goUser.Tumblr != null) - { - context.AuthMethods.Add( - new AuthMethod - { - Id = SnowflakeGenerator.Instance.GenerateSnowflake(_user.Id.Time), - UserId = _user.Id, - AuthType = AuthType.Tumblr, - RemoteId = goUser.Tumblr, - RemoteUsername = goUser.TumblrUsername ?? "(unknown)", - } - ); - } - - if (goUser.Fediverse != null) - { - context.AuthMethods.Add( - new AuthMethod - { - Id = SnowflakeGenerator.Instance.GenerateSnowflake(_user.Id.Time), - UserId = _user.Id, - AuthType = AuthType.Fediverse, - RemoteId = goUser.Fediverse, - RemoteUsername = goUser.FediverseUsername ?? "(unknown)", - FediverseApplicationId = fediverseApplicationIds[goUser.FediverseAppId!.Value], - } - ); - } - } - - private void MigrateFlags() - { - foreach (GoPrideFlag flag in prideFlags.Where(f => f.UserId == goUser.Id)) - { - _flagIds[flag.Id] = flag.SnowflakeId; - context.PrideFlags.Add( - new PrideFlag - { - Id = flag.SnowflakeId, - LegacyId = flag.Id, - UserId = _user!.Id, - Hash = flag.Hash, - Name = flag.Name, - Description = flag.Description, - } - ); - } - - context.UserFlags.AddRange( - userFlags - .Where(f => f.UserId == goUser.Id) - .Where(f => _flagIds.ContainsKey(f.FlagId)) - .Select(f => new UserFlag { UserId = _user!.Id, PrideFlagId = _flagIds[f.FlagId] }) - ); - } - - private async Task MigrateMembersAsync() - { - List members = ( - await conn.QueryAsync( - "select * from members where user_id = @Id", - new { goUser.Id } - ) - ).ToList(); - - foreach (GoMember member in members) - { - Log.Debug("Migrating member {Id}/{SnowflakeId}", member.Id, member.SnowflakeId); - MigrateMember(member); - } - } - - private void MigrateMember(GoMember goMember) - { - var names = goMember.Names.Select(ConvertFieldEntry).ToList(); - var pronouns = goMember.Pronouns.Select(ConvertPronoun).ToList(); - var fields = memberFields - .Where(f => f.MemberId == goMember.Id) - .Select(f => new Field - { - Name = f.Name, - Entries = f.Entries.Select(ConvertFieldEntry).ToArray(), - }) - .ToList(); - - var member = new Member - { - Id = goMember.SnowflakeId, - UserId = _user!.Id, - Name = goMember.Name, - Sid = goMember.Sid, - LegacyId = goMember.Id, - DisplayName = goMember.DisplayName, - Bio = goMember.Bio, - Avatar = goMember.Avatar, - Links = goMember.Links ?? [], - Unlisted = goMember.Unlisted, - Names = names, - Pronouns = pronouns, - Fields = fields, - }; - context.Members.Add(member); - - context.MemberFlags.AddRange( - memberFlags - .Where(f => f.MemberId == goMember.Id) - .Select(f => new MemberFlag - { - MemberId = member.Id, - PrideFlagId = _flagIds[f.FlagId], - }) - ); - } - - private Dictionary ConvertPreferences() - { - var prefs = new Dictionary(); - - foreach ((string id, GoCustomPreference goPref) in goUser.CustomPreferences) - { - Snowflake newId = SnowflakeGenerator.Instance.GenerateSnowflake( - goUser.SnowflakeId.Time - ); - _preferenceIds[id] = newId; - prefs[newId] = new User.CustomPreference - { - Icon = goPref.Icon, - Tooltip = goPref.Tooltip, - Muted = goPref.Muted, - Favourite = goPref.Favourite, - Size = goPref.PreferenceSize switch - { - "large" => PreferenceSize.Large, - "normal" => PreferenceSize.Normal, - "small" => PreferenceSize.Small, - _ => PreferenceSize.Normal, - }, - LegacyId = new Guid(id), - }; - } - - return prefs; - } - - private FieldEntry ConvertFieldEntry(GoFieldEntry entry) => - new() { Value = entry.Value, Status = ConvertPreferenceId(entry.Status) }; - - private Pronoun ConvertPronoun(GoPronounEntry entry) => - new() - { - Value = entry.Pronouns, - DisplayText = entry.DisplayText, - Status = ConvertPreferenceId(entry.Status), - }; - - private string ConvertPreferenceId(string id) - { - if (_preferenceIds.TryGetValue(id, out Snowflake preferenceId)) - return preferenceId.ToString(); - return ValidationService.DefaultStatusOptions.Contains(id) ? id : "okay"; - } -} diff --git a/Foxnouns.Frontend/.dockerignore b/Foxnouns.Frontend/.dockerignore deleted file mode 100644 index 14ad623..0000000 --- a/Foxnouns.Frontend/.dockerignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/ -build/ -.env -.env* diff --git a/Foxnouns.Frontend/.env.example b/Foxnouns.Frontend/.env.example deleted file mode 100644 index 99c47b3..0000000 --- a/Foxnouns.Frontend/.env.example +++ /dev/null @@ -1,18 +0,0 @@ -# Example .env file--DO NOT EDIT, copy to .env or .env.local then edit - -# The language the frontend will use. Valid languages are listed in src/lib/i18n/index.ts. -PUBLIC_LANGUAGE=en -# The public base URL, i.e. the one users will see. Used for building links. -PUBLIC_BASE_URL=https://pronouns.cc -# The base URL for the URL shortener service. Used for building short links. -PUBLIC_SHORT_URL=https://prns.cc -# The base public URL for the API. This is (almost) always the public base URL + /api. -PUBLIC_API_BASE=https://pronouns.cc/api -# The base *private* URL for the API's rate limiter proxy. The frontend will rewrite API URLs to use this. -# In development, you can set this to the same value as $PRIVATE_INTERNAL_API_HOST, but be aware that this will disable rate limiting. -PRIVATE_API_HOST=http://localhost:5003/api -# The base private URL for the API, which bypasses the rate limiter. Used for /api/internal paths and unauthenticated GET requests. -PRIVATE_INTERNAL_API_HOST=http://localhost:6000/api - -# The Sentry URL to use. Optional. -PRIVATE_SENTRY_DSN=https://examplePublicKey@o0.ingest.sentry.io/0 diff --git a/Foxnouns.Frontend/.gitignore b/Foxnouns.Frontend/.gitignore deleted file mode 100644 index 79518f7..0000000 --- a/Foxnouns.Frontend/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -node_modules - -# Output -.output -.vercel -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* diff --git a/Foxnouns.Frontend/.npmrc b/Foxnouns.Frontend/.npmrc deleted file mode 100644 index b6f27f1..0000000 --- a/Foxnouns.Frontend/.npmrc +++ /dev/null @@ -1 +0,0 @@ -engine-strict=true diff --git a/Foxnouns.Frontend/.prettierignore b/Foxnouns.Frontend/.prettierignore deleted file mode 100644 index 11ea406..0000000 --- a/Foxnouns.Frontend/.prettierignore +++ /dev/null @@ -1,5 +0,0 @@ -# Package Managers -package-lock.json -pnpm-lock.yaml -yarn.lock -src/lib/icons.ts diff --git a/Foxnouns.Frontend/.prettierrc b/Foxnouns.Frontend/.prettierrc deleted file mode 100644 index f166279..0000000 --- a/Foxnouns.Frontend/.prettierrc +++ /dev/null @@ -1,13 +0,0 @@ -{ - "useTabs": true, - "printWidth": 100, - "plugins": ["prettier-plugin-svelte"], - "overrides": [ - { - "files": "*.svelte", - "options": { - "parser": "svelte" - } - } - ] -} diff --git a/Foxnouns.Frontend/.vscode/settings.json b/Foxnouns.Frontend/.vscode/settings.json deleted file mode 100644 index c5d12a5..0000000 --- a/Foxnouns.Frontend/.vscode/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "editor.formatOnSave": true, - "editor.formatOnSaveMode": "modificationsIfAvailable", - "i18n-ally.localesPaths": ["src/lib/i18n", "src/lib/i18n/locales"], - "i18n-ally.keystyle": "nested", - "explorer.sortOrder": "filesFirst", - "explorer.compactFolders": false, - "eslint.validate": ["javascript", "javascriptreact", "svelte"] -} diff --git a/Foxnouns.Frontend/Dockerfile b/Foxnouns.Frontend/Dockerfile deleted file mode 100644 index 2a86593..0000000 --- a/Foxnouns.Frontend/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM docker.io/node:23-slim - -COPY ./Foxnouns.Frontend /app -COPY ./docker/frontend.env /app/.env.local -WORKDIR /app - -ENV PRIVATE_API_HOST=http://rate:5003/api -ENV PRIVATE_INTERNAL_API_HOST=http://backend:5000/api - -RUN npm ci -RUN npm run build - -CMD ["node", "build/index.js"] diff --git a/Foxnouns.Frontend/README.md b/Foxnouns.Frontend/README.md deleted file mode 100644 index b5b2950..0000000 --- a/Foxnouns.Frontend/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# sv - -Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). - -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npx sv create - -# create a new project in my-app -npx sv create my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/Foxnouns.Frontend/eslint.config.js b/Foxnouns.Frontend/eslint.config.js deleted file mode 100644 index c1d1e14..0000000 --- a/Foxnouns.Frontend/eslint.config.js +++ /dev/null @@ -1,45 +0,0 @@ -import prettier from "eslint-config-prettier"; -import js from "@eslint/js"; -import svelte from "eslint-plugin-svelte"; -import globals from "globals"; -import ts from "typescript-eslint"; - -export default ts.config( - js.configs.recommended, - ...ts.configs.recommended, - ...svelte.configs["flat/recommended"], - prettier, - ...svelte.configs["flat/prettier"], - { - languageOptions: { - globals: { - ...globals.browser, - ...globals.node, - }, - }, - }, - { - files: ["**/*.svelte"], - - languageOptions: { - parserOptions: { - parser: ts.parser, - }, - }, - }, - { - ignores: ["build/", ".svelte-kit/", "dist/"], - }, - { - rules: { - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_", - caughtErrorsIgnorePattern: "^_", - }, - ], - }, - }, -); diff --git a/Foxnouns.Frontend/icons.js b/Foxnouns.Frontend/icons.js deleted file mode 100644 index 0c9ffc4..0000000 --- a/Foxnouns.Frontend/icons.js +++ /dev/null @@ -1,42 +0,0 @@ -// This script regenerates the list of icons for the frontend (Foxnouns.Frontend/src/lib/icons.ts) -// and the backend (Foxnouns.Backend/Utils/BootstrapIcons.Icons.cs) from the currently installed version of Bootstrap Icons. -// Run with `node icons.js` in the frontend directory. - -import { writeFileSync } from "fs"; -import icons from "bootstrap-icons/font/bootstrap-icons.json" with { type: "json" }; - -const keys = Object.keys(icons); - -console.log(`Found ${keys.length} icons`); -const output = JSON.stringify(keys); -console.log(`Saving file as src/icons.ts`); - -writeFileSync( - "src/lib/icons.ts", - `// Generated code: DO NOT EDIT\n\nconst icons = ${output};\nexport default icons;`, -); - -const csCode1 = `// - -namespace Foxnouns.Backend.Utils; - -public static partial class BootstrapIcons -{ - private static readonly HashSet Icons = - [ -`; - -const csCode2 = ` ]; -} -`; - -let csOutput = csCode1; - -keys.forEach((element) => { - csOutput += ` "${element}",\n`; -}); - -csOutput += csCode2; - -console.log("Writing C# code"); -writeFileSync("../Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs", csOutput); diff --git a/Foxnouns.Frontend/package-lock.json b/Foxnouns.Frontend/package-lock.json deleted file mode 100644 index ac5f6f1..0000000 --- a/Foxnouns.Frontend/package-lock.json +++ /dev/null @@ -1,6354 +0,0 @@ -{ - "name": "foxnouns.frontend", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "foxnouns.frontend", - "version": "0.0.1", - "dependencies": { - "@fontsource/firago": "^5.2.5", - "@sentry/sveltekit": "^9.11.0", - "base64-arraybuffer": "^1.0.2", - "bootstrap-icons": "^1.11.3", - "luxon": "^3.6.1", - "markdown-it": "^14.1.0", - "minidenticons": "^4.2.1", - "pretty-bytes": "^6.1.1", - "sanitize-html": "^2.15.0", - "svelte-tippy": "^1.3.2", - "tippy.js": "^6.3.7", - "tslog": "^4.9.3" - }, - "devDependencies": { - "@sveltejs/adapter-node": "^5.2.12", - "@sveltejs/kit": "^2.20.4", - "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@sveltestrap/sveltestrap": "^7.1.0", - "@types/eslint": "^9.6.1", - "@types/luxon": "^3.6.2", - "@types/markdown-it": "^14.1.2", - "@types/sanitize-html": "^2.15.0", - "bootstrap": "^5.3.5", - "dotenv": "^16.4.7", - "eslint": "^9.24.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.46.1", - "globals": "^16.0.0", - "prettier": "^3.5.3", - "prettier-plugin-svelte": "^3.3.3", - "sass": "^1.86.3", - "svelte": "^5.25.7", - "svelte-bootstrap-icons": "^3.1.2", - "svelte-check": "^4.1.5", - "svelte-easy-crop": "^4.0.1", - "sveltekit-i18n": "^2.4.2", - "typescript": "^5.8.3", - "typescript-eslint": "^8.29.0", - "vite": "^6.2.5" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", - "integrity": "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", - "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", - "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", - "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.26.9" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", - "integrity": "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/parser": "^7.27.0", - "@babel/types": "^7.27.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/types": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.0.tgz", - "integrity": "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", - "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.6", - "debug": "^4.3.1", - "minimatch": "^3.1.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.1.tgz", - "integrity": "sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", - "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.24.0.tgz", - "integrity": "sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", - "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.13.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", - "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@fontsource/firago": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/@fontsource/firago/-/firago-5.2.5.tgz", - "integrity": "sha512-11vSR9Vyh0Tp/ChtheVSsK3yP9UGUUV5xJCdSOE8xNsQH/NZIGF36p0aeFTQ6uzBBaxaVjCGm0LEIFmxAwJoRw==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.57.2.tgz", - "integrity": "sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/context-async-hooks": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.30.1.tgz", - "integrity": "sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", - "integrity": "sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.28.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/core/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/instrumentation": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.57.2.tgz", - "integrity": "sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api-logs": "0.57.2", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.46.1.tgz", - "integrity": "sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.43.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.43.1.tgz", - "integrity": "sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/connect": "3.4.38" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.16.1.tgz", - "integrity": "sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.47.1.tgz", - "integrity": "sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fastify": { - "version": "0.44.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.44.2.tgz", - "integrity": "sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.19.1.tgz", - "integrity": "sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.43.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.43.1.tgz", - "integrity": "sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.47.1.tgz", - "integrity": "sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.45.2.tgz", - "integrity": "sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.57.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.57.2.tgz", - "integrity": "sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/instrumentation": "0.57.2", - "@opentelemetry/semantic-conventions": "1.28.0", - "forwarded-parse": "2.1.2", - "semver": "^7.5.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.47.1.tgz", - "integrity": "sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.7.1.tgz", - "integrity": "sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.44.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.44.1.tgz", - "integrity": "sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.47.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.47.1.tgz", - "integrity": "sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.44.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.44.1.tgz", - "integrity": "sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.52.0.tgz", - "integrity": "sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.46.1.tgz", - "integrity": "sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.45.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.45.1.tgz", - "integrity": "sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/mysql": "2.15.26" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.45.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.45.2.tgz", - "integrity": "sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@opentelemetry/sql-common": "^0.40.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.51.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.51.1.tgz", - "integrity": "sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.26.0", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@opentelemetry/sql-common": "^0.40.1", - "@types/pg": "8.6.1", - "@types/pg-pool": "2.0.6" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.46.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.46.1.tgz", - "integrity": "sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.18.1.tgz", - "integrity": "sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/semantic-conventions": "^1.27.0", - "@types/tedious": "^4.0.14" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.10.1.tgz", - "integrity": "sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.57.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.7.0" - } - }, - "node_modules/@opentelemetry/redis-common": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", - "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/resources": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.30.1.tgz", - "integrity": "sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.30.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.30.1.tgz", - "integrity": "sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "1.30.1", - "@opentelemetry/resources": "1.30.1", - "@opentelemetry/semantic-conventions": "1.28.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.28.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.28.0.tgz", - "integrity": "sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.32.0.tgz", - "integrity": "sha512-s0OpmpQFSfMrmedAn9Lhg4KWJELHCU6uU9dtIJ28N8UGhf9Y55im5X8fEzwhwDwiSqN+ZPSNrDJF7ivf/AuRPQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sql-common": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", - "integrity": "sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/core": "^1.1.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0" - } - }, - "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^1.0.3", - "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "license": "MIT" - }, - "node_modules/@popperjs/core": { - "version": "2.11.8", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, - "node_modules/@prisma/instrumentation": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-6.5.0.tgz", - "integrity": "sha512-morJDtFRoAp5d/KENEm+K6Y3PQcn5bCvpJ5a9y3V3DNMrNy/ZSn2zulPGj+ld+Xj2UYVoaMJ8DpBX/o6iF6OiA==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0 || ^0.53.0 || ^0.54.0 || ^0.55.0 || ^0.56.0 || ^0.57.0" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.8" - } - }, - "node_modules/@rollup/plugin-commonjs": { - "version": "28.0.3", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.3.tgz", - "integrity": "sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "commondir": "^1.0.1", - "estree-walker": "^2.0.2", - "fdir": "^6.2.0", - "is-reference": "1.2.1", - "magic-string": "^0.30.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=16.0.0 || 14 >= 14.17" - }, - "peerDependencies": { - "rollup": "^2.68.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-json": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", - "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.1.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.1.tgz", - "integrity": "sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz", - "integrity": "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz", - "integrity": "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz", - "integrity": "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz", - "integrity": "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz", - "integrity": "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz", - "integrity": "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz", - "integrity": "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz", - "integrity": "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz", - "integrity": "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz", - "integrity": "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz", - "integrity": "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz", - "integrity": "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz", - "integrity": "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz", - "integrity": "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz", - "integrity": "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz", - "integrity": "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz", - "integrity": "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz", - "integrity": "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz", - "integrity": "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz", - "integrity": "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sentry-internal/browser-utils": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-9.13.0.tgz", - "integrity": "sha512-uZcbwcGI49oPC/YDEConJ+3xi2mu0TsVsDiMQKb6JoSc33KH37wq2IwXJb9nakzKJXxyMNemb44r8irAswjItw==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.13.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/feedback": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-9.13.0.tgz", - "integrity": "sha512-fOhMnhEbOR5QVPtn5Gc5+UKQHjvAN/LmtYE6Qya3w2FDh3ZlnIXNFJWqwOneuICV3kCWjN4lLckwmzzwychr7A==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.13.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/replay": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-9.13.0.tgz", - "integrity": "sha512-l+Atwab/bqI1N8+PSG1WWTCVmiOl7swL85Z9ntwS39QBnd66CTyzt/+j/n/UbAs8GienJK6FIfX1dvG1WmvUhA==", - "license": "MIT", - "dependencies": { - "@sentry-internal/browser-utils": "9.13.0", - "@sentry/core": "9.13.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry-internal/replay-canvas": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-9.13.0.tgz", - "integrity": "sha512-5muW2BmEfWP1fpVWDNcIsph/WgqOqpHaXC1QMr4hk8/BWgt1/S2KPy85YiGVtM5lJJr0VhASKK8rBXG+9zm9IQ==", - "license": "MIT", - "dependencies": { - "@sentry-internal/replay": "9.13.0", - "@sentry/core": "9.13.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/babel-plugin-component-annotate": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.4.tgz", - "integrity": "sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/@sentry/browser": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-9.13.0.tgz", - "integrity": "sha512-KiC8s9/6HvdlfCRqA420YbiBiXMBif7GYESJ8VQqOKUmlPczn8V2CRrEZjMqxhlHdIGiR0PS6jb2VSgeJBchJQ==", - "license": "MIT", - "dependencies": { - "@sentry-internal/browser-utils": "9.13.0", - "@sentry-internal/feedback": "9.13.0", - "@sentry-internal/replay": "9.13.0", - "@sentry-internal/replay-canvas": "9.13.0", - "@sentry/core": "9.13.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/bundler-plugin-core": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.2.4.tgz", - "integrity": "sha512-YMj9XW5W2JA89EeweE7CPKLDz245LBsI1JhCmqpt/bjSvmsSIAAPsLYnvIJBS3LQFm0OhtG8NB54PTi96dAcMA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.18.5", - "@sentry/babel-plugin-component-annotate": "3.2.4", - "@sentry/cli": "2.42.2", - "dotenv": "^16.3.1", - "find-up": "^5.0.0", - "glob": "^9.3.2", - "magic-string": "0.30.8", - "unplugin": "1.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@sentry/bundler-plugin-core/node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@sentry/cli": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.42.2.tgz", - "integrity": "sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.7", - "progress": "^2.0.3", - "proxy-from-env": "^1.1.0", - "which": "^2.0.2" - }, - "bin": { - "sentry-cli": "bin/sentry-cli" - }, - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@sentry/cli-darwin": "2.42.2", - "@sentry/cli-linux-arm": "2.42.2", - "@sentry/cli-linux-arm64": "2.42.2", - "@sentry/cli-linux-i686": "2.42.2", - "@sentry/cli-linux-x64": "2.42.2", - "@sentry/cli-win32-i686": "2.42.2", - "@sentry/cli-win32-x64": "2.42.2" - } - }, - "node_modules/@sentry/cli-darwin": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz", - "integrity": "sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==", - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.42.2.tgz", - "integrity": "sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==", - "cpu": [ - "arm" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-arm64": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.42.2.tgz", - "integrity": "sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==", - "cpu": [ - "arm64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-i686": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.42.2.tgz", - "integrity": "sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-linux-x64": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.42.2.tgz", - "integrity": "sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==", - "cpu": [ - "x64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "linux", - "freebsd" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-i686": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.42.2.tgz", - "integrity": "sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==", - "cpu": [ - "x86", - "ia32" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cli-win32-x64": { - "version": "2.42.2", - "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.42.2.tgz", - "integrity": "sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==", - "cpu": [ - "x64" - ], - "license": "BSD-3-Clause", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/@sentry/cloudflare": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry/cloudflare/-/cloudflare-9.13.0.tgz", - "integrity": "sha512-XaG/Kl5dSUJtzYkalQjaejGhrgFoj5w3cSWoXkxd8J+LXHsq7BFg4S0uCkzGJUmDHItlzfEY8BIaPpgPTJL7MQ==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.13.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.x" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/@sentry/core": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-9.13.0.tgz", - "integrity": "sha512-Zn1Qec5XNkNRE/M5QjL6YJLghETg6P188G/v2OzdHdHIRf0Y58/SnJilu3louF+ogos6kaSqqdMgzqKgZ8tCdg==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/node": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-9.13.0.tgz", - "integrity": "sha512-75UVkrED5b0BaazNQKCmF8NqeqjErxildPojDyC037JN+cVFMPr/kFFGGm7E+eCvA/j2pAPUzqifHp/PjykPcw==", - "license": "MIT", - "dependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1", - "@opentelemetry/core": "^1.30.1", - "@opentelemetry/instrumentation": "^0.57.2", - "@opentelemetry/instrumentation-amqplib": "^0.46.1", - "@opentelemetry/instrumentation-connect": "0.43.1", - "@opentelemetry/instrumentation-dataloader": "0.16.1", - "@opentelemetry/instrumentation-express": "0.47.1", - "@opentelemetry/instrumentation-fastify": "0.44.2", - "@opentelemetry/instrumentation-fs": "0.19.1", - "@opentelemetry/instrumentation-generic-pool": "0.43.1", - "@opentelemetry/instrumentation-graphql": "0.47.1", - "@opentelemetry/instrumentation-hapi": "0.45.2", - "@opentelemetry/instrumentation-http": "0.57.2", - "@opentelemetry/instrumentation-ioredis": "0.47.1", - "@opentelemetry/instrumentation-kafkajs": "0.7.1", - "@opentelemetry/instrumentation-knex": "0.44.1", - "@opentelemetry/instrumentation-koa": "0.47.1", - "@opentelemetry/instrumentation-lru-memoizer": "0.44.1", - "@opentelemetry/instrumentation-mongodb": "0.52.0", - "@opentelemetry/instrumentation-mongoose": "0.46.1", - "@opentelemetry/instrumentation-mysql": "0.45.1", - "@opentelemetry/instrumentation-mysql2": "0.45.2", - "@opentelemetry/instrumentation-pg": "0.51.1", - "@opentelemetry/instrumentation-redis-4": "0.46.1", - "@opentelemetry/instrumentation-tedious": "0.18.1", - "@opentelemetry/instrumentation-undici": "0.10.1", - "@opentelemetry/resources": "^1.30.1", - "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.30.0", - "@prisma/instrumentation": "6.5.0", - "@sentry/core": "9.13.0", - "@sentry/opentelemetry": "9.13.0", - "import-in-the-middle": "^1.13.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/opentelemetry": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-9.13.0.tgz", - "integrity": "sha512-TLSP0n+sXKVcVkAM2ttVmXcAT2K3e9D5gdPfr6aCnW+KIGJuD7wzla/TIcTWFaVwUejbvXAB6IFpZ/qA8HFwyA==", - "license": "MIT", - "dependencies": { - "@sentry/core": "9.13.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.9.0", - "@opentelemetry/context-async-hooks": "^1.30.1", - "@opentelemetry/core": "^1.30.1", - "@opentelemetry/instrumentation": "^0.57.1", - "@opentelemetry/sdk-trace-base": "^1.30.1", - "@opentelemetry/semantic-conventions": "^1.28.0" - } - }, - "node_modules/@sentry/svelte": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry/svelte/-/svelte-9.13.0.tgz", - "integrity": "sha512-Sy8YOlIA0x4yhW4WM5ra2aarzKKrLgFTqkY6gAG3XrJ3DNFURrTDiaHUwQCICkLf2+zJKpuw6sfovBWqo1Z+DQ==", - "license": "MIT", - "dependencies": { - "@sentry/browser": "9.13.0", - "@sentry/core": "9.13.0", - "magic-string": "^0.30.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "svelte": "3.x || 4.x || 5.x" - } - }, - "node_modules/@sentry/sveltekit": { - "version": "9.13.0", - "resolved": "https://registry.npmjs.org/@sentry/sveltekit/-/sveltekit-9.13.0.tgz", - "integrity": "sha512-U+uDKxAB+bI7nIiz/SfqPpoQMnenARXj0E3+z916bkgfdEyZUODkaQvHmmEKEOX7VRcRUT53mD/c1LwbxhSWxw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "7.26.9", - "@sentry/cloudflare": "9.13.0", - "@sentry/core": "9.13.0", - "@sentry/node": "9.13.0", - "@sentry/opentelemetry": "9.13.0", - "@sentry/svelte": "9.13.0", - "@sentry/vite-plugin": "3.2.4", - "magic-string": "0.30.7", - "recast": "0.23.11", - "sorcery": "1.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@sveltejs/kit": "2.x", - "vite": "*" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/@sentry/vite-plugin": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@sentry/vite-plugin/-/vite-plugin-3.2.4.tgz", - "integrity": "sha512-ZRn5TLlq5xtwKOqaWP+XqS1PYVfbBCgsbMk7wW2Ly6EgF9wYePvtLqKgYnE3hwPg2LpBnRPR2ti1ohlUkR+wXA==", - "license": "MIT", - "dependencies": { - "@sentry/bundler-plugin-core": "3.2.4", - "unplugin": "1.0.1" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz", - "integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8.9.0" - } - }, - "node_modules/@sveltejs/adapter-node": { - "version": "5.2.12", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz", - "integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rollup/plugin-commonjs": "^28.0.1", - "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "rollup": "^4.9.5" - }, - "peerDependencies": { - "@sveltejs/kit": "^2.4.0" - } - }, - "node_modules/@sveltejs/kit": { - "version": "2.20.7", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.7.tgz", - "integrity": "sha512-dVbLMubpJJSLI4OYB+yWYNHGAhgc2bVevWuBjDj8jFUXIJOAnLwYP3vsmtcgoxNGUXoq0rHS5f7MFCsryb6nzg==", - "license": "MIT", - "dependencies": { - "@types/cookie": "^0.6.0", - "cookie": "^0.6.0", - "devalue": "^5.1.0", - "esm-env": "^1.2.2", - "import-meta-resolve": "^4.1.0", - "kleur": "^4.1.5", - "magic-string": "^0.30.5", - "mrmime": "^2.0.0", - "sade": "^1.8.1", - "set-cookie-parser": "^2.6.0", - "sirv": "^3.0.0" - }, - "bin": { - "svelte-kit": "svelte-kit.js" - }, - "engines": { - "node": ">=18.13" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0", - "svelte": "^4.0.0 || ^5.0.0-next.0", - "vite": "^5.0.3 || ^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.0.3.tgz", - "integrity": "sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==", - "license": "MIT", - "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.0", - "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.15", - "vitefu": "^1.0.4" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.7" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" - }, - "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "svelte": "^5.0.0", - "vite": "^6.0.0" - } - }, - "node_modules/@sveltejs/vite-plugin-svelte/node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/@sveltekit-i18n/base": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/@sveltekit-i18n/base/-/base-1.3.7.tgz", - "integrity": "sha512-kg1kql1/ro/lIudwFiWrv949Q07gmweln87tflUZR51MNdXXzK4fiJQv5Mw50K/CdQ5BOk/dJ0WOH2vOtBI6yw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "svelte": ">=3.49.0" - } - }, - "node_modules/@sveltekit-i18n/parser-default": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@sveltekit-i18n/parser-default/-/parser-default-1.1.1.tgz", - "integrity": "sha512-/gtzLlqm/sox7EoPKD56BxGZktK/syGc79EbJAPWY5KVitQD9SM0TP8yJCqDxTVPk7Lk0WJhrBGUE2Nn0f5M1w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@sveltestrap/sveltestrap": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@sveltestrap/sveltestrap/-/sveltestrap-7.1.0.tgz", - "integrity": "sha512-TpIx25kqLV+z+VD3yfqYayOI1IaCeWFbT0uqM6NfA4vQgDs9PjFwmjkU4YEAlV/ngs9e7xPmaRWE7lkrg4Miow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@popperjs/core": "^2.11.8" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0 || ^5.0.0-next.0" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "license": "MIT" - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/luxon": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.6.2.tgz", - "integrity": "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mysql": { - "version": "2.15.26", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", - "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@types/pg": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", - "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/pg-pool": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", - "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", - "license": "MIT", - "dependencies": { - "@types/pg": "*" - } - }, - "node_modules/@types/resolve": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", - "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sanitize-html": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/@types/sanitize-html/-/sanitize-html-2.15.0.tgz", - "integrity": "sha512-71Z6PbYsVKfp4i6Jvr37s5ql6if1Q/iJQT80NbaSi7uGaG8CqBMXP0pk/EsURAOuGdk5IJCd/vnzKrR7S3Txsw==", - "dev": true, - "license": "MIT", - "dependencies": { - "htmlparser2": "^8.0.0" - } - }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", - "license": "MIT" - }, - "node_modules/@types/tedious": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz", - "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.30.1.tgz", - "integrity": "sha512-v+VWphxMjn+1t48/jO4t950D6KR8JaJuNXzi33Ve6P8sEmPr5k6CEXjdGwT6+LodVnEa91EQCtwjWNUCPweo+Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/type-utils": "8.30.1", - "@typescript-eslint/utils": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.30.1.tgz", - "integrity": "sha512-H+vqmWwT5xoNrXqWs/fesmssOW70gxFlgcMlYcBaWNPIEWDgLa4W9nkSPmhuOgLnXq9QYgkZ31fhDyLhleCsAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.30.1.tgz", - "integrity": "sha512-+C0B6ChFXZkuaNDl73FJxRYT0G7ufVPOSQkqkpM/U198wUwUFOtgo1k/QzFh1KjpBitaK7R1tgjVz6o9HmsRPg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.30.1.tgz", - "integrity": "sha512-64uBF76bfQiJyHgZISC7vcNz3adqQKIccVoKubyQcOnNcdJBvYOILV1v22Qhsw3tw3VQu5ll8ND6hycgAR5fEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "8.30.1", - "@typescript-eslint/utils": "8.30.1", - "debug": "^4.3.4", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.30.1.tgz", - "integrity": "sha512-81KawPfkuulyWo5QdyG/LOKbspyyiW+p4vpn4bYO7DM/hZImlVnFwrpCTnmNMOt8CvLRr5ojI9nU1Ekpw4RcEw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.30.1.tgz", - "integrity": "sha512-kQQnxymiUy9tTb1F2uep9W6aBiYODgq5EMSk6Nxh4Z+BDUoYUSa029ISs5zTzKBFnexQEh71KqwjKnRz58lusQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/visitor-keys": "8.30.1", - "debug": "^4.3.4", - "fast-glob": "^3.3.2", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^2.0.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.30.1.tgz", - "integrity": "sha512-T/8q4R9En2tcEsWPQgB5BQ0XJVOtfARcUvOa8yJP3fh9M/mXraLxZrkCfGb6ChrO/V3W+Xbd04RacUEqk1CFEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.30.1", - "@typescript-eslint/types": "8.30.1", - "@typescript-eslint/typescript-estree": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.30.1.tgz", - "integrity": "sha512-aEhgas7aJ6vZnNFC7K4/vMGDGyOiqWcYZPpIWrTKuTAlsvDNKy2GFDqh9smL+iq069ZvR0YzEeq0B8NJlLzjFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.30.1", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ast-types": { - "version": "0.16.1", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", - "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/axobject-query": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", - "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/base64-arraybuffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/bootstrap": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz", - "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT", - "peerDependencies": { - "@popperjs/core": "^2.11.8" - } - }, - "node_modules/bootstrap-icons": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", - "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/twbs" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/bootstrap" - } - ], - "license": "MIT" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001714", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001714.tgz", - "integrity": "sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cjs-module-lexer": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", - "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", - "license": "MIT" - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true, - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/devalue": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz", - "integrity": "sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==", - "license": "MIT" - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.137", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", - "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==", - "license": "ISC" - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.24.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.24.0.tgz", - "integrity": "sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.20.0", - "@eslint/config-helpers": "^0.2.0", - "@eslint/core": "^0.12.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.24.0", - "@eslint/plugin-kit": "^0.2.7", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.3.0", - "eslint-visitor-keys": "^4.2.0", - "espree": "^10.3.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-compat-utils": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", - "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.4" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "eslint": ">=6.0.0" - } - }, - "node_modules/eslint-config-prettier": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", - "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-svelte": { - "version": "2.46.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.46.1.tgz", - "integrity": "sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@jridgewell/sourcemap-codec": "^1.4.15", - "eslint-compat-utils": "^0.5.1", - "esutils": "^2.0.3", - "known-css-properties": "^0.35.0", - "postcss": "^8.4.38", - "postcss-load-config": "^3.1.4", - "postcss-safe-parser": "^6.0.0", - "postcss-selector-parser": "^6.1.0", - "semver": "^7.6.2", - "svelte-eslint-parser": "^0.43.0" - }, - "engines": { - "node": "^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", - "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", - "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esm-env": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", - "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", - "license": "MIT" - }, - "node_modules/espree": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", - "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.14.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrap": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz", - "integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fdir": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz", - "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==", - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC" - }, - "node_modules/forwarded-parse": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", - "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==", - "license": "MIT" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/glob": { - "version": "9.3.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", - "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "minimatch": "^8.0.2", - "minipass": "^4.2.4", - "path-scurry": "^1.6.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", - "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/globals": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz", - "integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "license": "MIT" - }, - "node_modules/globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "license": "MIT" - }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-in-the-middle": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.13.1.tgz", - "integrity": "sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==", - "license": "Apache-2.0", - "dependencies": { - "acorn": "^8.14.0", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - }, - "node_modules/import-meta-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", - "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-module": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", - "dev": true, - "license": "MIT" - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-plain-object": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", - "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-reference": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", - "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/known-css-properties": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.35.0.tgz", - "integrity": "sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==", - "dev": true, - "license": "MIT" - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, - "node_modules/locate-character": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", - "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", - "license": "MIT" - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/luxon": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.6.1.tgz", - "integrity": "sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/minidenticons": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/minidenticons/-/minidenticons-4.2.1.tgz", - "integrity": "sha512-oWfFivA0lOx/V/bO/YIJbthB26lV8JXYvhnv9zM2hNd3fzsHTXQ6c6bWZPcvhD3nnOB+lQk/D9lF43BXixrN8g==", - "license": "MIT", - "engines": { - "node": ">=15.14.0" - } - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", - "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/module-details-from-path": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", - "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==", - "license": "MIT" - }, - "node_modules/mri": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT", - "optional": true - }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "license": "MIT", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==", - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "license": "ISC" - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz", - "integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.8", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", - "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-safe-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz", - "integrity": "sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": "^8.3.3" - } - }, - "node_modules/postcss-scss": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", - "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss-scss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.4.29" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/prettier-plugin-svelte": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz", - "integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^3.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" - } - }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/recast": { - "version": "0.23.11", - "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", - "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.16.1", - "esprima": "~4.0.0", - "source-map": "~0.6.1", - "tiny-invariant": "^1.3.3", - "tslib": "^2.0.1" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/require-in-the-middle": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-7.5.2.tgz", - "integrity": "sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==", - "license": "MIT", - "dependencies": { - "debug": "^4.3.5", - "module-details-from-path": "^1.0.3", - "resolve": "^1.22.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rollup": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.0.tgz", - "integrity": "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w==", - "license": "MIT", - "dependencies": { - "@types/estree": "1.0.7" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.40.0", - "@rollup/rollup-android-arm64": "4.40.0", - "@rollup/rollup-darwin-arm64": "4.40.0", - "@rollup/rollup-darwin-x64": "4.40.0", - "@rollup/rollup-freebsd-arm64": "4.40.0", - "@rollup/rollup-freebsd-x64": "4.40.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", - "@rollup/rollup-linux-arm-musleabihf": "4.40.0", - "@rollup/rollup-linux-arm64-gnu": "4.40.0", - "@rollup/rollup-linux-arm64-musl": "4.40.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-gnu": "4.40.0", - "@rollup/rollup-linux-riscv64-musl": "4.40.0", - "@rollup/rollup-linux-s390x-gnu": "4.40.0", - "@rollup/rollup-linux-x64-gnu": "4.40.0", - "@rollup/rollup-linux-x64-musl": "4.40.0", - "@rollup/rollup-win32-arm64-msvc": "4.40.0", - "@rollup/rollup-win32-ia32-msvc": "4.40.0", - "@rollup/rollup-win32-x64-msvc": "4.40.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/sade": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", - "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", - "license": "MIT", - "dependencies": { - "mri": "^1.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/sanitize-html": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.16.0.tgz", - "integrity": "sha512-0s4caLuHHaZFVxFTG74oW91+j6vW7gKbGD6CD2+miP73CE6z6YtOBN0ArtLd2UGyi4IC7K47v3ENUbQX4jV3Mg==", - "license": "MIT", - "dependencies": { - "deepmerge": "^4.2.2", - "escape-string-regexp": "^4.0.0", - "htmlparser2": "^8.0.0", - "is-plain-object": "^5.0.0", - "parse-srcset": "^1.0.2", - "postcss": "^8.3.11" - } - }, - "node_modules/sass": { - "version": "1.86.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.86.3.tgz", - "integrity": "sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, - "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/set-cookie-parser": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", - "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", - "license": "BSD-2-Clause" - }, - "node_modules/sirv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", - "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/sorcery": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-1.0.0.tgz", - "integrity": "sha512-5ay9oJE+7sNmhzl3YNG18jEEEf4AOQCM/FAqR5wMmzqd1FtRorFbJXn3w3SKOhbiQaVgHM+Q1lszZspjri7bpA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.14", - "minimist": "^1.2.0", - "tiny-glob": "^0.2.9" - }, - "bin": { - "sorcery": "bin/sorcery" - } - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svelte": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.27.0.tgz", - "integrity": "sha512-Uai13Ydt1ZE+bUHme6b9U38PCYVNCqBRoBMkUKbFbKiD7kHWjdUUrklYAQZJxyKK81qII4mrBwe/YmvEMSlC9w==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.5", - "@types/estree": "^1.0.5", - "acorn": "^8.12.1", - "aria-query": "^5.3.1", - "axobject-query": "^4.1.0", - "clsx": "^2.1.1", - "esm-env": "^1.2.1", - "esrap": "^1.4.6", - "is-reference": "^3.0.3", - "locate-character": "^3.0.0", - "magic-string": "^0.30.11", - "zimmerframe": "^1.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/svelte-bootstrap-icons": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/svelte-bootstrap-icons/-/svelte-bootstrap-icons-3.1.2.tgz", - "integrity": "sha512-vy+qmWFfLJZxu5BaDlmaUG4uzki1rodX5ERZAP6KQdyO/2WNeGBDU4Yke3Z0NRq+VSepK86iAy+iUJvlUdsbBg==", - "dev": true, - "license": "MIT" - }, - "node_modules/svelte-check": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.1.6.tgz", - "integrity": "sha512-P7w/6tdSfk3zEVvfsgrp3h3DFC75jCdZjTQvgGJtjPORs1n7/v2VMPIoty3PWv7jnfEm3x0G/p9wH4pecTb0Wg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "chokidar": "^4.0.1", - "fdir": "^6.2.0", - "picocolors": "^1.0.0", - "sade": "^1.7.4" - }, - "bin": { - "svelte-check": "bin/svelte-check" - }, - "engines": { - "node": ">= 18.0.0" - }, - "peerDependencies": { - "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": ">=5.0.0" - } - }, - "node_modules/svelte-easy-crop": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svelte-easy-crop/-/svelte-easy-crop-4.0.1.tgz", - "integrity": "sha512-0k7vVpHVLrPyobSXqey5IJUmFVxOoCaQrobFEsFXpSCyK8N5jTkRj1VX6NuCOZK8XXcMAqUvV0MktB8D5x1oCw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "svelte": "^5.0.0" - } - }, - "node_modules/svelte-eslint-parser": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz", - "integrity": "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "postcss": "^8.4.39", - "postcss-scss": "^4.0.9" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ota-meshi" - }, - "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "svelte": { - "optional": true - } - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-tippy": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svelte-tippy/-/svelte-tippy-1.3.2.tgz", - "integrity": "sha512-41f+85hwhKBRqX0UNYrgFsi34Kk/KDvUkIZXYANxkWoA2NTVTCZbUC2J8hRNZ4TRVxObTshoZRjK2co5+i6LMw==", - "dependencies": { - "tippy.js": "6.3.7" - } - }, - "node_modules/svelte/node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.6" - } - }, - "node_modules/svelte/node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/sveltekit-i18n": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/sveltekit-i18n/-/sveltekit-i18n-2.4.2.tgz", - "integrity": "sha512-hjRWn4V4DBL8JQKJoJa3MRvn6d32Zo+rWkoSP5bsQ/XIAguPdQUZJ8LMe6Nc1rST8WEVdu9+vZI3aFdKYGR3+Q==", - "dev": true, - "license": "MIT", - "workspaces": [ - "./examples/*/" - ], - "dependencies": { - "@sveltekit-i18n/base": "~1.3.0", - "@sveltekit-i18n/parser-default": "~1.1.0" - }, - "peerDependencies": { - "svelte": ">=3.49.0" - } - }, - "node_modules/tiny-glob": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", - "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", - "license": "MIT", - "dependencies": { - "globalyzer": "0.1.0", - "globrex": "^0.1.2" - } - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tinyglobby": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz", - "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==", - "license": "MIT", - "dependencies": { - "fdir": "^6.4.3", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tippy.js": { - "version": "6.3.7", - "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", - "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", - "license": "MIT", - "dependencies": { - "@popperjs/core": "^2.9.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT" - }, - "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tslog": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz", - "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/fullstack-build/tslog?sponsor=1" - } - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/typescript-eslint": { - "version": "8.30.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.30.1.tgz", - "integrity": "sha512-D7lC0kcehVH7Mb26MRQi64LMyRJsj3dToJxM1+JVTl53DQSV5/7oUGWQLcKl1C1KnoVHxMMU2FNQMffr7F3Row==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.30.1", - "@typescript-eslint/parser": "8.30.1", - "@typescript-eslint/utils": "8.30.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", - "typescript": ">=4.8.4 <5.9.0" - } - }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "license": "MIT" - }, - "node_modules/unplugin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.0.1.tgz", - "integrity": "sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==", - "license": "MIT", - "dependencies": { - "acorn": "^8.8.1", - "chokidar": "^3.5.3", - "webpack-sources": "^3.2.3", - "webpack-virtual-modules": "^0.5.0" - } - }, - "node_modules/unplugin/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/unplugin/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/unplugin/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/unplugin/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.1.tgz", - "integrity": "sha512-kkzzkqtMESYklo96HKKPE5KKLkC1amlsqt+RjFMlX2AvbRB/0wghap19NdBxxwGZ+h/C6DLCrcEphPIItlGrRQ==", - "license": "MIT", - "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.3", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.12" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "jiti": ">=1.21.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vitefu": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.6.tgz", - "integrity": "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==", - "license": "MIT", - "workspaces": [ - "tests/deps/*", - "tests/projects/*" - ], - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause" - }, - "node_modules/webpack-sources": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-virtual-modules": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", - "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", - "license": "MIT" - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "license": "MIT", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "license": "MIT" - } - } -} diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json deleted file mode 100644 index 507e11f..0000000 --- a/Foxnouns.Frontend/package.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "name": "foxnouns.frontend", - "version": "0.0.1", - "type": "module", - "scripts": { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - "format": "prettier --write .", - "lint": "prettier --check . && eslint ." - }, - "devDependencies": { - "@sveltejs/adapter-node": "^5.2.12", - "@sveltejs/kit": "^2.20.4", - "@sveltejs/vite-plugin-svelte": "^5.0.3", - "@sveltestrap/sveltestrap": "^7.1.0", - "@types/eslint": "^9.6.1", - "@types/luxon": "^3.6.2", - "@types/markdown-it": "^14.1.2", - "@types/sanitize-html": "^2.15.0", - "bootstrap": "^5.3.5", - "dotenv": "^16.4.7", - "eslint": "^9.24.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.46.1", - "globals": "^16.0.0", - "prettier": "^3.5.3", - "prettier-plugin-svelte": "^3.3.3", - "sass": "^1.86.3", - "svelte": "^5.25.7", - "svelte-bootstrap-icons": "^3.1.2", - "svelte-check": "^4.1.5", - "svelte-easy-crop": "^4.0.1", - "sveltekit-i18n": "^2.4.2", - "typescript": "^5.8.3", - "typescript-eslint": "^8.29.0", - "vite": "^6.2.5" - }, - "dependencies": { - "@fontsource/firago": "^5.2.5", - "@sentry/sveltekit": "^9.11.0", - "base64-arraybuffer": "^1.0.2", - "bootstrap-icons": "^1.11.3", - "luxon": "^3.6.1", - "markdown-it": "^14.1.0", - "minidenticons": "^4.2.1", - "pretty-bytes": "^6.1.1", - "sanitize-html": "^2.15.0", - "svelte-tippy": "^1.3.2", - "tippy.js": "^6.3.7", - "tslog": "^4.9.3" - } -} diff --git a/Foxnouns.Frontend/src/app.d.ts b/Foxnouns.Frontend/src/app.d.ts deleted file mode 100644 index 243d72d..0000000 --- a/Foxnouns.Frontend/src/app.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -// See https://svelte.dev/docs/kit/types#app.d.ts - -import type { ErrorCode } from "$api/error"; - -// for information about these interfaces -declare global { - namespace App { - interface Error { - message: string; - status: number; - code: ErrorCode; - errors?: Array<{ key: string; errors: ValidationError[] }>; - error_id?: string; - } - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html deleted file mode 100644 index 1391f88..0000000 --- a/Foxnouns.Frontend/src/app.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - diff --git a/Foxnouns.Frontend/src/app.scss b/Foxnouns.Frontend/src/app.scss deleted file mode 100644 index c678355..0000000 --- a/Foxnouns.Frontend/src/app.scss +++ /dev/null @@ -1,74 +0,0 @@ -@use "bootstrap/scss/bootstrap" with ( - $color-mode-type: media-query, - $font-family-sans-serif: ( - "FiraGO", - system-ui, - -apple-system, - "Segoe UI", - Roboto, - "Helvetica Neue", - "Noto Sans", - "Liberation Sans", - Arial, - sans-serif, - "Apple Color Emoji", - "Segoe UI Emoji", - "Segoe UI Symbol", - "Noto Color Emoji", - ) -); - -@import "bootstrap-icons/font/bootstrap-icons.css"; -@import "@fontsource/firago/400.css"; -@import "@fontsource/firago/400-italic.css"; -@import "@fontsource/firago/700.css"; -@import "@fontsource/firago/700-italic.css"; - -// This is necessary for line breaks in translation strings to show up. Don't ask me why -.text-has-newline { - white-space: pre-line; -} - -// Make tippy tooltips look like bootstrap's -.tippy-box { - padding: bootstrap.$spacer * 0.25 bootstrap.$spacer * 0.5; - opacity: 0.9; - color: var(--bs-body-bg); - background-color: var(--bs-emphasis-color); - border-radius: var(--bs-border-radius); -} - -// Add breakpoint-dependent w-{size} utilities -// Source: https://stackoverflow.com/questions/47760132/any-way-to-get-breakpoint-specific-width-classes -@each $breakpoint in map-keys(bootstrap.$grid-breakpoints) { - @each $size, $length in (25: 25%, 50: 50%, 75: 75%, 100: 100%) { - @include bootstrap.media-breakpoint-up($breakpoint) { - .w-#{$breakpoint}-#{$size} { - width: $length !important; - } - } - } -} - -// Give identicons a background distinguishable from the page -.identicon { - @media (prefers-color-scheme: dark) { - background-color: var(--bs-secondary-border-subtle); - } - - background-color: var(--bs-light-border-subtle); -} - -.profile-flag { - height: 1.5rem; - max-width: 200px; - border-radius: 3px; -} - -.big-footer { - @media (prefers-color-scheme: dark) { - background-color: bootstrap.shade-color(bootstrap.$dark, 20%); - } - - background-color: bootstrap.shade-color(bootstrap.$light, 5%); -} diff --git a/Foxnouns.Frontend/src/hooks.server.ts b/Foxnouns.Frontend/src/hooks.server.ts deleted file mode 100644 index 35a0048..0000000 --- a/Foxnouns.Frontend/src/hooks.server.ts +++ /dev/null @@ -1,47 +0,0 @@ -import ApiError, { ErrorCode } from "$api/error"; -import { PRIVATE_API_HOST, PRIVATE_INTERNAL_API_HOST } from "$env/static/private"; -import { env } from "$env/dynamic/private"; -import { PUBLIC_API_BASE } from "$env/static/public"; -import log from "$lib/log"; -import type { HandleFetch, HandleServerError } from "@sveltejs/kit"; -import * as Sentry from "@sentry/sveltekit"; - -export const handleFetch: HandleFetch = async ({ request, fetch }) => { - if (request.url.startsWith(`${PUBLIC_API_BASE}/internal`)) { - request = new Request(request.url.replace(PUBLIC_API_BASE, PRIVATE_INTERNAL_API_HOST), request); - } else if (request.url.startsWith(PUBLIC_API_BASE)) { - request = new Request(request.url.replace(PUBLIC_API_BASE, PRIVATE_API_HOST), request); - } - - return await fetch(request); -}; - -Sentry.init({ - dsn: env.PRIVATE_SENTRY_DSN, -}); - -export const handleError: HandleServerError = async ({ error, status, message }) => { - if (error instanceof ApiError) { - return { - status: error.raw?.status || status, - message: error.raw?.message || "Unknown error", - code: error.code, - }; - } - - if (status >= 400 && status <= 499) { - return { status, message, code: ErrorCode.GenericApiError }; - } - - // client errors and backend API errors just clog up sentry, so we don't send those. - const id = Sentry.captureException(error, { - mechanism: { - type: "sveltekit", - handled: false, - }, - }); - - log.error("[%s] error in handler:", id, error); - - return { error_id: id, status, message, code: ErrorCode.InternalServerError }; -}; diff --git a/Foxnouns.Frontend/src/lib/actions/callback.ts b/Foxnouns.Frontend/src/lib/actions/callback.ts deleted file mode 100644 index 5c525ab..0000000 --- a/Foxnouns.Frontend/src/lib/actions/callback.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { apiRequest } from "$api"; -import ApiError, { ErrorCode } from "$api/error"; -import type { AddAccountResponse, CallbackResponse } from "$api/models"; -import { setToken } from "$lib"; -import log from "$lib/log"; -import { isRedirect, redirect, type ServerLoadEvent } from "@sveltejs/kit"; -import type { TicketData } from "../../routes/auth/callback/register/[ticket]/+page.server"; - -export default function createCallbackLoader( - callbackType: string, - bodyFn?: (event: ServerLoadEvent) => Promise, - returnData?: boolean, -) { - return async (event: ServerLoadEvent) => { - const { parent, fetch, cookies } = event; - - bodyFn ??= async ({ url }) => { - const code = url.searchParams.get("code") as string | null; - const state = url.searchParams.get("state") as string | null; - if (!code || !state) throw new ApiError(undefined, ErrorCode.BadRequest).obj; - return { code, state }; - }; - - const { meUser } = await parent(); - if (meUser) { - try { - const resp = await apiRequest( - "POST", - `/auth/${callbackType}/add-account/callback`, - { - isInternal: true, - body: await bodyFn(event), - fetch, - cookies, - }, - ); - - return { hasAccount: true, isLinkRequest: true, newAuthMethod: resp }; - } catch (e) { - if (e instanceof ApiError) return { isLinkRequest: true, error: e.obj }; - log.error("error linking new %s account to user %s:", callbackType, meUser.id, e); - throw e; - } - } - - try { - const resp = await apiRequest("POST", `/auth/${callbackType}/callback`, { - body: await bodyFn(event), - isInternal: true, - fetch, - }); - - if (resp.has_account) { - setToken(cookies, resp.token!); - redirect(303, `/@${resp.user!.username}`); - } - - if (returnData) - return { - ticket: resp.ticket!, - remoteUser: resp.remote_username!, - }; - - const ticket = btoa( - JSON.stringify({ - type: callbackType, - ticket: resp.ticket!, - remoteUsername: resp.remote_username!, - } satisfies TicketData), - ) - .replaceAll("+", "-") - .replaceAll("/", "_"); - - redirect(303, "/auth/callback/register/" + ticket); - } catch (e) { - if (isRedirect(e)) throw e; - if (e instanceof ApiError) return { isLinkRequest: false, error: e.obj }; - log.error("error while requesting %s callback:", callbackType, e); - throw e; - } - }; -} diff --git a/Foxnouns.Frontend/src/lib/actions/modaction.ts b/Foxnouns.Frontend/src/lib/actions/modaction.ts deleted file mode 100644 index 15fd16f..0000000 --- a/Foxnouns.Frontend/src/lib/actions/modaction.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { apiRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import type { AuditLogEntry, ClearableField } from "$api/models/moderation"; -import log from "$lib/log"; -import { type RequestEvent } from "@sveltejs/kit"; - -type ModactionResponse = { ok: boolean; resp: AuditLogEntry | null; error: RawApiError | null }; -type ModactionFunction = (evt: RequestEvent) => Promise; - -export default function createModactionAction( - type: "ignore" | "warn" | "suspend", - requireReason: boolean, -): ModactionFunction { - return async function ({ request, fetch, cookies }) { - const body = await request.formData(); - const userId = body.get("user") as string; - const memberId = body.get("member") as string | null; - const reportId = body.get("report") as string | null; - const reason = body.get("reason") as string | null; - - if (!reportId && type === "ignore") { - return { - ok: false, - resp: null, - error: { - status: 400, - message: "Bad request", - code: ErrorCode.BadRequest, - errors: [ - { key: "report", errors: [{ message: "Ignoring a report requires a report ID" }] }, - ], - } satisfies RawApiError, - }; - } - - if (!reason && requireReason) { - return { - ok: false, - resp: null, - error: { - status: 400, - message: "Bad request", - code: ErrorCode.BadRequest, - errors: [{ key: "reason", errors: [{ message: "You must give a reason" }] }], - } satisfies RawApiError, - }; - } - - let clearFields: ClearableField[] | undefined = undefined; - if (type === "warn") { - clearFields = body.getAll("clear-fields") as ClearableField[]; - } - - let path: string; - if (type === "warn") path = `/moderation/warnings/${userId}`; - else if (type === "suspend") path = `/moderation/suspensions/${userId}`; - else path = `/moderation/reports/${reportId}/ignore`; - - try { - const resp = await apiRequest("POST", path, { - fetch, - cookies, - body: { - reason: reason, - // These are ignored by POST /reports/{id}/ignore - member_id: memberId, - report_id: reportId, - // This is ignored by everything but POST /warnings/{id} - clear_fields: clearFields, - // This is ignored by everything but POST /suspensions/{id} - clear_profile: !!body.get("clear-profile"), - }, - }); - - return { ok: true, resp, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj, resp: null }; - log.error("could not take action on %s:", path, e); - throw e; - } - }; -} - -export function createModactions() { - return { - ignore: createModactionAction("ignore", false), - warn: createModactionAction("warn", true), - suspend: createModactionAction("suspend", true), - }; -} diff --git a/Foxnouns.Frontend/src/lib/api/error.ts b/Foxnouns.Frontend/src/lib/api/error.ts deleted file mode 100644 index e893a86..0000000 --- a/Foxnouns.Frontend/src/lib/api/error.ts +++ /dev/null @@ -1,70 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export default class ApiError { - raw?: RawApiError; - code: ErrorCode; - - constructor(err?: RawApiError, code?: ErrorCode) { - this.raw = err; - this.code = err?.code || code || ErrorCode.InternalServerError; - } - - get obj(): RawApiError { - return this.toObject(); - } - - toObject(): RawApiError { - return { - error_id: this.raw?.error_id, - status: this.raw?.status || 500, - code: this.code, - message: this.raw?.message || "Internal server error", - errors: this.raw?.errors, - }; - } -} - -export type RawApiError = { - error_id?: string; - status: number; - message: string; - code: ErrorCode; - errors?: Array<{ key: string; errors: ValidationError[] }>; -}; - -export enum ErrorCode { - InternalServerError = "INTERNAL_SERVER_ERROR", - Forbidden = "FORBIDDEN", - BadRequest = "BAD_REQUEST", - AuthenticationError = "AUTHENTICATION_ERROR", - AuthenticationRequired = "AUTHENTICATION_REQUIRED", - MissingScopes = "MISSING_SCOPES", - GenericApiError = "GENERIC_API_ERROR", - UserNotFound = "USER_NOT_FOUND", - MemberNotFound = "MEMBER_NOT_FOUND", - AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED", - LastAuthMethod = "LAST_AUTH_METHOD", - PageNotFound = "PAGE_NOT_FOUND", - // This code isn't actually returned by the API - Non204Response = "(non 204 response)", -} - -export type ValidationError = { - message: string; - min_length?: number; - max_length?: number; - actual_length?: number; - allowed_values?: any[]; - actual_value?: any; -}; - -/** - * Returns the first error for the value `key` in `error`. - * @param error The error object to traverse. - * @param key The JSON key to find. - */ -export const firstErrorFor = (error: RawApiError, key: string): ValidationError | undefined => { - if (!error.errors) return undefined; - const field = error.errors.find((e) => e.key == key); - if (!field?.errors) return undefined; - return field.errors.length != 0 ? field.errors[0] : undefined; -}; diff --git a/Foxnouns.Frontend/src/lib/api/index.ts b/Foxnouns.Frontend/src/lib/api/index.ts deleted file mode 100644 index e52918a..0000000 --- a/Foxnouns.Frontend/src/lib/api/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { PUBLIC_API_BASE } from "$env/static/public"; -import type { Cookies } from "@sveltejs/kit"; -import ApiError, { ErrorCode } from "./error"; -import { TOKEN_COOKIE_NAME } from "$lib"; -import log from "$lib/log"; - -export type Method = "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; - -/** - * Optional arguments for a request. `load` and `action` functions should always pass `fetch` and `cookies`. - */ -export type RequestArgs = { - /** - * The token for this request. Where possible, `cookies` should be passed instead. - * Will override `cookies` if both are passed. - */ - token?: string; - /** - * Whether this request is to an internal endpoint. - * Internal requests bypass the rate limiter and are prefixed with /api/internal/ rather than /api/v2/. - */ - isInternal?: boolean; - /** - * The body for this request, which will be serialized to JSON. Should be a plain JS object. - */ - body?: T; - /** - * The fetch function to use. Should be passed in loader and action functions, but can be safely ignored for client-side requests. - */ - fetch?: typeof fetch; - /** - * The cookies object to try to get the token from. Can only be passed in loader and action functions. - */ - cookies?: Cookies; -}; - -/** - * Makes a raw request to the API. - * @param method The HTTP method for this request - * @param path The path for this request, without the /api/v2 prefix, starting with a slash. - * @param args Optional arguments to the request function. - * @returns A Response object. - */ -export async function baseRequest( - method: Method, - path: string, - args: RequestArgs = {}, -): Promise { - const token = args.token ?? args.cookies?.get(TOKEN_COOKIE_NAME); - - const fetchFn = args.fetch ?? fetch; - const url = `/${args.isInternal ? "internal" : "v2"}${path}`; - - log.debug("Sending request to %s %s", method, url); - - const headers = { - ...(args.body ? { "Content-Type": "application/json; charset=utf-8" } : {}), - ...(token ? { Authorization: token } : {}), - }; - - return await fetchFn(PUBLIC_API_BASE + url, { - method, - headers, - body: args.body ? JSON.stringify(args.body) : undefined, - }); -} - -/** - * Makes a request to the API and parses the returned object. - * @param method The HTTP method for this request - * @param path The path for this request, without the /api/v2 prefix, starting with a slash. - * @param args Optional arguments to the request function. - * @returns The response deserialized as `T`. - */ -export async function apiRequest( - method: Method, - path: string, - args: RequestArgs = {}, -): Promise { - const resp = await baseRequest(method, path, args); - - if (resp.status < 200 || resp.status > 299) { - const err = await resp.json(); - log.error("Received error for request to %s %s:", method, path, err); - if ("code" in err) throw new ApiError(err); - else throw new ApiError(); - } - return (await resp.json()) as TResponse; -} - -/** - * Makes a request without reading the body (unless the API returns an error). - * @param method The HTTP method for this request - * @param path The path for this request, without the /api/v2 prefix, starting with a slash. - * @param args Optional arguments to the request function. - * @param enforce204 Whether to throw an error on a non-204 status code. - */ -export async function fastRequest( - method: Method, - path: string, - args: RequestArgs = {}, - enforce204: boolean = false, -): Promise { - const resp = await baseRequest(method, path, args); - - if (resp.status < 200 || resp.status > 299) { - const err = await resp.json(); - if ("code" in err) throw new ApiError(err); - else throw new ApiError(); - } - - if (enforce204 && resp.status !== 204) throw new ApiError(undefined, ErrorCode.Non204Response); -} diff --git a/Foxnouns.Frontend/src/lib/api/models/auth.ts b/Foxnouns.Frontend/src/lib/api/models/auth.ts deleted file mode 100644 index 2129425..0000000 --- a/Foxnouns.Frontend/src/lib/api/models/auth.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { AuthType, User } from "./user"; - -export type AuthResponse = { - user: User; - token: string; - expires_at: string; -}; - -export type CallbackResponse = { - has_account: boolean; - ticket?: string; - remote_username?: string; - user?: User; - token?: string; - expires_at?: string; -}; - -export type AuthUrls = { - email_enabled: boolean; - discord?: string; - google?: string; - tumblr?: string; -}; - -export type AddAccountResponse = { - id: string; - type: AuthType; - remote_id: string; - remote_username?: string; -}; diff --git a/Foxnouns.Frontend/src/lib/api/models/index.ts b/Foxnouns.Frontend/src/lib/api/models/index.ts deleted file mode 100644 index cc8fd7e..0000000 --- a/Foxnouns.Frontend/src/lib/api/models/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./meta"; -export * from "./user"; -export * from "./member"; -export * from "./auth"; diff --git a/Foxnouns.Frontend/src/lib/api/models/member.ts b/Foxnouns.Frontend/src/lib/api/models/member.ts deleted file mode 100644 index 26eab92..0000000 --- a/Foxnouns.Frontend/src/lib/api/models/member.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Field, PartialMember, PartialUser, PrideFlag } from "./user"; - -export type Member = PartialMember & { - fields: Field[]; - flags: PrideFlag[]; - links: string[]; - - user: PartialUser; -}; diff --git a/Foxnouns.Frontend/src/lib/api/models/meta.ts b/Foxnouns.Frontend/src/lib/api/models/meta.ts deleted file mode 100644 index 28ea494..0000000 --- a/Foxnouns.Frontend/src/lib/api/models/meta.ts +++ /dev/null @@ -1,22 +0,0 @@ -export type Meta = { - repository: string; - version: string; - hash: string; - users: { - total: number; - active_month: number; - active_week: number; - active_day: number; - }; - members: number; - limits: Limits; - notice: { id: string; message: string } | null; -}; - -export type Limits = { - member_count: number; - bio_length: number; - custom_preferences: number; - max_auth_methods: number; - max_flags: number; -}; diff --git a/Foxnouns.Frontend/src/lib/api/models/moderation.ts b/Foxnouns.Frontend/src/lib/api/models/moderation.ts deleted file mode 100644 index 689e9b8..0000000 --- a/Foxnouns.Frontend/src/lib/api/models/moderation.ts +++ /dev/null @@ -1,123 +0,0 @@ -import type { Member } from "./member"; -import type { AuthMethod, PartialMember, PartialUser, User } from "./user"; - -export type CreateReportRequest = { - reason: ReportReason; - context: string | null; -}; - -export enum ReportReason { - Totalitarianism = "TOTALITARIANISM", - HateSpeech = "HATE_SPEECH", - Racism = "RACISM", - Homophobia = "HOMOPHOBIA", - Transphobia = "TRANSPHOBIA", - Queerphobia = "QUEERPHOBIA", - Exclusionism = "EXCLUSIONISM", - Sexism = "SEXISM", - Ableism = "ABLEISM", - ChildPornography = "CHILD_PORNOGRAPHY", - PedophiliaAdvocacy = "PEDOPHILIA_ADVOCACY", - Harassment = "HARASSMENT", - Impersonation = "IMPERSONATION", - Doxxing = "DOXXING", - EncouragingSelfHarm = "ENCOURAGING_SELF_HARM", - Spam = "SPAM", - Trolling = "TROLLING", - Advertisement = "ADVERTISEMENT", - CopyrightViolation = "COPYRIGHT_VIOLATION", -} - -export type Report = { - id: string; - reporter: PartialUser; - target_user: PartialUser; - target_member?: PartialMember; - status: "OPEN" | "CLOSED"; - reason: ReportReason; - context: string | null; - target_type: "USER" | "MEMBER"; - snapshot: User | Member | null; -}; - -export type AuditLogEntry = { - id: string; - moderator: AuditLogEntity; - target_user?: AuditLogEntity; - target_member?: AuditLogEntity; - report?: PartialReport; - type: AuditLogEntryType; - reason: string | null; - cleared_fields?: string[]; -}; - -export type AuditLogEntity = { id: string; username: string }; - -export enum AuditLogEntryType { - IgnoreReport = "IGNORE_REPORT", - WarnUser = "WARN_USER", - WarnUserAndClearProfile = "WARN_USER_AND_CLEAR_PROFILE", - SuspendUser = "SUSPEND_USER", - QuerySensitiveUserData = "QUERY_SENSITIVE_USER_DATA", -} - -export type PartialReport = { - id: string; - reporter_id: string; - target_user_id: string; - target_member_id?: string; - reason: ReportReason; - context: string | null; - target_type: "USER" | "MEMBER"; -}; - -export type ReportDetails = { - report: Report; - user: User; - member?: Member; - audit_log_entry?: AuditLogEntry; -}; - -export type QueriedUser = { - user: User; - member_list_hidden: boolean; - last_active: string; - last_sid_reroll: string; - suspended: boolean; - deleted: boolean; - auth_methods?: AuthMethod[]; -}; - -export type WarnUserRequest = { - reason: string; - clear_fields?: ClearableField[]; - member_id?: string; - report_id?: string; -}; - -export type SuspendUserRequest = { - reason: string; - clear_profile: boolean; - report_id?: string; -}; - -export enum ClearableField { - DisplayName = "DISPLAY_NAME", - Avatar = "AVATAR", - Bio = "BIO", - Links = "LINKS", - Names = "NAMES", - Pronouns = "PRONOUNS", - Fields = "FIELDS", - Flags = "FLAGS", - CustomPreferences = "CUSTOM_PREFERENCES", -} - -export type Notification = { - id: string; - type: "NOTICE" | "WARNING" | "SUSPENSION"; - message?: string; - localization_key?: string; - localization_params: Record; - acknowledged: boolean; -}; diff --git a/Foxnouns.Frontend/src/lib/api/models/user.ts b/Foxnouns.Frontend/src/lib/api/models/user.ts deleted file mode 100644 index be9d961..0000000 --- a/Foxnouns.Frontend/src/lib/api/models/user.ts +++ /dev/null @@ -1,150 +0,0 @@ -export type PartialUser = { - id: string; - sid: string; - username: string; - display_name: string | null; - avatar_url: string | null; - custom_preferences: Record; -}; - -export type User = PartialUser & { - bio: string | null; - member_title: string | null; - links: string[]; - names: FieldEntry[]; - pronouns: Pronoun[]; - fields: Field[]; - flags: PrideFlag[]; - utc_offset: number | null; - role: "USER" | "MODERATOR" | "ADMIN"; -}; - -export type MeUser = UserWithMembers & { - members: PartialMember[]; - auth_methods: AuthMethod[]; - member_list_hidden: boolean; - last_active: string; - last_sid_reroll: string; - timezone: string; - suspended: boolean; - deleted: boolean; - settings: UserSettings; -}; - -export type UserWithMembers = User & { members: PartialMember[] | null }; - -export type UserWithHiddenFields = User & { - auth_methods?: unknown[]; - member_list_hidden: boolean; - last_active: string; -}; - -export type UserSettings = { - dark_mode: boolean | null; - last_read_notice: string | null; -}; - -export type PartialMember = { - id: string; - sid: string; - name: string; - display_name: string; - bio: string | null; - avatar_url: string | null; - names: FieldEntry[]; - pronouns: Pronoun[]; - unlisted: boolean | null; -}; - -export type FieldEntry = { - value: string; - status: string; -}; - -export type Pronoun = FieldEntry & { display_text: string | null }; - -export type Field = { - name: string; - entries: FieldEntry[]; -}; - -export type PrideFlag = { - id: string; - image_url: string | null; - name: string; - description: string | null; -}; - -export type AuthType = "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; - -export type AuthMethod = { - id: string; - type: AuthType; - remote_id: string; - remote_username?: string; -}; - -export type CustomPreference = { - icon: string; - tooltip: string; - muted: boolean; - favourite: boolean; - size: PreferenceSize; -}; - -export enum PreferenceSize { - Large = "LARGE", - Normal = "NORMAL", - Small = "SMALL", -} - -export function mergePreferences( - prefs: Record, -): Record { - return Object.assign({}, defaultPreferences, prefs); -} - -export const defaultPreferences = Object.freeze({ - favourite: { - icon: "heart-fill", - tooltip: "Favourite", - size: PreferenceSize.Large, - muted: false, - favourite: true, - }, - okay: { - icon: "hand-thumbs-up", - tooltip: "Okay", - size: PreferenceSize.Normal, - muted: false, - favourite: false, - }, - jokingly: { - icon: "emoji-laughing", - tooltip: "Jokingly", - size: PreferenceSize.Normal, - muted: false, - favourite: false, - }, - friends_only: { - icon: "people", - tooltip: "Friends only", - size: PreferenceSize.Normal, - muted: false, - favourite: false, - }, - avoid: { - icon: "hand-thumbs-down", - tooltip: "Avoid", - size: PreferenceSize.Small, - muted: true, - favourite: false, - }, - missing: { - icon: "question-lg", - tooltip: "Unknown (missing)", - size: PreferenceSize.Normal, - muted: false, - favourite: false, - }, -}); diff --git a/Foxnouns.Frontend/src/lib/components/Avatar.svelte b/Foxnouns.Frontend/src/lib/components/Avatar.svelte deleted file mode 100644 index eff2c7b..0000000 --- a/Foxnouns.Frontend/src/lib/components/Avatar.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - - diff --git a/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte b/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte deleted file mode 100644 index 7b08026..0000000 --- a/Foxnouns.Frontend/src/lib/components/ClientPaginator.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - -{#if pageCount > 1} -
- - - (currentPage = 0)} /> - - - (currentPage = prevPage)} /> - - {#if allPages} - {#each new Array(pageCount) as _, page} - - (currentPage = page)}>{page + 1} - - {/each} - {:else} - {#if currentPage !== 0} - (currentPage = prevPage)}> - {currentPage} - - {/if} - - {currentPage + 1} - - {#if currentPage !== pageCount - 1} - (currentPage = nextPage)}> - {currentPage + 2} - - {/if} - {/if} - - (currentPage = nextPage)} /> - - - (currentPage = pageCount - 1)} /> - - -
-{/if} diff --git a/Foxnouns.Frontend/src/lib/components/Error.svelte b/Foxnouns.Frontend/src/lib/components/Error.svelte deleted file mode 100644 index c9e2c0e..0000000 --- a/Foxnouns.Frontend/src/lib/components/Error.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - -{#if showHeader !== false} - - {#if error.code === ErrorCode.BadRequest} - {$t("error.bad-request-header")} - {:else if error.status === 404} - {$t("error.not-found-header")} - {:else} - {$t("error.generic-header")} - {/if} - -{/if} -

{errorDescription($t, error.code)}

-{#if error.error_id} -

- {$t("error.error-id")} - - {error.error_id} - -

-{/if} -{#if error.errors} -
- {$t("error.extra-info-header")} -
    - {#each error.errors as val} - - {/each} -
-
-{/if} -
- {$t("error.raw-header")} -
{JSON.stringify(error, undefined, "  ")}
-
diff --git a/Foxnouns.Frontend/src/lib/components/ErrorAlert.svelte b/Foxnouns.Frontend/src/lib/components/ErrorAlert.svelte deleted file mode 100644 index a94d9ed..0000000 --- a/Foxnouns.Frontend/src/lib/components/ErrorAlert.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/lib/components/Footer.svelte b/Foxnouns.Frontend/src/lib/components/Footer.svelte deleted file mode 100644 index 857c07c..0000000 --- a/Foxnouns.Frontend/src/lib/components/Footer.svelte +++ /dev/null @@ -1,84 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte b/Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte deleted file mode 100644 index c8a55f1..0000000 --- a/Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - -{#if renderNotice} - -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/IconButton.svelte b/Foxnouns.Frontend/src/lib/components/IconButton.svelte deleted file mode 100644 index a370002..0000000 --- a/Foxnouns.Frontend/src/lib/components/IconButton.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/lib/components/Logo.svelte b/Foxnouns.Frontend/src/lib/components/Logo.svelte deleted file mode 100644 index 9da9d99..0000000 --- a/Foxnouns.Frontend/src/lib/components/Logo.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/Foxnouns.Frontend/src/lib/components/Navbar.svelte b/Foxnouns.Frontend/src/lib/components/Navbar.svelte deleted file mode 100644 index 2074347..0000000 --- a/Foxnouns.Frontend/src/lib/components/Navbar.svelte +++ /dev/null @@ -1,111 +0,0 @@ - - -{#if user && unreadNotifications} -
- {$t("nav.unread-notification-text")} -
- {$t("nav.unread-notification-link")} -
-{/if} - -{#if user && user.deleted} -
- {#if user.suspended} - {$t("nav.suspended-account-hint")} -
- {$t("nav.delete-permanently-link")} • - {$t("nav.appeal-suspension-link")} • - {$t("nav.export-link")} - {:else} - {$t("nav.deleted-account-hint")} -
- {$t("nav.reactivate-or-delete-link")} • - {$t("nav.export-link")} - {/if} -
-{/if} - - - - - {#if meta.version.endsWith(".dirty")} - dev - {:else} - beta - {/if} - - - - - - - - diff --git a/Foxnouns.Frontend/src/lib/components/Paginator.svelte b/Foxnouns.Frontend/src/lib/components/Paginator.svelte deleted file mode 100644 index 07cbd8d..0000000 --- a/Foxnouns.Frontend/src/lib/components/Paginator.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - -{#if pageCount > 1} -
- - - - - - - - {#each new Array(pageCount) as _, page} - - {page + 1} - - {/each} - - - - - - - -
-{/if} diff --git a/Foxnouns.Frontend/src/lib/components/RequiredFieldMarker.svelte b/Foxnouns.Frontend/src/lib/components/RequiredFieldMarker.svelte deleted file mode 100644 index f67d9ef..0000000 --- a/Foxnouns.Frontend/src/lib/components/RequiredFieldMarker.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - -{#if required} - * -{:else} - {$t("form.optional")} -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte b/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte deleted file mode 100644 index 1dbd5a3..0000000 --- a/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - - - - -{preference.tooltip}: diff --git a/Foxnouns.Frontend/src/lib/components/URLAlert.svelte b/Foxnouns.Frontend/src/lib/components/URLAlert.svelte deleted file mode 100644 index 72689ab..0000000 --- a/Foxnouns.Frontend/src/lib/components/URLAlert.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -{#if key} -
- {$t(key)} -
-{/if} diff --git a/Foxnouns.Frontend/src/lib/components/admin/ActionForm.svelte b/Foxnouns.Frontend/src/lib/components/admin/ActionForm.svelte deleted file mode 100644 index 3c7f741..0000000 --- a/Foxnouns.Frontend/src/lib/components/admin/ActionForm.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - -
- - {#if memberId} - - {/if} - {#if reportId} - - {/if} - - - - {#if reportId} - - - - {/if} - -
- {#each fields as field} -
- - -
- {/each} -
-
- -
-
- -
- - -
- -
-
- diff --git a/Foxnouns.Frontend/src/lib/components/admin/AuditLogEntity.svelte b/Foxnouns.Frontend/src/lib/components/admin/AuditLogEntity.svelte deleted file mode 100644 index 1f3645f..0000000 --- a/Foxnouns.Frontend/src/lib/components/admin/AuditLogEntity.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -{entity.username} ({entity.id}) diff --git a/Foxnouns.Frontend/src/lib/components/admin/AuditLogEntryCard.svelte b/Foxnouns.Frontend/src/lib/components/admin/AuditLogEntryCard.svelte deleted file mode 100644 index b1d777a..0000000 --- a/Foxnouns.Frontend/src/lib/components/admin/AuditLogEntryCard.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - -
-
- - - {#if entry.type === "IGNORE_REPORT"} - ignored a report - {:else if entry.type === "WARN_USER" || entry.type === "WARN_USER_AND_CLEAR_PROFILE"} - warned - {:else if entry.type === "SUSPEND_USER"} - suspended - {:else if entry.type === "QUERY_SENSITIVE_USER_DATA"} - looked up sensitive data of - {:else} - (unknown action {entry.type}) - {/if} - {#if entry.target_user} - - {/if} - {#if entry.target_member} - for member - {/if} - - - {date} -
- - {#if entry.type === "IGNORE_REPORT"} - {#if entry.report} -
- Report -
    -
  • From: {entry.report.reporter_id}
  • -
  • Target: {entry.report.target_user_id}
  • -
  • Reason: {entry.report.reason}
  • - {#if entry.report.context} -
  • Context: {entry.report.context}
  • - {/if} -
-
- {:else} -

(the ignored report has been deleted)

- {/if} - {/if} - - {#if reason} -
- Reason - - {@html reason} -
- {:else} -

(no reason given)

- {/if} -
diff --git a/Foxnouns.Frontend/src/lib/components/admin/ClosedReportAuditLog.svelte b/Foxnouns.Frontend/src/lib/components/admin/ClosedReportAuditLog.svelte deleted file mode 100644 index 589539f..0000000 --- a/Foxnouns.Frontend/src/lib/components/admin/ClosedReportAuditLog.svelte +++ /dev/null @@ -1,38 +0,0 @@ - - -
-

- Closed by at - {idTimestamp(entry.id).toLocaleString(DateTime.DATETIME_MED)} -

-

- {#if entry.type === AuditLogEntryType.IgnoreReport} - Report was ignored - {:else if entry.type === AuditLogEntryType.WarnUser || entry.type === AuditLogEntryType.WarnUserAndClearProfile} - User was warned - {#if entry.cleared_fields && entry.cleared_fields.length > 0} -
Cleared fields: {entry.cleared_fields.join(", ")} - {/if} - {:else if entry.type === AuditLogEntryType.SuspendUser} - User was suspended - {/if} -

-

Reason

-

- {#if entry.reason} - - {@html renderMarkdown(entry.reason)} - {:else} - (no reason given) - {/if} -

-
diff --git a/Foxnouns.Frontend/src/lib/components/admin/DashboardCard.svelte b/Foxnouns.Frontend/src/lib/components/admin/DashboardCard.svelte deleted file mode 100644 index a6d04e4..0000000 --- a/Foxnouns.Frontend/src/lib/components/admin/DashboardCard.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - -
-
-
-
{title}
-

- {@render children()} -

-
-
-
diff --git a/Foxnouns.Frontend/src/lib/components/admin/PartialProfileCard.svelte b/Foxnouns.Frontend/src/lib/components/admin/PartialProfileCard.svelte deleted file mode 100644 index 885ca82..0000000 --- a/Foxnouns.Frontend/src/lib/components/admin/PartialProfileCard.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
- - - -

- - @{user.username} - -

-

Created {createdAt}

-
diff --git a/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte deleted file mode 100644 index 288bcc5..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte +++ /dev/null @@ -1,164 +0,0 @@ - - -

- -

- - (cropperOpen = !cropperOpen)} -> - - - - - - - - - - - - -{#if updated} -

- - {$t("edit-profile.avatar-updated")} -

-{/if} - -{#if avatarTooLarge} -

- - {$t("edit-profile.file-too-large", { - max: prettyBytes(MAX_AVATAR_BYTES), - current: prettyBytes(avatar.length), - })} -

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

- {$t("edit-profile.bio-length-hint", { - length: value.length, - maxLength, - })} -

- -{#if value !== ""} -
-
{$t("edit-profile.preview")}
- - -
{@html renderMarkdown(value)}
-
-{/if} diff --git a/Foxnouns.Frontend/src/lib/components/editor/CustomPreferenceEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/CustomPreferenceEditor.svelte deleted file mode 100644 index b9eec51..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/CustomPreferenceEditor.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -
- - - - (pref.favourite = !pref.favourite)} - active={pref.favourite} - tooltip={$t("editor.custom-preference-favourite")} - /> - (pref.muted = !pref.muted)} - active={pref.muted} - tooltip={$t("editor.custom-preference-muted")} - /> - remove()} - /> -
diff --git a/Foxnouns.Frontend/src/lib/components/editor/CustomPreferencesNotice.svelte b/Foxnouns.Frontend/src/lib/components/editor/CustomPreferencesNotice.svelte deleted file mode 100644 index 4efbdfe..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/CustomPreferencesNotice.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - -
- {$t("editor.custom-preference-notice")} - {$t("editor.custom-preference-notice-link")} -
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/FieldEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte deleted file mode 100644 index b409290..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte +++ /dev/null @@ -1,94 +0,0 @@ - - -{#if index !== undefined && move && remove} -
- - move(index, true)} - /> - move(index, false)} - /> - {$t("editor.field-name")} - - remove(index)} - /> - -
-{:else} -

{name}

-{/if} - -{#each entries as _, i} - -{/each} - -
- - - diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte deleted file mode 100644 index 5e407ac..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - -
- moveValue(index, true)} - /> - moveValue(index, false)} - /> - - - - - - - - - {#each prefIds as id} - (value.status = id)} active={value.status === id}> - - {allPreferences[id].tooltip} - - {/each} - - - removeValue(index)} - /> -
diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte deleted file mode 100644 index bbc57d5..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - - - - - -

{$t("edit-profile.editing-fields-header")}

- -
-
{$t("editor.add-field")}
-
- - - -
- -{#if fields.length > 0} -
- {#each fields as field, index} - - {/each} -{/if} -
- -
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 bf7c1f0..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - - - -
- {#each arr 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/FormStatusMarker.svelte b/Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte deleted file mode 100644 index 8ffbdb6..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - -{#if form?.error} - -{:else if form?.ok} -

- - {successMessage ?? $t("edit-profile.saved-changes")} -

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

- {$t("editor.links-header")} - -

- - - -{#each links as _, index} -
- moveValue(index, true)} - /> - moveValue(index, false)} - /> - - removeValue(index)} - /> -
-{/each} - -
- - - diff --git a/Foxnouns.Frontend/src/lib/components/editor/NoscriptWarning.svelte b/Foxnouns.Frontend/src/lib/components/editor/NoscriptWarning.svelte deleted file mode 100644 index 4bddf68..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/NoscriptWarning.svelte +++ /dev/null @@ -1,12 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/lib/components/editor/PreferenceIconSelector.svelte b/Foxnouns.Frontend/src/lib/components/editor/PreferenceIconSelector.svelte deleted file mode 100644 index 3bc3f05..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/PreferenceIconSelector.svelte +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - {$t("editor.icons-change-icon")} - - - -

- -

-
    - {#each filteredIcons as selectable} - (icon = selectable)} - /> - {/each} -
- {#if totalIcons > MAX_VISIBLE_ITEMS || showAll} - - (showAll = !showAll)}> - {#if showAll} - {$t("editor.icons-stop-showing-all")} - {:else} - {$t("editor.icons-show-all", { count: totalIcons })} - {/if} - - {/if} -
-
- - diff --git a/Foxnouns.Frontend/src/lib/components/editor/PreferenceSizeEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/PreferenceSizeEditor.svelte deleted file mode 100644 index 46fc3e0..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/PreferenceSizeEditor.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - {$t("editor.custom-preference-size")} - - - - (size = PreferenceSize.Large)} - > - {$t("editor.custom-preference-size-large")} - - (size = PreferenceSize.Normal)} - > - {$t("editor.custom-preference-size-medium")} - - (size = PreferenceSize.Small)} - > - {$t("editor.custom-preference-size-small")} - - - 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 304ae88..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte +++ /dev/null @@ -1,103 +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/editor/PronounEntryEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte deleted file mode 100644 index 64b354e..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte +++ /dev/null @@ -1,101 +0,0 @@ - - -
-
- moveValue(index, true)} - /> - moveValue(index, false)} - /> - - - - - - - - - {#each prefIds as id} - (value.status = id)} active={value.status === id}> - - {allPreferences[id].tooltip} - - {/each} - - - (displayOpen = !displayOpen)} - /> - removeValue(index)} - /> -
- - -
- {$t("editor.display-text-label")} - - - - - {$t("editor.display-text-info")} - -
-
-
diff --git a/Foxnouns.Frontend/src/lib/components/editor/PronounsEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/PronounsEditor.svelte deleted file mode 100644 index 5f49e73..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/PronounsEditor.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - -

{$t("profile.pronouns-header")}

- -{#each entries as _, index} - -{/each} - -
- - - diff --git a/Foxnouns.Frontend/src/lib/components/editor/ShortNoscriptWarning.svelte b/Foxnouns.Frontend/src/lib/components/editor/ShortNoscriptWarning.svelte deleted file mode 100644 index fb474db..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/ShortNoscriptWarning.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/lib/components/editor/SidEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/SidEditor.svelte deleted file mode 100644 index 1547ff2..0000000 --- a/Foxnouns.Frontend/src/lib/components/editor/SidEditor.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -{$t("edit-profile.sid-current")} {sid} - - - - - -

- - {$t("edit-profile.sid-hint")} -

diff --git a/Foxnouns.Frontend/src/lib/components/errors/KeyedValidationErrors.svelte b/Foxnouns.Frontend/src/lib/components/errors/KeyedValidationErrors.svelte deleted file mode 100644 index 86c21a9..0000000 --- a/Foxnouns.Frontend/src/lib/components/errors/KeyedValidationErrors.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
  • - {key}: -
      - {#each errors as error} - - {/each} -
    -
  • diff --git a/Foxnouns.Frontend/src/lib/components/errors/RequestValidationError.svelte b/Foxnouns.Frontend/src/lib/components/errors/RequestValidationError.svelte deleted file mode 100644 index c840fb7..0000000 --- a/Foxnouns.Frontend/src/lib/components/errors/RequestValidationError.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -{#if isLengthError} - {#if error.actual_length! > error.max_length!} -
  • - {$t("error.validation-max-length-error", { - max: error.max_length, - actual: error.actual_length, - })} -
  • - {:else} -
  • - {$t("error.validation-min-length-error", { - min: error.min_length, - actual: error.actual_length, - })} -
  • - {/if} -{:else if isDisallowedValueError} -
  • - {$t("error.validation-disallowed-value-1")}: {error.actual_value}
    - {$t("error.validation-disallowed-value-2")}: - {error.allowed_values!.map((v) => v.toString()).join(", ")} -
  • -{:else if error.actual_value} -
  • - {$t("error.validation-disallowed-value-1")}: {error.actual_value}
    - {$t("error.validation-reason")}: {error.message} -
  • -{:else} -
  • {$t("error.validation-generic")}: {error.message}
  • -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/profile/OwnProfileNotice.svelte b/Foxnouns.Frontend/src/lib/components/profile/OwnProfileNotice.svelte deleted file mode 100644 index 9515628..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/OwnProfileNotice.svelte +++ /dev/null @@ -1,16 +0,0 @@ - - -
    - {#if memberName} - {$t("profile.edit-member-profile-notice", { memberName })} - {:else} - {$t("profile.edit-user-profile-notice")} - {/if} -
    - {$t("profile.edit-profile-link")} -
    diff --git a/Foxnouns.Frontend/src/lib/components/profile/PreferenceCheatsheet.svelte b/Foxnouns.Frontend/src/lib/components/profile/PreferenceCheatsheet.svelte deleted file mode 100644 index 5e03a4e..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/PreferenceCheatsheet.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
    -
      - {#each preferences as preference} -
    • - - {preference.tooltip} -
    • - {/each} -
    -
    diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileButtons.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileButtons.svelte deleted file mode 100644 index d3de215..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileButtons.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -
    - - - {#if meUser && meUser.username !== user} - {$t("profile.report-button")} - {/if} -
    diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileFields.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileFields.svelte deleted file mode 100644 index d6594f8..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileFields.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - -
    - {#if profile.names.length > 0} - - {/if} - {#if profile.pronouns.length > 0} - - {/if} - {#each profile.fields as field} - {#if field.entries.length > 0} - - {/if} - {/each} -
    diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte deleted file mode 100644 index ab098db..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - - - {flag.description - {flag.name} - diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte deleted file mode 100644 index 9a0ebff..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - -
    -
    -
    - - - {#if profile.flags && profile.bio} -
    - {#each profile.flags as flag} - - {/each} -
    - {/if} -
    -
    - {#if profile.display_name} -
    -

    {profile.display_name}

    -

    {name}

    -
    - {:else} -

    {name}

    - {/if} - {#if offset}{/if} - {#if bio} -
    - - -

    {@html bio}

    - {/if} -
    - {#if profile.links.length > 0} -
    -
      - {#each profile.links as link} - - {/each} -
    -
    - {/if} -
    -
    -{#if profile.flags && !profile.bio} -
    - {#each profile.flags as flag} - - {/each} -
    -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte deleted file mode 100644 index 0a42c58..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - -{#if isLink} - -
  • - - {displayLink} -
  • -
    -{:else} -
  • - - {displayLink} -
  • -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/profile/TimeOffset.svelte b/Foxnouns.Frontend/src/lib/components/profile/TimeOffset.svelte deleted file mode 100644 index f6d2de4..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/TimeOffset.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - -{currentTime} (UTC{timezone}) diff --git a/Foxnouns.Frontend/src/lib/components/profile/field/ProfileField.svelte b/Foxnouns.Frontend/src/lib/components/profile/field/ProfileField.svelte deleted file mode 100644 index 01c998d..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/field/ProfileField.svelte +++ /dev/null @@ -1,30 +0,0 @@ - - -
    -

    {name}

    -
      - {#each entries as entry} -
    • - - {#if "display_text" in entry} - - {:else} - {entry.value} - {/if} - -
    • - {/each} -
    -
    diff --git a/Foxnouns.Frontend/src/lib/components/profile/field/ProfileFieldEntry.svelte b/Foxnouns.Frontend/src/lib/components/profile/field/ProfileFieldEntry.svelte deleted file mode 100644 index 5773757..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/field/ProfileFieldEntry.svelte +++ /dev/null @@ -1,28 +0,0 @@ - - - - - {@render children?.()} - diff --git a/Foxnouns.Frontend/src/lib/components/profile/field/PronounLink.svelte b/Foxnouns.Frontend/src/lib/components/profile/field/PronounLink.svelte deleted file mode 100644 index cae11ec..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/field/PronounLink.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -{#if shouldLink} - {pronounText} -{:else} - {pronounText} -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte b/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte deleted file mode 100644 index 765dda7..0000000 --- a/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - -
    - - - -

    - - {member.display_name} - - {#if pronouns} -
    - {pronouns} - {/if} -

    -
    diff --git a/Foxnouns.Frontend/src/lib/components/settings/AuthMethodList.svelte b/Foxnouns.Frontend/src/lib/components/settings/AuthMethodList.svelte deleted file mode 100644 index e889df2..0000000 --- a/Foxnouns.Frontend/src/lib/components/settings/AuthMethodList.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -{#if methods.length > 0} -
    - {#each methods as method (method.id)} - - {/each} -
    -{/if} -{#if methods.length < max} - {buttonText} -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte b/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte deleted file mode 100644 index f1c7964..0000000 --- a/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - -
    -
    -
    - {#if showType} - {method.type}: - {/if} - {name} - {#if showId}({method.remote_id}){/if} -
    - {#if canRemove} - - {/if} -
    -
    diff --git a/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte b/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte deleted file mode 100644 index 4bd3318..0000000 --- a/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte +++ /dev/null @@ -1,73 +0,0 @@ - - -

    {$t("auth.email-password-title")}

    - -{#if emails.length > 0} -
    -
    -

    Your email addresses

    -
    - {#each emails as method (method.id)} - - {/each} - {#if emails.length < max} - - {$t("auth.add-email-address")} - - {/if} -
    -
    -
    - -

    Change password

    -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    -
    -{:else} -

    {$t("auth.no-email-addresses")}

    -

    - - - {$t("auth.add-email-address")} - -

    -{/if} diff --git a/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte b/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte deleted file mode 100644 index 73405bc..0000000 --- a/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte +++ /dev/null @@ -1,36 +0,0 @@ - - -

    {$t("auth.new-auth-method-added")}

    - -

    {text} {name}

    -

    {$t("auth.successful-link-profile-hint")}

    -

    - {$t("auth.successful-link-profile-link")} -

    diff --git a/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte b/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte deleted file mode 100644 index 796d60a..0000000 --- a/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte +++ /dev/null @@ -1,43 +0,0 @@ - - -
    -
    -
    - -
    -

    - {#if notification.localization_key} - {$t(notification.localization_key, notification.localization_params)} - {:else} - {notification.message} - {/if} -

    -
    -
    -
    - -
    diff --git a/Foxnouns.Frontend/src/lib/components/settings/OauthRegistrationForm.svelte b/Foxnouns.Frontend/src/lib/components/settings/OauthRegistrationForm.svelte deleted file mode 100644 index cae5efa..0000000 --- a/Foxnouns.Frontend/src/lib/components/settings/OauthRegistrationForm.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - -

    {title}

    - -{#if error} - -{/if} - -
    -
    - - -
    -
    - - -
    - - -
    diff --git a/Foxnouns.Frontend/src/lib/defaultPronouns/en.ts b/Foxnouns.Frontend/src/lib/defaultPronouns/en.ts deleted file mode 100644 index 412689a..0000000 --- a/Foxnouns.Frontend/src/lib/defaultPronouns/en.ts +++ /dev/null @@ -1,16 +0,0 @@ -const enPronouns = { - "they/them": { pronouns: ["they", "them", "their", "theirs", "themself"] }, - "they/them (singular)": { - pronouns: ["they", "them", "their", "theirs", "themself"], - display: "they/them (singular)", - }, - "they/them (plural)": { - pronouns: ["they", "them", "their", "theirs", "themselves"], - display: "they/them (plural)", - }, - "he/him": { pronouns: ["he", "him", "his", "his", "himself"] }, - "she/her": { pronouns: ["she", "her", "her", "hers", "herself"] }, - "it/its": { pronouns: ["it", "it", "its", "its", "itself"], display: "it/its" }, -} as Record; - -export default enPronouns; diff --git a/Foxnouns.Frontend/src/lib/defaultPronouns/index.ts b/Foxnouns.Frontend/src/lib/defaultPronouns/index.ts deleted file mode 100644 index e60d38b..0000000 --- a/Foxnouns.Frontend/src/lib/defaultPronouns/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import enPronouns from "./en"; - -const defaultPronouns = { - en: enPronouns, -} as Record>; - -export default defaultPronouns; diff --git a/Foxnouns.Frontend/src/lib/errorCodes.ts b/Foxnouns.Frontend/src/lib/errorCodes.ts deleted file mode 100644 index 8b8ef44..0000000 --- a/Foxnouns.Frontend/src/lib/errorCodes.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ErrorCode } from "$api/error"; -import type { Modifier } from "sveltekit-i18n"; - -// eslint-disable-next-line -type TranslateFn = (key: string, payload?: any, props?: Modifier.Props<{}> | undefined) => any; - -export default function errorDescription(t: TranslateFn, code: ErrorCode): string { - switch (code) { - case ErrorCode.InternalServerError: - return t("error.internal-server-error"); - case ErrorCode.Forbidden: - return t("error.forbidden"); - case ErrorCode.BadRequest: - return t("error.bad-request"); - case ErrorCode.AuthenticationError: - return t("error.authentication-error"); - case ErrorCode.AuthenticationRequired: - return t("error.authentication-required"); - case ErrorCode.MissingScopes: - // This error should never be returned by site tokens, so ask the user if they messed with their cookies - return t("error.missing-scopes"); - case ErrorCode.GenericApiError: - return t("error.generic-error"); - case ErrorCode.UserNotFound: - return t("error.user-not-found"); - case ErrorCode.MemberNotFound: - return t("error.member-not-found"); - case ErrorCode.AccountAlreadyLinked: - return t("error.account-already-linked"); - case ErrorCode.LastAuthMethod: - return t("error.last-auth-method"); - case ErrorCode.PageNotFound: - return t("error.page-not-found"); - case ErrorCode.Non204Response: - return t("error.generic-error"); - } - - return t("error.generic-error"); -} diff --git a/Foxnouns.Frontend/src/lib/i18n/index.ts b/Foxnouns.Frontend/src/lib/i18n/index.ts deleted file mode 100644 index 27a9603..0000000 --- a/Foxnouns.Frontend/src/lib/i18n/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { PUBLIC_LANGUAGE } from "$env/static/public"; -import i18n, { type Config } from "sveltekit-i18n"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const config: Config = { - initLocale: PUBLIC_LANGUAGE, - fallbackLocale: "en", - loaders: [ - { - locale: "en", - key: "", - loader: async () => (await import("./locales/en.json")).default, - }, - { - locale: "en-PR", - key: "", - loader: async () => (await import("./locales/en-PR.json")).default, - }, - ], -}; - -export const { t, locales, locale, translations, loadTranslations, setLocale } = new i18n(config); - -loadTranslations(PUBLIC_LANGUAGE); -setLocale(PUBLIC_LANGUAGE); diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en-PR.json b/Foxnouns.Frontend/src/lib/i18n/locales/en-PR.json deleted file mode 100644 index 2cba2a5..0000000 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en-PR.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "hello": "Ahoy, {{name}}!", - "nav": { - "log-in": "Report for duty", - "settings": "Pref'rences" - }, - "avatar-tooltip": "Mugshot for {{name}}", - "profile": { - "edit-member-profile-notice": "You be viewin' the public persona of {memberName}.", - "edit-user-profile-notice": "You be viewin' yer public persona.", - "edit-profile-link": "Edit persona", - "names-header": "Names", - "pronouns-header": "Pronouns", - "default-members-header": "Members", - "create-member-button": "Create member" - }, - "title": { - "log-in": "Report for duty", - "welcome": "Ahoy" - }, - "auth": { - "log-in-form-title": "Use a message in a bottle", - "log-in-form-email-label": "Address", - "log-in-form-password-label": "Secret phrase", - "register-with-email-button": "Sign up", - "log-in-button": "Report for duty" - } -} diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json deleted file mode 100644 index 22428f7..0000000 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ /dev/null @@ -1,358 +0,0 @@ -{ - "hello": "Hello, {{name}}!", - "nav": { - "log-in": "Log in or sign up", - "settings": "Settings", - "suspended-account-hint": "Your account has been suspended. Your profile has been hidden and you will not be able to change any settings.", - "appeal-suspension-link": "I want to appeal", - "deleted-account-hint": "You have requested deletion of your account.", - "reactivate-account-link": "Reactivate account", - "delete-permanently-link": "I want my account deleted permanently", - "reactivate-or-delete-link": "I want to reactivate my account or delete all my data", - "export-link": "I want to export a copy of my data", - "unread-notification-text": "You have an unread notification.", - "unread-notification-link": "Go to your notifications" - }, - "avatar-tooltip": "Avatar for {{name}}", - "profile": { - "edit-member-profile-notice": "You are currently viewing the public profile of {{memberName}}.", - "edit-user-profile-notice": "You are currently viewing your public profile.", - "edit-profile-link": "Edit profile", - "names-header": "Names", - "pronouns-header": "Pronouns", - "default-members-header": "Members", - "create-member-button": "Create member", - "back-to-user": "Back to {{name}}", - "copy-link-button": "Copy link", - "copy-short-link-button": "Copy short link", - "report-button": "Report profile" - }, - "title": { - "log-in": "Log in", - "welcome": "Welcome", - "settings": "Settings", - "an-error-occurred": "An error occurred" - }, - "auth": { - "log-in-form-title": "Log in with email", - "log-in-form-email-label": "Email address", - "log-in-form-password-label": "Password", - "register-with-email-button": "Register with email", - "log-in-button": "Log in", - "log-in-3rd-party-header": "Log in with another service", - "log-in-3rd-party-desc": "If you prefer, you can also log in with one of these services:", - "log-in-with-discord": "Log in with Discord", - "log-in-with-google": "Log in with Google", - "log-in-with-tumblr": "Log in with Tumblr", - "log-in-with-the-fediverse": "Log in with the Fediverse", - "remote-fediverse-account-label": "Your Fediverse account", - "register-username-label": "Username", - "register-button": "Register account", - "register-with-fediverse": "Register with a Fediverse account", - "log-in-with-fediverse-error-blurb": "Is your instance returning an error?", - "log-in-with-fediverse-force-refresh-button": "Force a refresh on our end", - "register-with-discord": "Register with a Discord account", - "new-auth-method-added": "Successfully added authentication method!", - "successful-link-discord": "Your account has successfully been linked to the following Discord account:", - "successful-link-google": "Your account has successfully been linked to the following Google account:", - "successful-link-tumblr": "Your account has successfully been linked to the following Tumblr account:", - "successful-link-fedi": "Your account has successfully been linked to the following fediverse account:", - "successful-link-profile-hint": "You now can close this page, or go back to your profile:", - "successful-link-profile-link": "Go to your profile", - "remote-discord-account-label": "Your Discord account", - "log-in-with-fediverse-instance-placeholder": "Your instance (i.e. mastodon.social)", - "register-with-email": "Register with an email address", - "email-label": "Your email address", - "confirm-password-label": "Confirm password", - "register-with-email-init-success": "Success! An email has been sent to your inbox, please press the link there to continue.", - "register-with-google": "Register with a Google account", - "remote-google-account-label": "Your Google account", - "register-with-tumblr": "Register with a Tumblr account", - "remote-tumblr-account-label": "Your Tumblr account", - "email-password-title": "Email and password", - "add-email-address": "Add email address", - "no-email-addresses": "You haven't linked any email addresses yet.", - "check-inbox-for-link-hint": "Check your inbox for a link!", - "successful-link-email": "Your account has successfully been linked to the following email address:", - "reset-password-button": "Reset password", - "log-in-forgot-password-link": "Forgot your password?", - "log-in-sign-up-link": "Sign up with email", - "forgot-password-title": "Forgot password", - "reset-password-title": "Reset password", - "password-changed-hint": "Your password has been changed!", - "link-email-header": "Link a new email address", - "unlink-email-header": "Unlink email address", - "unlink-fediverse-header": "Unlink fediverse account", - "unlink-tumblr-header": "Unlink Tumblr account", - "unlink-google-header": "Unlink Google account", - "unlink-discord-header": "Unlink Discord account", - "unlink-confirmation-1": "Are you sure you want to unlink {{username}} from your account?", - "unlink-confirmation-2": "You will no longer be able to use this account to log in. Please make sure at least one of your other linked accounts is accessible before continuing.", - "unlink-button": "Unlink account", - "log-in-3rd-party-desc-no-email": "You can use any of the following services to log in. You can add or remove others at any time." - }, - "error": { - "bad-request-header": "Something was wrong with your input", - "generic-header": "Something went wrong", - "raw-header": "Raw error", - "authentication-error": "Something went wrong when logging you in.", - "bad-request": "Your input was rejected by the server, please check for any mistakes and try again.", - "forbidden": "You are not allowed to perform that action.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "authentication-required": "You need to log in first.", - "missing-scopes": "The current token is missing a required scope. Did you manually edit your cookies?", - "generic-error": "An unknown error occurred.", - "user-not-found": "User not found, please check your spelling and try again. Remember that usernames are case sensitive.", - "member-not-found": "Member not found, please check your spelling and try again.", - "account-already-linked": "This account is already linked with a pronouns.cc account.", - "last-auth-method": "You cannot remove your last authentication method.", - "validation-max-length-error": "Value is too long, maximum length is {{max}}, current length is {{actual}}.", - "validation-min-length-error": "Value is too short, minimum length is {{min}}, current length is {{actual}}.", - "validation-disallowed-value-1": "The following value is not allowed here", - "validation-disallowed-value-2": "Allowed values are", - "validation-reason": "Reason", - "validation-generic": "The value you entered is not allowed here. Reason", - "extra-info-header": "Extra error information", - "noscript-title": "This page requires JavaScript", - "noscript-info": "This page requires JavaScript to function correctly. Some buttons may not work, or the page may not work at all.", - "noscript-short": "Requires JavaScript", - "404-description": "The page you were trying to visit was not found. If you're sure the page should exist, check your address bar for any typos.", - "back-to-profile-button": "Go back to your profile", - "back-to-main-page-button": "Go back to the main page", - "back-to-prev-page-button": "Go back to the previous page", - "400-description": "Something went wrong with your request. This error should never land you on this page, so it's probably a bug.", - "500-description": "Something went wrong on the server. Please try again later.", - "unknown-status-description": "Something went wrong, but we're not sure what. Please try again.", - "error-id": "If you report this error to the developers, please give them this ID:", - "page-not-found": "No page exists at this URL.", - "not-found-header": "That page could not be found" - }, - "settings": { - "general-information-tab": "General information", - "your-profile-tab": "Your profile", - "members-tab": "Members", - "authentication-tab": "Authentication", - "export-tab": "Export your data", - "change-username-button": "Change username", - "username-change-hint": "Changing your username will make any existing links to your or your members' profiles invalid.\nYour username must be unique, be at most 40 characters long, and only contain letters from the basic English alphabet, dashes, underscores, and periods. Your username is used as part of your profile link, you can set a separate display name.", - "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}", - "change-avatar-link": "Change your avatar here", - "new-username": "New username", - "table-role": "Role", - "table-custom-preferences": "Custom preferences", - "table-member-list-hidden": "Member list hidden?", - "table-member-count": "Member count", - "table-created-at": "Account created at", - "table-id": "Your ID", - "table-title": "Account information", - "force-log-out-title": "Log out everywhere", - "force-log-out-button": "Force log out", - "force-log-out-hint": "If you think one of your tokens might have been compromised, you can log out on all devices by clicking this button.", - "log-out-title": "Log out", - "log-out-hint": "Use this button to log out on this device only.", - "log-out-button": "Log out", - "avatar": "Avatar", - "username-update-success": "Successfully changed your username!", - "create-member-title": "Create a new member", - "create-member-name-label": "Member name", - "auth-remove-method": "Remove", - "force-log-out-warning": "Make sure you're still able to log in before using this!", - "force-log-out-confirmation": "Are you sure you want to log out from all devices? If you just want to log out from this device, click the \"Log out\" button on your settings page.", - "export-request-success": "Successfully requested a new export! Please note that it may take a few minutes to complete, especially if you have a lot of members.", - "export-title": "Request a copy of your data", - "export-info": "You can request a copy of your data once every 24 hours. Exports are stored for 15 days (a little over two weeks) and then deleted.", - "export-expires-at": "(expires {{expiresAt}})", - "export-download": "Download export", - "export-request-button": "Request a new export", - "flag-delete-button": "Delete flag", - "flag-current-flags-title": "Current flags ({{count}}/{{max}})", - "flag-title": "Flags", - "flag-upload-title": "Upload a new flag", - "flag-upload-button": "Upload", - "flag-description-placeholder": "Description", - "flag-name-placeholder": "Name", - "flag-upload-success": "Successfully uploaded your flag! It may take a few seconds before it's saved.", - "custom-preferences-title": "Custom preferences", - "change-username-header": "Change your username", - "force-delete-button": "Delete my account permanently", - "force-delete-warning": "This is irreversible. Consider exporting a copy of your data before doing this.", - "force-delete-explanation": "Your account is currently pending deletion. If you want your data deleted permanently, use the button below.", - "reactivate-explanation": "Your account is currently pending deletion. If you want to cancel this and keep using your account, use the link below.", - "reactivate-header": "Reactivate your account", - "force-delete-header": "Permanently delete your account", - "reactivate-button": "Reactivate my account", - "reactivated-header": "Account reactivated", - "reactivated-explanation": "Your account has been reactivated!", - "force-delete-input-label": "To delete your account, type your username (@{{username}}), including the @, in the box below:", - "force-delete-export-hint": "If you haven't done so yet, we recommend you download an export of your data before continuing:", - "force-delete-export-link": "export your data", - "force-delete-irreversible": "This process is irreversible.", - "force-delete-username-available": "Your username will immediately be available for other users to take.", - "force-delete-immediate-delete": "This will immediately delete all of your profiles, including avatars.", - "force-delete-page-explanation": "Your account is currently pending deletion. If you want all your data deleted immediately, you can do so here.", - "force-delete-page-header": "Permanently delete your account", - "force-delete-checkbox-label": "Yes, I understand that my data will be permanently deleted and cannot be recovered.", - "force-delete-page-button": "Delete my account", - "account-is-deleted-header": "Your account has been deleted", - "account-is-deleted-permanently-description": "Your account has been deleted. Note that it may take a few minutes for all of your data to be removed.", - "account-is-deleted-close-page": "You may now close this page.", - "soft-delete-button": "Deactivate your account", - "soft-delete-hint": "If you want to delete your account, use the button below.", - "soft-delete-header": "Deactivate your account", - "force-delete-page-cancel": "I changed my mind, cancel", - "soft-delete-page-header": "Deactivate your account", - "soft-delete-page-explanation": "If you want to delete your account, you can do so here.", - "soft-delete-90-days": "Your account will be permanently deleted after 90 days.", - "soft-delete-can-reactivate": "If you change your mind, you can log in and go to the settings page at any time to reactivate your account.", - "soft-delete-keep-username": "You will keep your current username until your account is permanently deleted.", - "soft-delete-can-delete-permanently": "If you want to delete all your data early, you can do so by logging in and going to the settings page.", - "soft-delete-page-button": "Deactivate my account", - "soft-delete-input-label": "To deactivate your account, type your username (@{{username}}), including the @, in the box below:", - "account-is-deactivated-header": "Your account has been deactivated", - "account-is-deactivated-description": "Your account has been deactivated, and will be deleted in 90 days. If you change your mind, just log in again, and you will have the option to reactivate your account. If you want to delete your data immediately, you should also log in again, and you will be able to request immediate deletion." - }, - "yes": "Yes", - "no": "No", - "edit-profile": { - "user-header": "Editing your profile", - "general-tab": "General", - "names-pronouns-tab": "Names & pronouns", - "file-too-large": "This file is too large, please resize it (maximum is {{max}}, the file you're trying to upload is {{current}})", - "sid-current": "Current short ID:", - "sid": "Short ID", - "sid-reroll": "Reroll short ID", - "sid-hint": "This ID is used in prns.cc links. You can reroll one short ID every hour (shared between your main profile and all members) by pressing the button above.", - "sid-copy": "Copy short link", - "update-avatar": "Update avatar", - "avatar-updated": "Avatar updated! It might take a moment to be reflected on your profile.", - "member-header-label": "\"Members\" header text", - "member-header-info": "This is the text used for the \"Members\" heading. If you leave it blank, the default text will be used.", - "hide-member-list-label": "Hide member list", - "timezone-label": "Timezone", - "timezone-preview": "This will show up on your profile like this:", - "timezone-info": "This is optional. Your timezone is never shared directly, only the difference between UTC and your current timezone is.", - "hide-member-list-info": "This only hides your member list. Individual members will still be visible to anyone with a direct link to their pages.", - "profile-options-header": "Profile options", - "bio-tab": "Bio", - "saved-changes": "Successfully saved changes!", - "bio-length-hint": "Using {{length}}/{{maxLength}} characters", - "preview": "Preview", - "fields-tab": "Fields", - "flags-links-tab": "Flags & links", - "back-to-settings-tab": "Back to settings", - "member-header": "Editing profile of {{name}}", - "username": "Username", - "change-username-info": "As changing your username will also change all of your members' links, you can only change it in your account settings.", - "change-username-link": "Go to settings", - "member-name": "Name", - "change-member-name": "Change name", - "display-name": "Display name", - "unlisted-label": "Hide from member list", - "unlisted-note": "This only hides this member from your public member list. They will still be visible to anyone at this link:", - "edit-names-pronouns-header": "Edit names and pronouns", - "back-to-profile-tab": "Back to profile", - "editing-fields-header": "Editing fields" - }, - "save-changes": "Save changes", - "change": "Change", - "editor": { - "remove-entry": "Remove entry", - "move-entry-down": "Move entry down", - "move-entry-up": "Move entry up", - "add-entry": "Add entry", - "change-display-text": "Change display text", - "display-text-example": "Optional display text (e.g. it/its)", - "display-text-label": "Display text", - "display-text-info": "This is the short text shown on your profile page. If you leave it empty, it will default to the first two forms of the full set.", - "move-field-up": "Move field up", - "move-field-down": "Move field down", - "remove-field": "Remove field", - "field-name": "Field name", - "add-field": "Add field", - "new-entry": "New entry", - "add-this-flag": "Add this flag", - "add-flags-header": "Add flags", - "move-flag-up": "Move flag up", - "move-flag-down": "Move flag down", - "remove-this-flag": "Remove this flag", - "no-flags-hint": "This profile doesn't have any flags yet! Add some with the search box.", - "flag-search-placeholder": "Type to start searching", - "flag-search-no-flags": "No flags matched your search query.", - "flag-search-no-account-flags": "You haven't uploaded any flags yet.", - "flag-search-hint": "Can't find the flag you're looking for? Try using the search bar above.", - "flag-manage-your-flags": "Manage your flags", - "links-header": "Links", - "icons-stop-showing-all": "Stop showing all results", - "icons-show-all": "Show all results ({{count}})", - "icons-change-icon": "Change icon", - "tooltip-hint": "Tooltip", - "add-custom-preference": "Add", - "custom-preference-size-large": "Large", - "custom-preference-size-medium": "Medium", - "custom-preference-size-small": "Small", - "custom-preference-size": "Text size", - "custom-preference-muted": "Show as muted text", - "custom-preference-favourite": "Treat like favourite", - "custom-preference-notice": "Want to edit your custom preferences?", - "custom-preference-notice-link": "Go to settings", - "crop-avatar-header": "Crop avatar", - "crop-avatar-button": "Crop", - "max-custom-preferences": "You have reached the maximum amount of custom preferences ({{current}}/{{max}}), and cannot add new ones." - }, - "cancel": "Cancel", - "report": { - "title": "Reporting {{name}}", - "totalitarianism": "Support of totalitarian regimes", - "hate-speech": "Hate speech", - "racism": "Racism or xenophobia", - "homophobia": "Homophobia", - "transphobia": "Transphobia", - "queerphobia": "Queerphobia (other)", - "exclusionism": "Queer or plural exclusionism", - "sexism": "Sexism or misogyny", - "ableism": "Ableism", - "child-pornography": "Child pornography", - "pedophilia-advocacy": "Pedophilia advocacy", - "harassment": "Harassment", - "impersonation": "Impersonation", - "doxxing": "Doxxing", - "encouraging-self-harm": "Encouraging self-harm or suicide", - "spam": "Spam", - "trolling": "Trolling", - "advertisement": "Advertising", - "copyright-violation": "Copyright or trademark violation", - "success": "Successfully submitted report!", - "reason-label": "Why are you reporting this profile?", - "context-label": "Is there any context you'd like to give us?", - "submit-button": "Submit report" - }, - "form": { - "optional": "(optional)", - "required": "Required" - }, - "alert": { - "auth-method-remove-success": "Successfully unlinked account!", - "auth-required": "You must log in to access this page.", - "notif-ack-successful": "Successfully marked notification as read!", - "notif-ack-fail": "Could not mark notification as read." - }, - "footer": { - "version": "Version", - "users": "users", - "members": "members", - "source": "Source code", - "status": "Status", - "terms": "Terms of service", - "privacy": "Privacy policy", - "changelog": "Changelog", - "donate": "Donate", - "about-contact": "About and contact" - }, - "notification": { - "suspension": "Your account has been suspended for the following reason: {{reason}}", - "warning": "You have been warned for the following reason: {{reason}}", - "warning-cleared-fields": "You have been warned for the following reason: {{reason}}\n\nAdditionally, the following fields have been cleared from your profile:\n{{clearedFields}}", - "mark-as-read": "Mark as read", - "no-notifications": "You have no notifications." - } -} diff --git a/Foxnouns.Frontend/src/lib/icons.ts b/Foxnouns.Frontend/src/lib/icons.ts deleted file mode 100644 index daf700e..0000000 --- a/Foxnouns.Frontend/src/lib/icons.ts +++ /dev/null @@ -1,4 +0,0 @@ -// Generated code: DO NOT EDIT - -const icons = ["123","alarm-fill","alarm","align-bottom","align-center","align-end","align-middle","align-start","align-top","alt","app-indicator","app","archive-fill","archive","arrow-90deg-down","arrow-90deg-left","arrow-90deg-right","arrow-90deg-up","arrow-bar-down","arrow-bar-left","arrow-bar-right","arrow-bar-up","arrow-clockwise","arrow-counterclockwise","arrow-down-circle-fill","arrow-down-circle","arrow-down-left-circle-fill","arrow-down-left-circle","arrow-down-left-square-fill","arrow-down-left-square","arrow-down-left","arrow-down-right-circle-fill","arrow-down-right-circle","arrow-down-right-square-fill","arrow-down-right-square","arrow-down-right","arrow-down-short","arrow-down-square-fill","arrow-down-square","arrow-down-up","arrow-down","arrow-left-circle-fill","arrow-left-circle","arrow-left-right","arrow-left-short","arrow-left-square-fill","arrow-left-square","arrow-left","arrow-repeat","arrow-return-left","arrow-return-right","arrow-right-circle-fill","arrow-right-circle","arrow-right-short","arrow-right-square-fill","arrow-right-square","arrow-right","arrow-up-circle-fill","arrow-up-circle","arrow-up-left-circle-fill","arrow-up-left-circle","arrow-up-left-square-fill","arrow-up-left-square","arrow-up-left","arrow-up-right-circle-fill","arrow-up-right-circle","arrow-up-right-square-fill","arrow-up-right-square","arrow-up-right","arrow-up-short","arrow-up-square-fill","arrow-up-square","arrow-up","arrows-angle-contract","arrows-angle-expand","arrows-collapse","arrows-expand","arrows-fullscreen","arrows-move","aspect-ratio-fill","aspect-ratio","asterisk","at","award-fill","award","back","backspace-fill","backspace-reverse-fill","backspace-reverse","backspace","badge-3d-fill","badge-3d","badge-4k-fill","badge-4k","badge-8k-fill","badge-8k","badge-ad-fill","badge-ad","badge-ar-fill","badge-ar","badge-cc-fill","badge-cc","badge-hd-fill","badge-hd","badge-tm-fill","badge-tm","badge-vo-fill","badge-vo","badge-vr-fill","badge-vr","badge-wc-fill","badge-wc","bag-check-fill","bag-check","bag-dash-fill","bag-dash","bag-fill","bag-plus-fill","bag-plus","bag-x-fill","bag-x","bag","bar-chart-fill","bar-chart-line-fill","bar-chart-line","bar-chart-steps","bar-chart","basket-fill","basket","basket2-fill","basket2","basket3-fill","basket3","battery-charging","battery-full","battery-half","battery","bell-fill","bell","bezier","bezier2","bicycle","binoculars-fill","binoculars","blockquote-left","blockquote-right","book-fill","book-half","book","bookmark-check-fill","bookmark-check","bookmark-dash-fill","bookmark-dash","bookmark-fill","bookmark-heart-fill","bookmark-heart","bookmark-plus-fill","bookmark-plus","bookmark-star-fill","bookmark-star","bookmark-x-fill","bookmark-x","bookmark","bookmarks-fill","bookmarks","bookshelf","bootstrap-fill","bootstrap-reboot","bootstrap","border-all","border-bottom","border-center","border-inner","border-left","border-middle","border-outer","border-right","border-style","border-top","border-width","border","bounding-box-circles","bounding-box","box-arrow-down-left","box-arrow-down-right","box-arrow-down","box-arrow-in-down-left","box-arrow-in-down-right","box-arrow-in-down","box-arrow-in-left","box-arrow-in-right","box-arrow-in-up-left","box-arrow-in-up-right","box-arrow-in-up","box-arrow-left","box-arrow-right","box-arrow-up-left","box-arrow-up-right","box-arrow-up","box-seam","box","braces","bricks","briefcase-fill","briefcase","brightness-alt-high-fill","brightness-alt-high","brightness-alt-low-fill","brightness-alt-low","brightness-high-fill","brightness-high","brightness-low-fill","brightness-low","broadcast-pin","broadcast","brush-fill","brush","bucket-fill","bucket","bug-fill","bug","building","bullseye","calculator-fill","calculator","calendar-check-fill","calendar-check","calendar-date-fill","calendar-date","calendar-day-fill","calendar-day","calendar-event-fill","calendar-event","calendar-fill","calendar-minus-fill","calendar-minus","calendar-month-fill","calendar-month","calendar-plus-fill","calendar-plus","calendar-range-fill","calendar-range","calendar-week-fill","calendar-week","calendar-x-fill","calendar-x","calendar","calendar2-check-fill","calendar2-check","calendar2-date-fill","calendar2-date","calendar2-day-fill","calendar2-day","calendar2-event-fill","calendar2-event","calendar2-fill","calendar2-minus-fill","calendar2-minus","calendar2-month-fill","calendar2-month","calendar2-plus-fill","calendar2-plus","calendar2-range-fill","calendar2-range","calendar2-week-fill","calendar2-week","calendar2-x-fill","calendar2-x","calendar2","calendar3-event-fill","calendar3-event","calendar3-fill","calendar3-range-fill","calendar3-range","calendar3-week-fill","calendar3-week","calendar3","calendar4-event","calendar4-range","calendar4-week","calendar4","camera-fill","camera-reels-fill","camera-reels","camera-video-fill","camera-video-off-fill","camera-video-off","camera-video","camera","camera2","capslock-fill","capslock","card-checklist","card-heading","card-image","card-list","card-text","caret-down-fill","caret-down-square-fill","caret-down-square","caret-down","caret-left-fill","caret-left-square-fill","caret-left-square","caret-left","caret-right-fill","caret-right-square-fill","caret-right-square","caret-right","caret-up-fill","caret-up-square-fill","caret-up-square","caret-up","cart-check-fill","cart-check","cart-dash-fill","cart-dash","cart-fill","cart-plus-fill","cart-plus","cart-x-fill","cart-x","cart","cart2","cart3","cart4","cash-stack","cash","cast","chat-dots-fill","chat-dots","chat-fill","chat-left-dots-fill","chat-left-dots","chat-left-fill","chat-left-quote-fill","chat-left-quote","chat-left-text-fill","chat-left-text","chat-left","chat-quote-fill","chat-quote","chat-right-dots-fill","chat-right-dots","chat-right-fill","chat-right-quote-fill","chat-right-quote","chat-right-text-fill","chat-right-text","chat-right","chat-square-dots-fill","chat-square-dots","chat-square-fill","chat-square-quote-fill","chat-square-quote","chat-square-text-fill","chat-square-text","chat-square","chat-text-fill","chat-text","chat","check-all","check-circle-fill","check-circle","check-square-fill","check-square","check","check2-all","check2-circle","check2-square","check2","chevron-bar-contract","chevron-bar-down","chevron-bar-expand","chevron-bar-left","chevron-bar-right","chevron-bar-up","chevron-compact-down","chevron-compact-left","chevron-compact-right","chevron-compact-up","chevron-contract","chevron-double-down","chevron-double-left","chevron-double-right","chevron-double-up","chevron-down","chevron-expand","chevron-left","chevron-right","chevron-up","circle-fill","circle-half","circle-square","circle","clipboard-check","clipboard-data","clipboard-minus","clipboard-plus","clipboard-x","clipboard","clock-fill","clock-history","clock","cloud-arrow-down-fill","cloud-arrow-down","cloud-arrow-up-fill","cloud-arrow-up","cloud-check-fill","cloud-check","cloud-download-fill","cloud-download","cloud-drizzle-fill","cloud-drizzle","cloud-fill","cloud-fog-fill","cloud-fog","cloud-fog2-fill","cloud-fog2","cloud-hail-fill","cloud-hail","cloud-haze-fill","cloud-haze","cloud-haze2-fill","cloud-lightning-fill","cloud-lightning-rain-fill","cloud-lightning-rain","cloud-lightning","cloud-minus-fill","cloud-minus","cloud-moon-fill","cloud-moon","cloud-plus-fill","cloud-plus","cloud-rain-fill","cloud-rain-heavy-fill","cloud-rain-heavy","cloud-rain","cloud-slash-fill","cloud-slash","cloud-sleet-fill","cloud-sleet","cloud-snow-fill","cloud-snow","cloud-sun-fill","cloud-sun","cloud-upload-fill","cloud-upload","cloud","clouds-fill","clouds","cloudy-fill","cloudy","code-slash","code-square","code","collection-fill","collection-play-fill","collection-play","collection","columns-gap","columns","command","compass-fill","compass","cone-striped","cone","controller","cpu-fill","cpu","credit-card-2-back-fill","credit-card-2-back","credit-card-2-front-fill","credit-card-2-front","credit-card-fill","credit-card","crop","cup-fill","cup-straw","cup","cursor-fill","cursor-text","cursor","dash-circle-dotted","dash-circle-fill","dash-circle","dash-square-dotted","dash-square-fill","dash-square","dash","diagram-2-fill","diagram-2","diagram-3-fill","diagram-3","diamond-fill","diamond-half","diamond","dice-1-fill","dice-1","dice-2-fill","dice-2","dice-3-fill","dice-3","dice-4-fill","dice-4","dice-5-fill","dice-5","dice-6-fill","dice-6","disc-fill","disc","discord","display-fill","display","distribute-horizontal","distribute-vertical","door-closed-fill","door-closed","door-open-fill","door-open","dot","download","droplet-fill","droplet-half","droplet","earbuds","easel-fill","easel","egg-fill","egg-fried","egg","eject-fill","eject","emoji-angry-fill","emoji-angry","emoji-dizzy-fill","emoji-dizzy","emoji-expressionless-fill","emoji-expressionless","emoji-frown-fill","emoji-frown","emoji-heart-eyes-fill","emoji-heart-eyes","emoji-laughing-fill","emoji-laughing","emoji-neutral-fill","emoji-neutral","emoji-smile-fill","emoji-smile-upside-down-fill","emoji-smile-upside-down","emoji-smile","emoji-sunglasses-fill","emoji-sunglasses","emoji-wink-fill","emoji-wink","envelope-fill","envelope-open-fill","envelope-open","envelope","eraser-fill","eraser","exclamation-circle-fill","exclamation-circle","exclamation-diamond-fill","exclamation-diamond","exclamation-octagon-fill","exclamation-octagon","exclamation-square-fill","exclamation-square","exclamation-triangle-fill","exclamation-triangle","exclamation","exclude","eye-fill","eye-slash-fill","eye-slash","eye","eyedropper","eyeglasses","facebook","file-arrow-down-fill","file-arrow-down","file-arrow-up-fill","file-arrow-up","file-bar-graph-fill","file-bar-graph","file-binary-fill","file-binary","file-break-fill","file-break","file-check-fill","file-check","file-code-fill","file-code","file-diff-fill","file-diff","file-earmark-arrow-down-fill","file-earmark-arrow-down","file-earmark-arrow-up-fill","file-earmark-arrow-up","file-earmark-bar-graph-fill","file-earmark-bar-graph","file-earmark-binary-fill","file-earmark-binary","file-earmark-break-fill","file-earmark-break","file-earmark-check-fill","file-earmark-check","file-earmark-code-fill","file-earmark-code","file-earmark-diff-fill","file-earmark-diff","file-earmark-easel-fill","file-earmark-easel","file-earmark-excel-fill","file-earmark-excel","file-earmark-fill","file-earmark-font-fill","file-earmark-font","file-earmark-image-fill","file-earmark-image","file-earmark-lock-fill","file-earmark-lock","file-earmark-lock2-fill","file-earmark-lock2","file-earmark-medical-fill","file-earmark-medical","file-earmark-minus-fill","file-earmark-minus","file-earmark-music-fill","file-earmark-music","file-earmark-person-fill","file-earmark-person","file-earmark-play-fill","file-earmark-play","file-earmark-plus-fill","file-earmark-plus","file-earmark-post-fill","file-earmark-post","file-earmark-ppt-fill","file-earmark-ppt","file-earmark-richtext-fill","file-earmark-richtext","file-earmark-ruled-fill","file-earmark-ruled","file-earmark-slides-fill","file-earmark-slides","file-earmark-spreadsheet-fill","file-earmark-spreadsheet","file-earmark-text-fill","file-earmark-text","file-earmark-word-fill","file-earmark-word","file-earmark-x-fill","file-earmark-x","file-earmark-zip-fill","file-earmark-zip","file-earmark","file-easel-fill","file-easel","file-excel-fill","file-excel","file-fill","file-font-fill","file-font","file-image-fill","file-image","file-lock-fill","file-lock","file-lock2-fill","file-lock2","file-medical-fill","file-medical","file-minus-fill","file-minus","file-music-fill","file-music","file-person-fill","file-person","file-play-fill","file-play","file-plus-fill","file-plus","file-post-fill","file-post","file-ppt-fill","file-ppt","file-richtext-fill","file-richtext","file-ruled-fill","file-ruled","file-slides-fill","file-slides","file-spreadsheet-fill","file-spreadsheet","file-text-fill","file-text","file-word-fill","file-word","file-x-fill","file-x","file-zip-fill","file-zip","file","files-alt","files","film","filter-circle-fill","filter-circle","filter-left","filter-right","filter-square-fill","filter-square","filter","flag-fill","flag","flower1","flower2","flower3","folder-check","folder-fill","folder-minus","folder-plus","folder-symlink-fill","folder-symlink","folder-x","folder","folder2-open","folder2","fonts","forward-fill","forward","front","fullscreen-exit","fullscreen","funnel-fill","funnel","gear-fill","gear-wide-connected","gear-wide","gear","gem","geo-alt-fill","geo-alt","geo-fill","geo","gift-fill","gift","github","globe","globe2","google","graph-down","graph-up","grid-1x2-fill","grid-1x2","grid-3x2-gap-fill","grid-3x2-gap","grid-3x2","grid-3x3-gap-fill","grid-3x3-gap","grid-3x3","grid-fill","grid","grip-horizontal","grip-vertical","hammer","hand-index-fill","hand-index-thumb-fill","hand-index-thumb","hand-index","hand-thumbs-down-fill","hand-thumbs-down","hand-thumbs-up-fill","hand-thumbs-up","handbag-fill","handbag","hash","hdd-fill","hdd-network-fill","hdd-network","hdd-rack-fill","hdd-rack","hdd-stack-fill","hdd-stack","hdd","headphones","headset","heart-fill","heart-half","heart","heptagon-fill","heptagon-half","heptagon","hexagon-fill","hexagon-half","hexagon","hourglass-bottom","hourglass-split","hourglass-top","hourglass","house-door-fill","house-door","house-fill","house","hr","hurricane","image-alt","image-fill","image","images","inbox-fill","inbox","inboxes-fill","inboxes","info-circle-fill","info-circle","info-square-fill","info-square","info","input-cursor-text","input-cursor","instagram","intersect","journal-album","journal-arrow-down","journal-arrow-up","journal-bookmark-fill","journal-bookmark","journal-check","journal-code","journal-medical","journal-minus","journal-plus","journal-richtext","journal-text","journal-x","journal","journals","joystick","justify-left","justify-right","justify","kanban-fill","kanban","key-fill","key","keyboard-fill","keyboard","ladder","lamp-fill","lamp","laptop-fill","laptop","layer-backward","layer-forward","layers-fill","layers-half","layers","layout-sidebar-inset-reverse","layout-sidebar-inset","layout-sidebar-reverse","layout-sidebar","layout-split","layout-text-sidebar-reverse","layout-text-sidebar","layout-text-window-reverse","layout-text-window","layout-three-columns","layout-wtf","life-preserver","lightbulb-fill","lightbulb-off-fill","lightbulb-off","lightbulb","lightning-charge-fill","lightning-charge","lightning-fill","lightning","link-45deg","link","linkedin","list-check","list-nested","list-ol","list-stars","list-task","list-ul","list","lock-fill","lock","mailbox","mailbox2","map-fill","map","markdown-fill","markdown","mask","megaphone-fill","megaphone","menu-app-fill","menu-app","menu-button-fill","menu-button-wide-fill","menu-button-wide","menu-button","menu-down","menu-up","mic-fill","mic-mute-fill","mic-mute","mic","minecart-loaded","minecart","moisture","moon-fill","moon-stars-fill","moon-stars","moon","mouse-fill","mouse","mouse2-fill","mouse2","mouse3-fill","mouse3","music-note-beamed","music-note-list","music-note","music-player-fill","music-player","newspaper","node-minus-fill","node-minus","node-plus-fill","node-plus","nut-fill","nut","octagon-fill","octagon-half","octagon","option","outlet","paint-bucket","palette-fill","palette","palette2","paperclip","paragraph","patch-check-fill","patch-check","patch-exclamation-fill","patch-exclamation","patch-minus-fill","patch-minus","patch-plus-fill","patch-plus","patch-question-fill","patch-question","pause-btn-fill","pause-btn","pause-circle-fill","pause-circle","pause-fill","pause","peace-fill","peace","pen-fill","pen","pencil-fill","pencil-square","pencil","pentagon-fill","pentagon-half","pentagon","people-fill","people","percent","person-badge-fill","person-badge","person-bounding-box","person-check-fill","person-check","person-circle","person-dash-fill","person-dash","person-fill","person-lines-fill","person-plus-fill","person-plus","person-square","person-x-fill","person-x","person","phone-fill","phone-landscape-fill","phone-landscape","phone-vibrate-fill","phone-vibrate","phone","pie-chart-fill","pie-chart","pin-angle-fill","pin-angle","pin-fill","pin","pip-fill","pip","play-btn-fill","play-btn","play-circle-fill","play-circle","play-fill","play","plug-fill","plug","plus-circle-dotted","plus-circle-fill","plus-circle","plus-square-dotted","plus-square-fill","plus-square","plus","power","printer-fill","printer","puzzle-fill","puzzle","question-circle-fill","question-circle","question-diamond-fill","question-diamond","question-octagon-fill","question-octagon","question-square-fill","question-square","question","rainbow","receipt-cutoff","receipt","reception-0","reception-1","reception-2","reception-3","reception-4","record-btn-fill","record-btn","record-circle-fill","record-circle","record-fill","record","record2-fill","record2","reply-all-fill","reply-all","reply-fill","reply","rss-fill","rss","rulers","save-fill","save","save2-fill","save2","scissors","screwdriver","search","segmented-nav","server","share-fill","share","shield-check","shield-exclamation","shield-fill-check","shield-fill-exclamation","shield-fill-minus","shield-fill-plus","shield-fill-x","shield-fill","shield-lock-fill","shield-lock","shield-minus","shield-plus","shield-shaded","shield-slash-fill","shield-slash","shield-x","shield","shift-fill","shift","shop-window","shop","shuffle","signpost-2-fill","signpost-2","signpost-fill","signpost-split-fill","signpost-split","signpost","sim-fill","sim","skip-backward-btn-fill","skip-backward-btn","skip-backward-circle-fill","skip-backward-circle","skip-backward-fill","skip-backward","skip-end-btn-fill","skip-end-btn","skip-end-circle-fill","skip-end-circle","skip-end-fill","skip-end","skip-forward-btn-fill","skip-forward-btn","skip-forward-circle-fill","skip-forward-circle","skip-forward-fill","skip-forward","skip-start-btn-fill","skip-start-btn","skip-start-circle-fill","skip-start-circle","skip-start-fill","skip-start","slack","slash-circle-fill","slash-circle","slash-square-fill","slash-square","slash","sliders","smartwatch","snow","snow2","snow3","sort-alpha-down-alt","sort-alpha-down","sort-alpha-up-alt","sort-alpha-up","sort-down-alt","sort-down","sort-numeric-down-alt","sort-numeric-down","sort-numeric-up-alt","sort-numeric-up","sort-up-alt","sort-up","soundwave","speaker-fill","speaker","speedometer","speedometer2","spellcheck","square-fill","square-half","square","stack","star-fill","star-half","star","stars","stickies-fill","stickies","sticky-fill","sticky","stop-btn-fill","stop-btn","stop-circle-fill","stop-circle","stop-fill","stop","stoplights-fill","stoplights","stopwatch-fill","stopwatch","subtract","suit-club-fill","suit-club","suit-diamond-fill","suit-diamond","suit-heart-fill","suit-heart","suit-spade-fill","suit-spade","sun-fill","sun","sunglasses","sunrise-fill","sunrise","sunset-fill","sunset","symmetry-horizontal","symmetry-vertical","table","tablet-fill","tablet-landscape-fill","tablet-landscape","tablet","tag-fill","tag","tags-fill","tags","telegram","telephone-fill","telephone-forward-fill","telephone-forward","telephone-inbound-fill","telephone-inbound","telephone-minus-fill","telephone-minus","telephone-outbound-fill","telephone-outbound","telephone-plus-fill","telephone-plus","telephone-x-fill","telephone-x","telephone","terminal-fill","terminal","text-center","text-indent-left","text-indent-right","text-left","text-paragraph","text-right","textarea-resize","textarea-t","textarea","thermometer-half","thermometer-high","thermometer-low","thermometer-snow","thermometer-sun","thermometer","three-dots-vertical","three-dots","toggle-off","toggle-on","toggle2-off","toggle2-on","toggles","toggles2","tools","tornado","trash-fill","trash","trash2-fill","trash2","tree-fill","tree","triangle-fill","triangle-half","triangle","trophy-fill","trophy","tropical-storm","truck-flatbed","truck","tsunami","tv-fill","tv","twitch","twitter","type-bold","type-h1","type-h2","type-h3","type-italic","type-strikethrough","type-underline","type","ui-checks-grid","ui-checks","ui-radios-grid","ui-radios","umbrella-fill","umbrella","union","unlock-fill","unlock","upc-scan","upc","upload","vector-pen","view-list","view-stacked","vinyl-fill","vinyl","voicemail","volume-down-fill","volume-down","volume-mute-fill","volume-mute","volume-off-fill","volume-off","volume-up-fill","volume-up","vr","wallet-fill","wallet","wallet2","watch","water","whatsapp","wifi-1","wifi-2","wifi-off","wifi","wind","window-dock","window-sidebar","window","wrench","x-circle-fill","x-circle","x-diamond-fill","x-diamond","x-octagon-fill","x-octagon","x-square-fill","x-square","x","youtube","zoom-in","zoom-out","bank","bank2","bell-slash-fill","bell-slash","cash-coin","check-lg","coin","currency-bitcoin","currency-dollar","currency-euro","currency-exchange","currency-pound","currency-yen","dash-lg","exclamation-lg","file-earmark-pdf-fill","file-earmark-pdf","file-pdf-fill","file-pdf","gender-ambiguous","gender-female","gender-male","gender-trans","headset-vr","info-lg","mastodon","messenger","piggy-bank-fill","piggy-bank","pin-map-fill","pin-map","plus-lg","question-lg","recycle","reddit","safe-fill","safe2-fill","safe2","sd-card-fill","sd-card","skype","slash-lg","translate","x-lg","safe","apple","microsoft","windows","behance","dribbble","line","medium","paypal","pinterest","signal","snapchat","spotify","stack-overflow","strava","wordpress","vimeo","activity","easel2-fill","easel2","easel3-fill","easel3","fan","fingerprint","graph-down-arrow","graph-up-arrow","hypnotize","magic","person-rolodex","person-video","person-video2","person-video3","person-workspace","radioactive","webcam-fill","webcam","yin-yang","bandaid-fill","bandaid","bluetooth","body-text","boombox","boxes","dpad-fill","dpad","ear-fill","ear","envelope-check-fill","envelope-check","envelope-dash-fill","envelope-dash","envelope-exclamation-fill","envelope-exclamation","envelope-plus-fill","envelope-plus","envelope-slash-fill","envelope-slash","envelope-x-fill","envelope-x","explicit-fill","explicit","git","infinity","list-columns-reverse","list-columns","meta","nintendo-switch","pc-display-horizontal","pc-display","pc-horizontal","pc","playstation","plus-slash-minus","projector-fill","projector","qr-code-scan","qr-code","quora","quote","robot","send-check-fill","send-check","send-dash-fill","send-dash","send-exclamation-fill","send-exclamation","send-fill","send-plus-fill","send-plus","send-slash-fill","send-slash","send-x-fill","send-x","send","steam","terminal-dash","terminal-plus","terminal-split","ticket-detailed-fill","ticket-detailed","ticket-fill","ticket-perforated-fill","ticket-perforated","ticket","tiktok","window-dash","window-desktop","window-fullscreen","window-plus","window-split","window-stack","window-x","xbox","ethernet","hdmi-fill","hdmi","usb-c-fill","usb-c","usb-fill","usb-plug-fill","usb-plug","usb-symbol","usb","boombox-fill","displayport","gpu-card","memory","modem-fill","modem","motherboard-fill","motherboard","optical-audio-fill","optical-audio","pci-card","router-fill","router","thunderbolt-fill","thunderbolt","usb-drive-fill","usb-drive","usb-micro-fill","usb-micro","usb-mini-fill","usb-mini","cloud-haze2","device-hdd-fill","device-hdd","device-ssd-fill","device-ssd","displayport-fill","mortarboard-fill","mortarboard","terminal-x","arrow-through-heart-fill","arrow-through-heart","badge-sd-fill","badge-sd","bag-heart-fill","bag-heart","balloon-fill","balloon-heart-fill","balloon-heart","balloon","box2-fill","box2-heart-fill","box2-heart","box2","braces-asterisk","calendar-heart-fill","calendar-heart","calendar2-heart-fill","calendar2-heart","chat-heart-fill","chat-heart","chat-left-heart-fill","chat-left-heart","chat-right-heart-fill","chat-right-heart","chat-square-heart-fill","chat-square-heart","clipboard-check-fill","clipboard-data-fill","clipboard-fill","clipboard-heart-fill","clipboard-heart","clipboard-minus-fill","clipboard-plus-fill","clipboard-pulse","clipboard-x-fill","clipboard2-check-fill","clipboard2-check","clipboard2-data-fill","clipboard2-data","clipboard2-fill","clipboard2-heart-fill","clipboard2-heart","clipboard2-minus-fill","clipboard2-minus","clipboard2-plus-fill","clipboard2-plus","clipboard2-pulse-fill","clipboard2-pulse","clipboard2-x-fill","clipboard2-x","clipboard2","emoji-kiss-fill","emoji-kiss","envelope-heart-fill","envelope-heart","envelope-open-heart-fill","envelope-open-heart","envelope-paper-fill","envelope-paper-heart-fill","envelope-paper-heart","envelope-paper","filetype-aac","filetype-ai","filetype-bmp","filetype-cs","filetype-css","filetype-csv","filetype-doc","filetype-docx","filetype-exe","filetype-gif","filetype-heic","filetype-html","filetype-java","filetype-jpg","filetype-js","filetype-jsx","filetype-key","filetype-m4p","filetype-md","filetype-mdx","filetype-mov","filetype-mp3","filetype-mp4","filetype-otf","filetype-pdf","filetype-php","filetype-png","filetype-ppt","filetype-psd","filetype-py","filetype-raw","filetype-rb","filetype-sass","filetype-scss","filetype-sh","filetype-svg","filetype-tiff","filetype-tsx","filetype-ttf","filetype-txt","filetype-wav","filetype-woff","filetype-xls","filetype-xml","filetype-yml","heart-arrow","heart-pulse-fill","heart-pulse","heartbreak-fill","heartbreak","hearts","hospital-fill","hospital","house-heart-fill","house-heart","incognito","magnet-fill","magnet","person-heart","person-hearts","phone-flip","plugin","postage-fill","postage-heart-fill","postage-heart","postage","postcard-fill","postcard-heart-fill","postcard-heart","postcard","search-heart-fill","search-heart","sliders2-vertical","sliders2","trash3-fill","trash3","valentine","valentine2","wrench-adjustable-circle-fill","wrench-adjustable-circle","wrench-adjustable","filetype-json","filetype-pptx","filetype-xlsx","1-circle-fill","1-circle","1-square-fill","1-square","2-circle-fill","2-circle","2-square-fill","2-square","3-circle-fill","3-circle","3-square-fill","3-square","4-circle-fill","4-circle","4-square-fill","4-square","5-circle-fill","5-circle","5-square-fill","5-square","6-circle-fill","6-circle","6-square-fill","6-square","7-circle-fill","7-circle","7-square-fill","7-square","8-circle-fill","8-circle","8-square-fill","8-square","9-circle-fill","9-circle","9-square-fill","9-square","airplane-engines-fill","airplane-engines","airplane-fill","airplane","alexa","alipay","android","android2","box-fill","box-seam-fill","browser-chrome","browser-edge","browser-firefox","browser-safari","c-circle-fill","c-circle","c-square-fill","c-square","capsule-pill","capsule","car-front-fill","car-front","cassette-fill","cassette","cc-circle-fill","cc-circle","cc-square-fill","cc-square","cup-hot-fill","cup-hot","currency-rupee","dropbox","escape","fast-forward-btn-fill","fast-forward-btn","fast-forward-circle-fill","fast-forward-circle","fast-forward-fill","fast-forward","filetype-sql","fire","google-play","h-circle-fill","h-circle","h-square-fill","h-square","indent","lungs-fill","lungs","microsoft-teams","p-circle-fill","p-circle","p-square-fill","p-square","pass-fill","pass","prescription","prescription2","r-circle-fill","r-circle","r-square-fill","r-square","repeat-1","repeat","rewind-btn-fill","rewind-btn","rewind-circle-fill","rewind-circle","rewind-fill","rewind","train-freight-front-fill","train-freight-front","train-front-fill","train-front","train-lightrail-front-fill","train-lightrail-front","truck-front-fill","truck-front","ubuntu","unindent","unity","universal-access-circle","universal-access","virus","virus2","wechat","yelp","sign-stop-fill","sign-stop-lights-fill","sign-stop-lights","sign-stop","sign-turn-left-fill","sign-turn-left","sign-turn-right-fill","sign-turn-right","sign-turn-slight-left-fill","sign-turn-slight-left","sign-turn-slight-right-fill","sign-turn-slight-right","sign-yield-fill","sign-yield","ev-station-fill","ev-station","fuel-pump-diesel-fill","fuel-pump-diesel","fuel-pump-fill","fuel-pump","0-circle-fill","0-circle","0-square-fill","0-square","rocket-fill","rocket-takeoff-fill","rocket-takeoff","rocket","stripe","subscript","superscript","trello","envelope-at-fill","envelope-at","regex","text-wrap","sign-dead-end-fill","sign-dead-end","sign-do-not-enter-fill","sign-do-not-enter","sign-intersection-fill","sign-intersection-side-fill","sign-intersection-side","sign-intersection-t-fill","sign-intersection-t","sign-intersection-y-fill","sign-intersection-y","sign-intersection","sign-merge-left-fill","sign-merge-left","sign-merge-right-fill","sign-merge-right","sign-no-left-turn-fill","sign-no-left-turn","sign-no-parking-fill","sign-no-parking","sign-no-right-turn-fill","sign-no-right-turn","sign-railroad-fill","sign-railroad","building-add","building-check","building-dash","building-down","building-exclamation","building-fill-add","building-fill-check","building-fill-dash","building-fill-down","building-fill-exclamation","building-fill-gear","building-fill-lock","building-fill-slash","building-fill-up","building-fill-x","building-fill","building-gear","building-lock","building-slash","building-up","building-x","buildings-fill","buildings","bus-front-fill","bus-front","ev-front-fill","ev-front","globe-americas","globe-asia-australia","globe-central-south-asia","globe-europe-africa","house-add-fill","house-add","house-check-fill","house-check","house-dash-fill","house-dash","house-down-fill","house-down","house-exclamation-fill","house-exclamation","house-gear-fill","house-gear","house-lock-fill","house-lock","house-slash-fill","house-slash","house-up-fill","house-up","house-x-fill","house-x","person-add","person-down","person-exclamation","person-fill-add","person-fill-check","person-fill-dash","person-fill-down","person-fill-exclamation","person-fill-gear","person-fill-lock","person-fill-slash","person-fill-up","person-fill-x","person-gear","person-lock","person-slash","person-up","scooter","taxi-front-fill","taxi-front","amd","database-add","database-check","database-dash","database-down","database-exclamation","database-fill-add","database-fill-check","database-fill-dash","database-fill-down","database-fill-exclamation","database-fill-gear","database-fill-lock","database-fill-slash","database-fill-up","database-fill-x","database-fill","database-gear","database-lock","database-slash","database-up","database-x","database","houses-fill","houses","nvidia","person-vcard-fill","person-vcard","sina-weibo","tencent-qq","wikipedia","alphabet-uppercase","alphabet","amazon","arrows-collapse-vertical","arrows-expand-vertical","arrows-vertical","arrows","ban-fill","ban","bing","cake","cake2","cookie","copy","crosshair","crosshair2","emoji-astonished-fill","emoji-astonished","emoji-grimace-fill","emoji-grimace","emoji-grin-fill","emoji-grin","emoji-surprise-fill","emoji-surprise","emoji-tear-fill","emoji-tear","envelope-arrow-down-fill","envelope-arrow-down","envelope-arrow-up-fill","envelope-arrow-up","feather","feather2","floppy-fill","floppy","floppy2-fill","floppy2","gitlab","highlighter","marker-tip","nvme-fill","nvme","opencollective","pci-card-network","pci-card-sound","radar","send-arrow-down-fill","send-arrow-down","send-arrow-up-fill","send-arrow-up","sim-slash-fill","sim-slash","sourceforge","substack","threads-fill","threads","transparency","twitter-x","type-h4","type-h5","type-h6","backpack-fill","backpack","backpack2-fill","backpack2","backpack3-fill","backpack3","backpack4-fill","backpack4","brilliance","cake-fill","cake2-fill","duffle-fill","duffle","exposure","gender-neuter","highlights","luggage-fill","luggage","mailbox-flag","mailbox2-flag","noise-reduction","passport-fill","passport","person-arms-up","person-raised-hand","person-standing-dress","person-standing","person-walking","person-wheelchair","shadows","suitcase-fill","suitcase-lg-fill","suitcase-lg","suitcase","suitcase2-fill","suitcase2","vignette"]; -export default icons; \ No newline at end of file diff --git a/Foxnouns.Frontend/src/lib/index.ts b/Foxnouns.Frontend/src/lib/index.ts deleted file mode 100644 index 42d3314..0000000 --- a/Foxnouns.Frontend/src/lib/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. - -import type { Cookies } from "@sveltejs/kit"; -import { DateTime } from "luxon"; - -export const TOKEN_COOKIE_NAME = "__Host-pronounscc-token"; - -export const setToken = (cookies: Cookies, token: string) => - cookies.set(TOKEN_COOKIE_NAME, token, { path: "/" }); -export const clearToken = (cookies: Cookies) => cookies.delete(TOKEN_COOKIE_NAME, { path: "/" }); - -export const DEFAULT_FLAG = "/unknown_flag.svg"; - -export const idTimestamp = (id: string) => - DateTime.fromMillis(parseInt(id, 10) / (1 << 22) + 1_640_995_200_000); - -export const alertKey = (url: URL): string | undefined => - url.searchParams.has("alert") ? "alert." + url.searchParams.get("alert") : undefined; diff --git a/Foxnouns.Frontend/src/lib/log.ts b/Foxnouns.Frontend/src/lib/log.ts deleted file mode 100644 index f8995c6..0000000 --- a/Foxnouns.Frontend/src/lib/log.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Logger } from "tslog"; - -const log = new Logger(); -export default log; diff --git a/Foxnouns.Frontend/src/lib/markdown.ts b/Foxnouns.Frontend/src/lib/markdown.ts deleted file mode 100644 index 9c4ff35..0000000 --- a/Foxnouns.Frontend/src/lib/markdown.ts +++ /dev/null @@ -1,14 +0,0 @@ -import MarkdownIt from "markdown-it"; -import sanitize from "sanitize-html"; - -const md = new MarkdownIt({ - html: false, - breaks: true, - linkify: true, -}).disable(["heading", "lheading", "link", "table", "blockquote"]); - -const unsafeMd = new MarkdownIt(); - -export const renderMarkdown = (src: string | null) => (src ? sanitize(md.render(src)) : null); - -export const renderUnsafeMarkdown = (src: string) => sanitize(unsafeMd.render(src)); diff --git a/Foxnouns.Frontend/src/lib/pageUtils.svelte.ts b/Foxnouns.Frontend/src/lib/pageUtils.svelte.ts deleted file mode 100644 index 5f45815..0000000 --- a/Foxnouns.Frontend/src/lib/pageUtils.svelte.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { page } from "$app/state"; - -export const isActive = (path: string | string[], prefix: boolean = false) => - typeof path === "string" - ? prefix - ? page.url.pathname.startsWith(path) - : page.url.pathname === path - : prefix - ? path.some((p) => page.url.pathname.startsWith(p)) - : path.some((p) => page.url.pathname === p); diff --git a/Foxnouns.Frontend/src/lib/paginate.ts b/Foxnouns.Frontend/src/lib/paginate.ts deleted file mode 100644 index 1d4e0d1..0000000 --- a/Foxnouns.Frontend/src/lib/paginate.ts +++ /dev/null @@ -1,35 +0,0 @@ -export type PaginatedArray = { - data: T[]; - currentPage: number; - pageCount: number; -}; - -/** - * Paginates an array. - * @param arr The array to paginate. - * @param page The zero-indexed page number. - * @param perPage How many items to display per page. - * @returns An object containing a slice of the array, the current page number, and the page count. - */ -export default function paginate( - arr: T[] | null, - page: string | number | null, - perPage: number, -): PaginatedArray { - if (arr && arr.length > 0) { - let currentPage = 0; - if (page && typeof page === "string") currentPage = parseInt(page); - if (page && typeof page === "number") currentPage = page; - - const pageCount = Math.ceil(arr.length / perPage); - let data = arr.slice(currentPage * perPage, (currentPage + 1) * perPage); - if (data.length === 0) { - data = arr.slice(0, perPage); - currentPage = 0; - } - - return { data, currentPage, pageCount }; - } - - return { data: [], currentPage: 0, pageCount: 1 }; -} diff --git a/Foxnouns.Frontend/src/lib/state.svelte.ts b/Foxnouns.Frontend/src/lib/state.svelte.ts deleted file mode 100644 index 8358bf3..0000000 --- a/Foxnouns.Frontend/src/lib/state.svelte.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { onMount, onDestroy } from "svelte"; -import { browser } from "$app/environment"; -import log from "./log"; -// eslint-disable-next-line @typescript-eslint/no-unused-vars -import type { Snapshot } from "@sveltejs/kit"; - -/** - * Store ephemeral state in sessionStorage to persist between navigations. - * Similar to {@link Snapshot}, but doesn't attach it to a history entry. - * @param key Unique key to use for this state. - * @param capture Function that returns the state to store. - * @param restore Function that takes the state that was stored previously and assigns it back to component variables. - */ -export default function ephemeralState( - key: string, - capture: () => T, - restore: (data: T) => void, -): void { - if (!browser) return; - - onMount(() => { - if (!("sessionStorage" in window)) return; - const rawData = sessionStorage.getItem("ephemeral-" + key); - if (!rawData) return; - - log.debug("Restoring data %s from session storage", key); - const data = JSON.parse(rawData) as T; - restore(data); - }); - - onDestroy(() => { - if (!("sessionStorage" in window)) return; - - log.debug("Saving data %s to session storage", key); - sessionStorage.setItem("ephemeral-" + key, JSON.stringify(capture())); - }); -} diff --git a/Foxnouns.Frontend/src/lib/tippy.ts b/Foxnouns.Frontend/src/lib/tippy.ts deleted file mode 100644 index 39e2ca0..0000000 --- a/Foxnouns.Frontend/src/lib/tippy.ts +++ /dev/null @@ -1,11 +0,0 @@ -import "tippy.js/animations/scale-subtle.css"; -import { createTippy } from "svelte-tippy"; - -// use with use:tippy on elements -// temporary (probably) until sveltestrap works with svelte 5 -const tippy = createTippy({ - animation: "scale-subtle", - delay: [null, 0], -}); - -export default tippy; diff --git a/Foxnouns.Frontend/src/routes/+error.svelte b/Foxnouns.Frontend/src/routes/+error.svelte deleted file mode 100644 index e667611..0000000 --- a/Foxnouns.Frontend/src/routes/+error.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - {$t("title.an-error-occurred")} • pronouns.cc - - -
    - -
    - {#if data.meUser} - - {$t("error.back-to-profile-button")} - - {:else} - {$t("error.back-to-main-page-button")} - {/if} - -
    -
    diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts deleted file mode 100644 index 2debd7c..0000000 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { clearToken, TOKEN_COOKIE_NAME } from "$lib"; -import { apiRequest } from "$api"; -import ApiError, { ErrorCode } from "$api/error"; -import type { Meta, MeUser } from "$api/models"; -import type { Notification } from "$api/models/moderation"; -import log from "$lib/log"; -import type { LayoutServerLoad } from "./$types"; - -export const load = (async ({ fetch, cookies }) => { - let token: string | null = null; - let meUser: MeUser | null = null; - let unreadNotifications: boolean = false; - if (cookies.get(TOKEN_COOKIE_NAME)) { - try { - meUser = await apiRequest("GET", "/users/@me", { fetch, cookies }); - token = cookies.get(TOKEN_COOKIE_NAME) || null; - - const notifications = await apiRequest("GET", "/notifications", { - fetch, - cookies, - }); - unreadNotifications = notifications.filter((n) => !n.acknowledged).length > 0; - } catch (e) { - if (e instanceof ApiError && e.code === ErrorCode.AuthenticationRequired) clearToken(cookies); - else log.error("Could not fetch /users/@me and token has not expired:", e); - } - } - - const meta = await apiRequest("GET", "/meta", { fetch, cookies }); - return { meta, meUser, token, unreadNotifications }; -}) satisfies LayoutServerLoad; diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte deleted file mode 100644 index b991f8a..0000000 --- a/Foxnouns.Frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - -
    -
    - - {@render children?.()} -
    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte deleted file mode 100644 index 4fd36dc..0000000 --- a/Foxnouns.Frontend/src/routes/+page.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - - - pronouns.cc - - -
    - {#if data.meta.notice} - - {/if} - -

    pronouns.cc

    - -

    - {data.meta.repository} - {data.meta.version} - {data.meta.users.total} - {data.meta.limits.bio_length} -

    -
    diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts deleted file mode 100644 index a149d86..0000000 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { apiRequest } from "$api"; -import type { UserWithMembers } from "$api/models"; -import paginate from "$lib/paginate"; - -const MEMBERS_PER_PAGE = 20; - -export const load = async ({ params, fetch, cookies, url }) => { - const user = await apiRequest("GET", `/users/${params.username}`, { - fetch, - cookies, - }); - - const { data, currentPage, pageCount } = paginate( - user.members, - url.searchParams.get("page"), - MEMBERS_PER_PAGE, - ); - - return { user, members: data, currentPage, pageCount }; -}; diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte deleted file mode 100644 index e431e9b..0000000 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - - - @{data.user.username} • pronouns.cc - - -
    - {#if isMeUser} - - {/if} - - - - - - - {#if data.members.length > 0} -
    -

    - {data.user.member_title || $t("profile.default-members-header")} - {#if isMeUser} - - - {$t("profile.create-member-button")} - - {/if} -

    - -
    - {#each data.members as member (member.id)} - - {/each} -
    - - {/if} -
    diff --git a/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.server.ts b/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.server.ts deleted file mode 100644 index f3f8400..0000000 --- a/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { apiRequest } from "$api"; -import type { Member } from "$api/models/member"; - -export const load = async ({ params, fetch, cookies }) => { - const member = await apiRequest( - "GET", - `/users/${params.username}/members/${params.memberName}`, - { - fetch, - cookies, - }, - ); - - return { member }; -}; diff --git a/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.svelte deleted file mode 100644 index 209c388..0000000 --- a/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.svelte +++ /dev/null @@ -1,50 +0,0 @@ - - - - {data.member.display_name} • @{data.member.user.username} • pronouns.cc - - - diff --git a/Foxnouns.Frontend/src/routes/admin/+layout.server.ts b/Foxnouns.Frontend/src/routes/admin/+layout.server.ts deleted file mode 100644 index 38df461..0000000 --- a/Foxnouns.Frontend/src/routes/admin/+layout.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { apiRequest } from "$api"; -import ApiError, { ErrorCode } from "$api/error"; -import type { Report } from "$api/models/moderation"; -import { idTimestamp } from "$lib"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent, fetch, cookies }) => { - const { meUser } = await parent(); - if (!meUser) redirect(303, "/"); - - if (meUser.role !== "ADMIN" && meUser.role !== "MODERATOR") { - throw new ApiError({ - status: 403, - code: ErrorCode.Forbidden, - message: "Only admins and moderators can use this page.", - }); - } - - const reports = await apiRequest("GET", "/moderation/reports", { fetch, cookies }); - const staleReportCount = reports.filter( - (r) => idTimestamp(r.id).diffNow(["days"]).days <= -7, - ).length; - - return { - user: meUser, - isAdmin: meUser.role === "ADMIN", - reportCount: reports.length, - staleReportCount, - }; -}; diff --git a/Foxnouns.Frontend/src/routes/admin/+layout.svelte b/Foxnouns.Frontend/src/routes/admin/+layout.svelte deleted file mode 100644 index c40279e..0000000 --- a/Foxnouns.Frontend/src/routes/admin/+layout.svelte +++ /dev/null @@ -1,57 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/routes/admin/+page.svelte b/Foxnouns.Frontend/src/routes/admin/+page.svelte deleted file mode 100644 index bf0c8f7..0000000 --- a/Foxnouns.Frontend/src/routes/admin/+page.svelte +++ /dev/null @@ -1,27 +0,0 @@ - - - - Admin dashboard • pronouns.cc - - -

    Dashboard

    - -
    - - {data.meta.users.total.toLocaleString("en")} -
    - ({data.meta.users.active_month.toLocaleString("en")} active in the last month) -
    - {data.meta.members.toLocaleString("en")} - - {data.reportCount.toLocaleString("en")} -
    - ({data.staleReportCount} older than 1 week) -
    -
    diff --git a/Foxnouns.Frontend/src/routes/admin/audit-log/+page.server.ts b/Foxnouns.Frontend/src/routes/admin/audit-log/+page.server.ts deleted file mode 100644 index f48e334..0000000 --- a/Foxnouns.Frontend/src/routes/admin/audit-log/+page.server.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { apiRequest } from "$api"; -import { type AuditLogEntity, type AuditLogEntry } from "$api/models/moderation.js"; - -export const load = async ({ url, fetch, cookies }) => { - const type = url.searchParams.get("type"); - const before = url.searchParams.get("before"); - const after = url.searchParams.get("after"); - const byModerator = url.searchParams.get("by-moderator"); - let limit: number = 100; - if (url.searchParams.has("limit")) limit = parseInt(url.searchParams.get("limit")!); - - const params = new URLSearchParams(); - params.set("limit", limit.toString()); - if (type) params.set("type", type); - if (before) params.set("before", before); - if (after) params.set("after", after); - if (byModerator) params.set("by-moderator", byModerator); - - const entries = await apiRequest( - "GET", - `/moderation/audit-log?${params.toString()}`, - { - fetch, - cookies, - }, - ); - - const moderators = await apiRequest("GET", "/moderation/audit-log/moderators", { - fetch, - cookies, - }); - - let modFilter: AuditLogEntity | null = null; - if (byModerator) - modFilter = entries.find((e) => e.moderator.id === byModerator)?.moderator || null; - - return { entries, type, before, after, modFilter, url: url.toString(), moderators }; -}; diff --git a/Foxnouns.Frontend/src/routes/admin/audit-log/+page.svelte b/Foxnouns.Frontend/src/routes/admin/audit-log/+page.svelte deleted file mode 100644 index 0c1cf72..0000000 --- a/Foxnouns.Frontend/src/routes/admin/audit-log/+page.svelte +++ /dev/null @@ -1,115 +0,0 @@ - - - - Audit log • pronouns.cc - - -

    Audit log

    - -
    - - - Filter by type - - - - Ignore report - - - Warn user - - - Warn user and clear profile - - - Suspend user - - - Query sensitive user data - - {#if data.type} - Remove filter - {/if} - - - - - Filter by moderator - - - {#each data.moderators as mod (mod.id)} - - {mod.username} - - {/each} - {#if data.modFilter} - Remove filter - {/if} - - -
    - -{#if data.before} - Show newer entries -{/if} - -{#each data.entries as entry (entry.id)} - -{:else} -

    There are no entries matching your filter

    -{/each} - -{#if data.entries.length === 100} - Show older entries -{/if} diff --git a/Foxnouns.Frontend/src/routes/admin/lookup/+page.server.ts b/Foxnouns.Frontend/src/routes/admin/lookup/+page.server.ts deleted file mode 100644 index 90a284e..0000000 --- a/Foxnouns.Frontend/src/routes/admin/lookup/+page.server.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { apiRequest } from "$api"; -import { redirect } from "@sveltejs/kit"; - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const query = body.get("query") as string; - const fuzzy = body.get("fuzzy") === "yes"; - - const users = await apiRequest>( - "POST", - "/moderation/lookup", - { - fetch, - cookies, - body: { - query, - fuzzy, - }, - }, - ); - - if (!fuzzy && users.length > 0) redirect(303, `/admin/lookup/${users[0].id}`); - - return { users }; - }, -}; diff --git a/Foxnouns.Frontend/src/routes/admin/lookup/+page.svelte b/Foxnouns.Frontend/src/routes/admin/lookup/+page.svelte deleted file mode 100644 index b3c8a8d..0000000 --- a/Foxnouns.Frontend/src/routes/admin/lookup/+page.svelte +++ /dev/null @@ -1,33 +0,0 @@ - - - - Look up a user • pronouns.cc - - -

    Look up a user

    - -
    -
    - - -
    -
    - - -
    -
    - -
    - {#each form?.users || [] as user (user.id)} - - {user.username} ({user.id}) - - {:else} -
    No results
    - {/each} -
    diff --git a/Foxnouns.Frontend/src/routes/admin/lookup/[id]/+page.server.ts b/Foxnouns.Frontend/src/routes/admin/lookup/[id]/+page.server.ts deleted file mode 100644 index 130ae90..0000000 --- a/Foxnouns.Frontend/src/routes/admin/lookup/[id]/+page.server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { apiRequest } from "$api"; -import type { QueriedUser } from "$api/models/moderation"; - -export const load = async ({ params, fetch, cookies }) => { - const user = await apiRequest("GET", `/moderation/lookup/${params.id}`, { - fetch, - cookies, - }); - - return { user }; -}; diff --git a/Foxnouns.Frontend/src/routes/admin/lookup/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/admin/lookup/[id]/+page.svelte deleted file mode 100644 index e3cf389..0000000 --- a/Foxnouns.Frontend/src/routes/admin/lookup/[id]/+page.svelte +++ /dev/null @@ -1,63 +0,0 @@ - - - - Looking up @{data.user.user.username} • pronouns.cc - - -

    Basic profile

    - - - - - -

    Extra information

    - - - - - - - - - - - - - - - - -
    Created at{createdAt.toLocaleString(DateTime.DATETIME_MED)}
    Last active{lastActive.toLocaleString(DateTime.DATETIME_MED)}
    Last SID reroll{lastSidReroll.toLocaleString(DateTime.DATETIME_MED)}
    - -{#if authMethods} -

    Authentication methods

    -
    - {#each authMethods as method (method.id)} - - {/each} -
    -{/if} diff --git a/Foxnouns.Frontend/src/routes/admin/reports/+page.server.ts b/Foxnouns.Frontend/src/routes/admin/reports/+page.server.ts deleted file mode 100644 index c2149c1..0000000 --- a/Foxnouns.Frontend/src/routes/admin/reports/+page.server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { apiRequest } from "$api"; -import type { Report } from "$api/models/moderation"; - -export const load = async ({ url, fetch, cookies }) => { - const before = url.searchParams.get("before"); - const after = url.searchParams.get("after"); - const byReporter = url.searchParams.get("by-reporter"); - const byTarget = url.searchParams.get("by-target"); - const includeClosed = url.searchParams.get("include-closed") === "true"; - - const params = new URLSearchParams(); - if (before) params.set("before", before); - if (after) params.set("after", after); - if (byReporter) params.set("by-reporter", byReporter); - if (byTarget) params.set("by-target", byTarget); - if (includeClosed) params.set("include-closed", "true"); - - const reports = await apiRequest("GET", `/moderation/reports?${params.toString()}`, { - fetch, - cookies, - }); - return { reports, url: url.toString(), includeClosed, byReporter, byTarget, before, after }; -}; diff --git a/Foxnouns.Frontend/src/routes/admin/reports/+page.svelte b/Foxnouns.Frontend/src/routes/admin/reports/+page.svelte deleted file mode 100644 index 5021846..0000000 --- a/Foxnouns.Frontend/src/routes/admin/reports/+page.svelte +++ /dev/null @@ -1,138 +0,0 @@ - - - - Reports • pronouns.cc - - -

    Reports

    - -
      - {#if data.byTarget} -
    • Filtering by target (clear)
    • - {/if} - {#if data.byReporter} -
    • Filtering by reporter (clear)
    • - {/if} - {#if data.includeClosed} -
    • Showing all reports (only show open reports)
    • - {:else} -
    • Showing open reports (show all reports)
    • - {/if} -
    - -{#if data.before} - Show newer reports -{/if} - - - - - - - - - - - - - - - {#each data.reports as report (report.id)} - - - - - - - - - - {/each} - -
    UserMemberReporterReasonContext?Created at
    - - - Open report - - - @{report.target_user.username} - ( - {#if data.byTarget === report.target_user.id}{:else}{/if} - ) - - {#if report.target_member} - - {report.target_member.name} - - {:else} - (none) - {/if} - - {report.reporter.username} - ( - {#if data.byReporter === report.reporter.id}{:else}{/if} - ) - {report.reason} - {#if report.context} - {$t("yes")} - {:else} - {$t("no")} - {/if} - - {idTimestamp(report.id).toLocaleString(DateTime.DATETIME_SHORT)} -
    - -{#if data.reports.length === 100} - Show older reports -{/if} diff --git a/Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.server.ts b/Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.server.ts deleted file mode 100644 index 214049a..0000000 --- a/Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.server.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { apiRequest } from "$api"; -import type { ReportDetails } from "$api/models/moderation"; -import { createModactions } from "$lib/actions/modaction"; - -export const load = async ({ params, fetch, cookies }) => { - const resp = await apiRequest("GET", `/moderation/reports/${params.id}`, { - fetch, - cookies, - }); - return { - report: resp.report, - user: resp.user, - member: resp.member, - auditLogEntry: resp.audit_log_entry, - }; -}; - -export const actions = createModactions(); diff --git a/Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.svelte deleted file mode 100644 index 8a1ae08..0000000 --- a/Foxnouns.Frontend/src/routes/admin/reports/[id]/+page.svelte +++ /dev/null @@ -1,99 +0,0 @@ - - - - Report on @{user.username} • pronouns.cc - - -{#if report.status === "CLOSED"} -
    - This report has already been handled. See audit log entry -
    -{/if} - -
    -
    -

    Target user

    - -
    - {#if report.target_member} -
    -

    Target member

    - -
    - {/if} -
    -

    Reporter

    - -
    -
    - -
    -
    -

    Reason

    -

    {report.reason}

    -
    -
    -

    Context

    -

    - {#if report.context} - - {@html renderMarkdown(report.context)} - {:else} - (no context given) - {/if} -

    -
    -
    - -{#if report.status === "OPEN"} -
    -

    Take action

    - -
    -{:else if report.status === "CLOSED" && auditLogEntry} - -{:else} -
    -

    Closed by an unknown moderator

    -

    - This should not happen! -

    -
    -{/if} - -{#if report.snapshot} -

    Profile at time of report

    -
    - {#if report.target_type === "USER"} - {@const snapshot = report.snapshot as User} - - {:else} - {@const snapshot = report.snapshot as Member} - - {/if} -{/if} diff --git a/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.server.ts deleted file mode 100644 index b297e25..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.server.ts +++ /dev/null @@ -1,3 +0,0 @@ -import createCallbackLoader from "$lib/actions/callback"; - -export const load = createCallbackLoader("discord"); diff --git a/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.svelte deleted file mode 100644 index 10fec06..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - {$t("auth.register-with-discord")} • pronouns.cc - - -
    - {#if data.error} -

    {$t("auth.register-with-discord")}

    - - {:else} - - {/if} -
    diff --git a/Foxnouns.Frontend/src/routes/auth/callback/email/[code]/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/email/[code]/+page.server.ts deleted file mode 100644 index 76829a6..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/email/[code]/+page.server.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { apiRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import type { AuthResponse } from "$api/models/auth"; -import { setToken } from "$lib"; -import createCallbackLoader from "$lib/actions/callback"; -import log from "$lib/log"; -import { redirect, isRedirect } from "@sveltejs/kit"; - -export const load = createCallbackLoader("email", async ({ params }) => { - log.info("params:", params, "code:", params.code); - - return { state: params.code! }; -}); - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const data = await request.formData(); - const username = data.get("username") as string | null; - const ticket = data.get("ticket") as string | null; - const password = data.get("password") as string | null; - const password2 = data.get("confirm-password") as string | null; - - if (!username || !ticket || !password || !password2) - return { - error: { message: "Bad request", code: ErrorCode.BadRequest, status: 400 } as RawApiError, - }; - - if (password !== password2) - return { - error: { - message: "Passwords do not match", - code: ErrorCode.BadRequest, - status: 400, - } as RawApiError, - }; - - try { - const resp = await apiRequest("POST", "/auth/email/register", { - body: { username, ticket, password }, - isInternal: true, - fetch, - }); - - setToken(cookies, resp.token); - redirect(303, "/auth/welcome"); - } catch (e) { - if (isRedirect(e)) throw e; - log.error("Could not sign up user with username %s:", username, e); - if (e instanceof ApiError) return { error: e.obj }; - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/auth/callback/email/[code]/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/email/[code]/+page.svelte deleted file mode 100644 index ac49624..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/email/[code]/+page.svelte +++ /dev/null @@ -1,51 +0,0 @@ - - - - {$t("auth.register-with-email")} • pronouns.cc - - -
    - {#if data.error} -

    {$t("auth.register-with-email")}

    - - {:else if data.isLinkRequest} - - {:else} -

    {$t("auth.register-with-email")}

    - - {#if form?.error} - - {/if} - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - - -
    - - -
    - {/if} -
    diff --git a/Foxnouns.Frontend/src/routes/auth/callback/google/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/google/+page.server.ts deleted file mode 100644 index d3e1b93..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/google/+page.server.ts +++ /dev/null @@ -1,3 +0,0 @@ -import createCallbackLoader from "$lib/actions/callback"; - -export const load = createCallbackLoader("google"); diff --git a/Foxnouns.Frontend/src/routes/auth/callback/google/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/google/+page.svelte deleted file mode 100644 index eeff070..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/google/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - {$t("auth.register-with-google")} • pronouns.cc - - -
    - {#if data.error} -

    {$t("auth.register-with-google")}

    - - {:else} - - {/if} -
    diff --git a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts deleted file mode 100644 index c2d290e..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import ApiError, { ErrorCode } from "$api/error"; -import createCallbackLoader from "$lib/actions/callback"; - -export const load = createCallbackLoader("fediverse", async ({ params, url }) => { - const code = url.searchParams.get("code") as string | null; - const state = url.searchParams.get("state") as string | null; - const token = url.searchParams.get("token") as string | null; - if ((!code || !state) && !token) throw new ApiError(undefined, ErrorCode.BadRequest).obj; - - return { code: code || token, state, instance: params.instance! }; -}); diff --git a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte deleted file mode 100644 index fdc8d7a..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - {$t("auth.register-with-mastodon")} • pronouns.cc - - -
    - {#if data.error} -

    {$t("auth.register-with-mastodon")}

    - - {:else} - - {/if} -
    diff --git a/Foxnouns.Frontend/src/routes/auth/callback/register/[ticket]/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/register/[ticket]/+page.server.ts deleted file mode 100644 index b14c1a2..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/register/[ticket]/+page.server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { apiRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import type { AuthResponse } from "$api/models/auth"; -import { setToken } from "$lib"; -import log from "$lib/log"; -import { isRedirect, redirect } from "@sveltejs/kit"; - -export type TicketData = { - type: string; - ticket: string; - remoteUsername: string; -}; - -export const load = async ({ params }) => { - const data = JSON.parse(atob(params.ticket)) as TicketData; - return data; -}; - -export const actions = { - default: async ({ request, fetch, cookies, params }) => { - const type = (JSON.parse(atob(params.ticket)) as TicketData).type; - - const data = await request.formData(); - const username = data.get("username") as string | null; - const ticket = data.get("ticket") as string | null; - - if (!username || !ticket) - return { - error: { message: "Bad request", code: ErrorCode.BadRequest, status: 403 } as RawApiError, - }; - - try { - const resp = await apiRequest("POST", `/auth/${type}/register`, { - body: { username, ticket }, - isInternal: true, - fetch, - }); - - setToken(cookies, resp.token); - redirect(303, "/auth/welcome"); - } catch (e) { - if (isRedirect(e)) throw e; - log.error("Could not sign up user with username %s:", username, e); - if (e instanceof ApiError) return { error: e.obj }; - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/auth/callback/register/[ticket]/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/register/[ticket]/+page.svelte deleted file mode 100644 index 6d1b6fc..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/register/[ticket]/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - {$t(`auth.register-with-${data.type}`)} • pronouns.cc - - -
    - -
    diff --git a/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.server.ts deleted file mode 100644 index 813f19e..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.server.ts +++ /dev/null @@ -1,3 +0,0 @@ -import createCallbackLoader from "$lib/actions/callback"; - -export const load = createCallbackLoader("tumblr"); diff --git a/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.svelte deleted file mode 100644 index 7a43261..0000000 --- a/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - - - {$t("auth.register-with-tumblr")} • pronouns.cc - - -
    - {#if data.error} -

    {$t("auth.register-with-tumblr")}

    - - {:else} - - {/if} -
    diff --git a/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.server.ts deleted file mode 100644 index f7195a0..0000000 --- a/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.server.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError from "$api/error.js"; -import type { AuthUrls } from "$api/models/auth"; -import log from "$lib/log.js"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent, fetch }) => { - const { meUser } = await parent(); - if (meUser) redirect(303, `/@${meUser.username}`); - - const urls = await apiRequest("POST", "/auth/urls", { fetch, isInternal: true }); - if (!urls.email_enabled) redirect(303, "/"); -}; - -export const actions = { - default: async ({ request, fetch }) => { - const data = await request.formData(); - const email = data.get("email") as string; - - try { - await fastRequest("POST", "/auth/email/forgot-password", { - body: { email }, - isInternal: true, - fetch, - }); - - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("error sending forget password email:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.svelte b/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.svelte deleted file mode 100644 index 97d902a..0000000 --- a/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.svelte +++ /dev/null @@ -1,35 +0,0 @@ - - - - {$t("auth.forgot-password-title")} • pronouns.cc - - -
    -
    -

    {$t("auth.forgot-password-title")}

    - - - -
    - - -
    - -
    -
    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.server.ts deleted file mode 100644 index 896504e..0000000 --- a/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import type { AuthUrls } from "$api/models"; -import log from "$lib/log"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ params, parent, fetch }) => { - const { meUser } = await parent(); - if (meUser) redirect(303, `/@${meUser.username}`); - - const urls = await apiRequest("POST", "/auth/urls", { fetch, isInternal: true }); - if (!urls.email_enabled) redirect(303, "/"); - - return { state: params.code }; -}; - -export const actions = { - default: async ({ request, fetch }) => { - const data = await request.formData(); - const state = data.get("state") as string; - const password = data.get("password") as string; - const password2 = data.get("confirm-password") as string; - if (password !== password2) { - return { - ok: false, - error: { - status: 400, - message: "Passwords don't match", - code: ErrorCode.BadRequest, - } as RawApiError, - }; - } - - try { - await fastRequest("POST", "/auth/email/reset-password", { - body: { state, password }, - isInternal: true, - fetch, - }); - - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("error resetting password:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.svelte b/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.svelte deleted file mode 100644 index fa01587..0000000 --- a/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - - - {$t("auth.reset-password-title")} • pronouns.cc - - -
    -
    -

    {$t("auth.reset-password-title")}

    - - - -
    - -
    - - -
    -
    - - -
    -
    - -
    -
    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/auth/log-in/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/log-in/+page.server.ts deleted file mode 100644 index 579ea67..0000000 --- a/Foxnouns.Frontend/src/routes/auth/log-in/+page.server.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { isRedirect, redirect } from "@sveltejs/kit"; - -import { apiRequest } from "$api"; -import type { AuthResponse, AuthUrls } from "$api/models/auth"; -import { alertKey, setToken } from "$lib"; -import ApiError, { ErrorCode } from "$api/error"; - -export const load = async ({ fetch, parent, url }) => { - const parentData = await parent(); - if (parentData.meUser) redirect(303, `/@${parentData.meUser.username}`); - - const urls = await apiRequest("POST", "/auth/urls", { fetch, isInternal: true }); - return { urls, alertKey: alertKey(url) }; -}; - -export const actions = { - login: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const email = body.get("email") as string | null; - const password = body.get("password") as string | null; - - try { - const resp = await apiRequest("POST", "/auth/email/login", { - body: { email, password }, - fetch, - isInternal: true, - }); - - setToken(cookies, resp.token); - redirect(303, `/@${resp.user.username}`); - } catch (e) { - if (isRedirect(e)) throw e; - - if (e instanceof ApiError) return { error: e.obj }; - throw e; - } - }, - fediToggle: () => { - return { error: null, showFediBox: true }; - }, - fedi: async ({ request, fetch }) => { - const body = await request.formData(); - const instance = body.get("instance") as string | null; - if (!instance) return { error: new ApiError(undefined, ErrorCode.BadRequest).obj }; - - try { - const resp = await apiRequest<{ url: string }>( - "GET", - `/auth/fediverse?instance=${encodeURIComponent(instance)}`, - { fetch, isInternal: true }, - ); - redirect(303, resp.url); - } catch (e) { - if (isRedirect(e)) throw e; - - if (e instanceof ApiError) return { error: e.obj }; - throw e; - } - }, - fediForceRefresh: async ({ request, fetch }) => { - const body = await request.formData(); - const instance = body.get("instance") as string | null; - if (!instance) return { error: new ApiError(undefined, ErrorCode.BadRequest).obj }; - - try { - const resp = await apiRequest<{ url: string }>( - "GET", - `/auth/fediverse?instance=${encodeURIComponent(instance)}&force-refresh=true`, - { fetch, isInternal: true }, - ); - redirect(303, resp.url); - } catch (e) { - if (isRedirect(e)) throw e; - - if (e instanceof ApiError) return { error: e.obj }; - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte b/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte deleted file mode 100644 index 3efbfa0..0000000 --- a/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte +++ /dev/null @@ -1,104 +0,0 @@ - - - - {$t("title.log-in")} • pronouns.cc - - -
    - -
    - {#if form?.error} - - {/if} -
    -
    - {#if data.urls.email_enabled} -
    -

    {$t("auth.log-in-form-title")}

    -
    -
    - - -
    -
    - - -
    - -
    -

    - {$t("auth.log-in-sign-up-link")} • - {$t("auth.log-in-forgot-password-link")} -

    -
    - {:else} -
    - {/if} -
    - {#if data.urls.email_enabled} -

    {$t("auth.log-in-3rd-party-header")}

    -

    {$t("auth.log-in-3rd-party-desc")}

    - {:else} -

    {$t("title.log-in")}

    -

    {$t("auth.log-in-3rd-party-desc-no-email")}

    - {/if} -
    -
    - {#if data.urls.discord} - - {$t("auth.log-in-with-discord")} - - {/if} - {#if data.urls.google} - - {$t("auth.log-in-with-google")} - - {/if} - {#if data.urls.tumblr} - - {$t("auth.log-in-with-tumblr")} - - {/if} - -
    -
    - {#if form?.showFediBox} -

    {$t("auth.log-in-with-the-fediverse")}

    -
    -
    - - -
    -

    - {$t("auth.log-in-with-fediverse-error-blurb")} - -

    -
    - {/if} -
    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/auth/register/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/register/+page.server.ts deleted file mode 100644 index 7a90a22..0000000 --- a/Foxnouns.Frontend/src/routes/auth/register/+page.server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError from "$api/error.js"; -import type { AuthUrls } from "$api/models/auth"; -import log from "$lib/log.js"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ fetch, parent }) => { - const parentData = await parent(); - if (parentData.meUser) redirect(303, `/@${parentData.meUser.username}`); - - const urls = await apiRequest("POST", "/auth/urls", { fetch, isInternal: true }); - if (!urls.email_enabled) redirect(303, "/"); -}; - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const email = body.get("email") as string; - - try { - await fastRequest("POST", `/auth/email/register/init`, { - body: { email }, - isInternal: true, - fetch, - cookies, - }); - - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("error initiating registration for email %s:", email, e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/auth/register/+page.svelte b/Foxnouns.Frontend/src/routes/auth/register/+page.svelte deleted file mode 100644 index 59c6181..0000000 --- a/Foxnouns.Frontend/src/routes/auth/register/+page.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - - {$t("auth.register-with-email")} • pronouns.cc - - -
    -
    -

    {$t("auth.register-with-email")}

    - - - -
    - - - - -
    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/auth/welcome/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/welcome/+page.server.ts deleted file mode 100644 index 4b76df2..0000000 --- a/Foxnouns.Frontend/src/routes/auth/welcome/+page.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent }) => { - const { meUser } = await parent(); - if (!meUser) redirect(303, "/auth/log-in?alert=auth-required"); -}; diff --git a/Foxnouns.Frontend/src/routes/auth/welcome/+page.svelte b/Foxnouns.Frontend/src/routes/auth/welcome/+page.svelte deleted file mode 100644 index 4dd7dd4..0000000 --- a/Foxnouns.Frontend/src/routes/auth/welcome/+page.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - - - {$t("title.welcome")} • pronouns.cc - - -
    -

    Welcome to pronouns.cc!

    - - - -

    Customize your profile

    - -

    (todo)

    - -

    Create members

    -

    (todo)

    - -

    Create custom preferences

    -

    (todo)

    - -

    - Check out your profile -

    -
    diff --git a/Foxnouns.Frontend/src/routes/page/[page]/+page.server.ts b/Foxnouns.Frontend/src/routes/page/[page]/+page.server.ts deleted file mode 100644 index 1d9e8fc..0000000 --- a/Foxnouns.Frontend/src/routes/page/[page]/+page.server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { baseRequest } from "$api"; -import ApiError from "$api/error"; - -export const load = async ({ fetch, params }) => { - const resp = await baseRequest("GET", `/meta/page/${params.page}`, { fetch }); - if (resp.status < 200 || resp.status > 299) { - const err = await resp.json(); - if ("code" in err) throw new ApiError(err); - else throw new ApiError(); - } - - const pageText = await resp.text(); - return { page: params.page, text: pageText }; -}; diff --git a/Foxnouns.Frontend/src/routes/page/[page]/+page.svelte b/Foxnouns.Frontend/src/routes/page/[page]/+page.svelte deleted file mode 100644 index 3af7b11..0000000 --- a/Foxnouns.Frontend/src/routes/page/[page]/+page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - - - {title} • pronouns.cc - - -
    - - {@html md} -
    diff --git a/Foxnouns.Frontend/src/routes/report/[id]/+page.server.ts b/Foxnouns.Frontend/src/routes/report/[id]/+page.server.ts deleted file mode 100644 index 5d36696..0000000 --- a/Foxnouns.Frontend/src/routes/report/[id]/+page.server.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError from "$api/error.js"; -import type { Member } from "$api/models/member.js"; -import { type CreateReportRequest, ReportReason } from "$api/models/moderation.js"; -import type { PartialUser, User } from "$api/models/user.js"; -import log from "$lib/log.js"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent, params, fetch, cookies, url }) => { - const { meUser } = await parent(); - if (!meUser) redirect(303, "/"); - - let user: PartialUser; - let member: Member | null = null; - if (url.searchParams.has("member")) { - const resp = await apiRequest( - "GET", - `/users/${params.id}/members/${url.searchParams.get("member")}`, - { fetch, cookies }, - ); - - user = resp.user; - member = resp; - } else { - user = await apiRequest("GET", `/users/${params.id}`, { fetch, cookies }); - } - - if (meUser.id === user.id) redirect(303, "/"); - - return { user, member }; -}; - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - - const targetIsMember = body.get("target-type") === "member"; - const target = body.get("target-id") as string; - const reason = body.get("reason") as ReportReason; - const context = body.get("context") as string | null; - - const url = targetIsMember - ? `/moderation/report-member/${target}` - : `/moderation/report-user/${target}`; - - try { - await fastRequest("POST", url, { - body: { reason, context }, - fetch, - cookies, - }); - - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("error reporting user or member %s:", target, e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/report/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/report/[id]/+page.svelte deleted file mode 100644 index a4ce0ac..0000000 --- a/Foxnouns.Frontend/src/routes/report/[id]/+page.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - - - {$t("report.title", { name })} • pronouns.cc - - -
    -
    -

    {$t("report.title", { name })}

    - - - - -

    {$t("report.reason-label")}

    -
    - {#each reasons as reason} -
    -
    - - -
    -
    - {/each} -
    - -

    - {$t("report.context-label")} - -

    - - -
    - - {$t("cancel")} -
    - -
    diff --git a/Foxnouns.Frontend/src/routes/settings/+layout.server.ts b/Foxnouns.Frontend/src/routes/settings/+layout.server.ts deleted file mode 100644 index 95228f0..0000000 --- a/Foxnouns.Frontend/src/routes/settings/+layout.server.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent }) => { - const data = await parent(); - if (!data.meUser) redirect(303, "/auth/log-in?alert=auth-required"); - - return { user: data.meUser!, token: data.token! }; -}; diff --git a/Foxnouns.Frontend/src/routes/settings/+layout.svelte b/Foxnouns.Frontend/src/routes/settings/+layout.svelte deleted file mode 100644 index 8d660ea..0000000 --- a/Foxnouns.Frontend/src/routes/settings/+layout.svelte +++ /dev/null @@ -1,54 +0,0 @@ - - - - {$t("title.settings")} • pronouns.cc - - -{#if data.meta.notice} -
    - -
    -{/if} - -
    - - - {@render children?.()} -
    diff --git a/Foxnouns.Frontend/src/routes/settings/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/+page.server.ts deleted file mode 100644 index 9e35bda..0000000 --- a/Foxnouns.Frontend/src/routes/settings/+page.server.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import { clearToken } from "$lib"; -import { redirect } from "@sveltejs/kit"; - -export const actions = { - logout: async ({ cookies }) => { - clearToken(cookies); - redirect(303, "/"); - }, - changeUsername: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const username = body.get("username") as string | null; - if (username == null) - return { - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid username", - } as RawApiError, - ok: false, - }; - - try { - await fastRequest("PATCH", "/users/@me", { - fetch, - cookies, - body: { username }, - }); - - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/+page.svelte b/Foxnouns.Frontend/src/routes/settings/+page.svelte deleted file mode 100644 index d5f90ac..0000000 --- a/Foxnouns.Frontend/src/routes/settings/+page.svelte +++ /dev/null @@ -1,158 +0,0 @@ - - -

    {$t("settings.general-information-tab")}

    - -{#if data.user.deleted} -
    - {#if !data.user.suspended} -
    -

    {$t("settings.reactivate-header")}

    -

    - {$t("settings.reactivate-explanation")} -

    - - {$t("settings.reactivate-button")} - -
    - {/if} -
    -

    {$t("settings.force-delete-header")}

    -

    - {$t("settings.force-delete-explanation")} - - {$t("settings.force-delete-warning")} - -

    - - {$t("settings.force-delete-button")} - -
    -
    -{/if} - -
    -
    -
    {$t("settings.change-username-header")}
    -
    - - - - - - - {#if form?.ok} -

    - - {$t("settings.username-update-success")} -

    - {:else if usernameError} -

    - - {$t("settings.username-update-error", { message: usernameError.message })} -

    - {:else if form?.error} - - {/if} - -

    - - {$t("settings.username-change-hint")} -

    -
    -
    -
    {$t("settings.avatar")}
    - -

    - {$t("settings.change-avatar-link")} -

    -
    -
    - -
    -

    {$t("settings.log-out-title")}

    -

    {$t("settings.log-out-hint")}

    -
    - -
    -
    - -
    -

    {$t("settings.force-log-out-title")}

    -

    {$t("settings.force-log-out-hint")}

    - {$t("settings.force-log-out-button")} -
    - -{#if !data.user.deleted} -
    -

    {$t("settings.soft-delete-header")}

    -

    {$t("settings.soft-delete-hint")}

    - {$t("settings.soft-delete-button")} -
    -{/if} - -
    -

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

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {$t("settings.table-id")} - {data.user.id} -
    {$t("settings.table-created-at")}{createdAt.toLocaleString(DateTime.DATETIME_MED)}
    {$t("settings.table-member-count")} - {data.user.members.length}/{data.meta.limits.member_count} -
    {$t("settings.table-member-list-hidden")}{data.user.member_list_hidden ? $t("yes") : $t("no")}
    {$t("settings.table-custom-preferences")} - {Object.keys(data.user.custom_preferences).length}/{data.meta.limits.custom_preferences} -
    {$t("settings.table-role")} - {data.user.role} -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts deleted file mode 100644 index 24b62a3..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error.js"; -import type { AuthUrls } from "$api/models/auth"; -import { alertKey } from "$lib"; -import log from "$lib/log"; - -export const load = async ({ fetch, url }) => { - const urls = await apiRequest("POST", "/auth/urls", { fetch, isInternal: true }); - return { urls, alertKey: alertKey(url) }; -}; - -export const actions = { - password: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const current = body.get("current") as string | null; - const password = body.get("password") as string | null; - const password2 = body.get("confirm-password") as string | null; - - if (password !== password2) { - return { - ok: false, - error: { - status: 400, - code: ErrorCode.BadRequest, - message: "Passwords do not match", - } as RawApiError, - }; - } - - try { - await fastRequest("POST", "/auth/email/change-password", { - body: { current, new: password }, - isInternal: true, - fetch, - cookies, - }); - - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("error changing password:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/auth/+page.svelte b/Foxnouns.Frontend/src/routes/settings/auth/+page.svelte deleted file mode 100644 index 84722b7..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/+page.svelte +++ /dev/null @@ -1,60 +0,0 @@ - - - - -{#if data.urls.email_enabled} - -{/if} -{#if data.urls.discord} -

    Discord accounts

    - -{/if} -{#if data.urls.google} -

    Google accounts

    - -{/if} -{#if data.urls.tumblr} -

    Tumblr accounts

    - -{/if} -

    Fediverse accounts

    - diff --git a/Foxnouns.Frontend/src/routes/settings/auth/add-discord/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/add-discord/+page.server.ts deleted file mode 100644 index 543b51e..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/add-discord/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { apiRequest } from "$api"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ fetch, cookies }) => { - const { url } = await apiRequest<{ url: string }>("GET", "/auth/discord/add-account", { - isInternal: true, - fetch, - cookies, - }); - - redirect(303, url); -}; diff --git a/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.server.ts deleted file mode 100644 index 7aa4c73..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.server.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error.js"; -import log from "$lib/log.js"; - -export const load = async ({ parent }) => { - const { user } = await parent(); - return { firstEmail: user.auth_methods.filter((a) => a.type === "EMAIL").length === 0 }; -}; - -export const actions = { - add: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const email = body.get("email") as string; - const password = body.get("password") as string | null; - const password2 = body.get("confirm-password") as string | null; - - if (password2 && password !== password2) { - return { - ok: false, - error: { - status: 400, - code: ErrorCode.BadRequest, - message: "Passwords do not match", - } as RawApiError, - }; - } - - try { - await fastRequest("POST", "/auth/email/add-account", { - body: { email, password }, - isInternal: true, - fetch, - cookies, - }); - - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("error adding email address to account:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.svelte b/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.svelte deleted file mode 100644 index 8e8d16a..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.svelte +++ /dev/null @@ -1,48 +0,0 @@ - - -
    -

    {$t("auth.link-email-header")}

    - - - -
    -
    - - -
    -
    - - -
    - {#if data.firstEmail} -
    - - -
    - {/if} - - -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/auth/add-fediverse/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/add-fediverse/+page.server.ts deleted file mode 100644 index 5c3327d..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/add-fediverse/+page.server.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { apiRequest } from "$api"; -import { redirect } from "@sveltejs/kit"; - -export const actions = { - add: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const instance = body.get("instance") as string; - - const { url } = await apiRequest<{ url: string }>( - "GET", - `/auth/fediverse/add-account?instance=${encodeURIComponent(instance)}`, - { - isInternal: true, - fetch, - cookies, - }, - ); - - redirect(303, url); - }, - forceRefresh: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const instance = body.get("instance") as string; - - const { url } = await apiRequest<{ url: string }>( - "GET", - `/auth/fediverse/add-account?instance=${encodeURIComponent(instance)}&force-refresh=true`, - { - isInternal: true, - fetch, - cookies, - }, - ); - - redirect(303, url); - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/auth/add-fediverse/+page.svelte b/Foxnouns.Frontend/src/routes/settings/auth/add-fediverse/+page.svelte deleted file mode 100644 index 36beec1..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/add-fediverse/+page.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - -
    -

    Link a new Fediverse account

    - -
    - - - - -

    - {$t("auth.log-in-with-fediverse-error-blurb")} - -

    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/auth/add-google/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/add-google/+page.server.ts deleted file mode 100644 index ca07805..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/add-google/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { apiRequest } from "$api"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ fetch, cookies }) => { - const { url } = await apiRequest<{ url: string }>("GET", "/auth/google/add-account", { - isInternal: true, - fetch, - cookies, - }); - - redirect(303, url); -}; diff --git a/Foxnouns.Frontend/src/routes/settings/auth/add-tumblr/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/add-tumblr/+page.server.ts deleted file mode 100644 index 75421b8..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/add-tumblr/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { apiRequest } from "$api"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ fetch, cookies }) => { - const { url } = await apiRequest<{ url: string }>("GET", "/auth/tumblr/add-account", { - isInternal: true, - fetch, - cookies, - }); - - redirect(303, url); -}; diff --git a/Foxnouns.Frontend/src/routes/settings/auth/remove-method/[id]/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/remove-method/[id]/+page.server.ts deleted file mode 100644 index b2c6318..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/remove-method/[id]/+page.server.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError, { ErrorCode } from "$api/error.js"; -import log from "$lib/log.js"; -import { error, isRedirect, redirect } from "@sveltejs/kit"; - -export const load = async ({ parent, params }) => { - const data = await parent(); - if (data.user.auth_methods.length < 2) { - error(403, { - message: "You cannot remove your last authentication method.", - status: 403, - code: ErrorCode.LastAuthMethod, - }); - } - - const authMethod = data.meUser!.auth_methods.find((m) => m.id === params.id); - if (!authMethod) { - error(404, { - message: "No authentication method with that ID found.", - status: 404, - code: ErrorCode.GenericApiError, - }); - } - - return { authMethod }; -}; - -export const actions = { - default: async ({ params, fetch, cookies }) => { - try { - fastRequest("DELETE", "/auth/methods/" + params.id, { fetch, cookies, isInternal: true }); - redirect(303, "/settings/auth?alert=auth-method-remove-success"); - } catch (e) { - if (isRedirect(e)) throw e; - if (e instanceof ApiError) return { error: e.obj }; - log.error("Could not remove auth method %s:", params.id, e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/auth/remove-method/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/settings/auth/remove-method/[id]/+page.svelte deleted file mode 100644 index 22b40ad..0000000 --- a/Foxnouns.Frontend/src/routes/settings/auth/remove-method/[id]/+page.svelte +++ /dev/null @@ -1,44 +0,0 @@ - - - - {unlinkHeader(data.authMethod.type)} • pronouns.cc - - -
    -

    {unlinkHeader(data.authMethod.type)}

    - {#if form?.error} - - {/if} -

    - {$t("auth.unlink-confirmation-1", { - username: data.authMethod.remote_username || data.authMethod.remote_id, - })} - {$t("auth.unlink-confirmation-2")} -

    -
    - -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts deleted file mode 100644 index 4ac1d19..0000000 --- a/Foxnouns.Frontend/src/routes/settings/delete/+page.server.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import { clearToken } from "$lib"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent }) => { - const { meUser } = await parent(); - if (!meUser) redirect(303, "/"); - - if (meUser.deleted) - throw new ApiError({ - message: "You cannot use this page.", - status: 403, - code: ErrorCode.Forbidden, - }); - - return { user: meUser! }; -}; - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const username = body.get("username") as string; - const currentUsername = body.get("current-username") as string; - - if (!username || username !== currentUsername) { - return { - ok: false, - error: { - message: "Username doesn't match your username.", - status: 400, - code: ErrorCode.BadRequest, - } as RawApiError, - }; - } - - await fastRequest("POST", "/self-delete/delete", { fetch, cookies, isInternal: true }); - clearToken(cookies); - redirect(303, "/settings/delete/success"); - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte deleted file mode 100644 index cb5fec2..0000000 --- a/Foxnouns.Frontend/src/routes/settings/delete/+page@.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - - - {$t("settings.soft-delete-page-header")} • pronouns.cc - - -
    -
    -

    {$t("settings.soft-delete-page-header")}

    - -

    - {$t("settings.soft-delete-page-explanation")} -

    - -
      -
    • {$t("settings.soft-delete-90-days")}
    • -
    • - {$t("settings.soft-delete-can-reactivate")} -
    • -
    • {$t("settings.soft-delete-keep-username")}
    • -
    • - {$t("settings.soft-delete-can-delete-permanently")} -
    • -
    - -
    - -

    - {$t("settings.soft-delete-input-label", { username: data.user.username })} - - -

    -
    - - {$t("settings.force-delete-page-cancel")} -
    - -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/delete/success/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/delete/success/+page@.svelte deleted file mode 100644 index 9b35518..0000000 --- a/Foxnouns.Frontend/src/routes/settings/delete/success/+page@.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - {$t("settings.soft-delete-page-header")} • pronouns.cc - - -
    -
    -

    {$t("settings.account-is-deactivated-header")}

    -

    - {$t("settings.account-is-deactivated-description")} -

    -

    {$t("settings.account-is-deleted-close-page")}

    -

    - {$t("error.back-to-main-page-button")} -

    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/export/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/export/+page.server.ts deleted file mode 100644 index 136af01..0000000 --- a/Foxnouns.Frontend/src/routes/settings/export/+page.server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError from "$api/error.js"; -import log from "$lib/log.js"; -import { DateTime, Duration } from "luxon"; - -type Export = { url: string | null; expires_at: string | null }; - -export const load = async ({ fetch, cookies }) => { - const resp = await apiRequest("GET", "/data-exports", { - fetch, - cookies, - isInternal: true, - }); - - let canExport = true; - if (resp.expires_at) { - const created = DateTime.fromISO(resp.expires_at).minus(Duration.fromObject({ days: 15 })); - canExport = DateTime.now().diff(created, "seconds").seconds >= 86400; - } - - return { url: resp.url, expiresAt: resp.expires_at, canExport }; -}; - -export const actions = { - default: async ({ fetch, cookies }) => { - try { - fastRequest("POST", "/data-exports", { fetch, cookies, isInternal: true }); - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("Error requesting data export:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/export/+page.svelte b/Foxnouns.Frontend/src/routes/settings/export/+page.svelte deleted file mode 100644 index 54f9dfa..0000000 --- a/Foxnouns.Frontend/src/routes/settings/export/+page.svelte +++ /dev/null @@ -1,41 +0,0 @@ - - -
    -

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

    - - - -

    - {$t("settings.export-info")} -

    - -
    - -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/flags/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/flags/+page.server.ts deleted file mode 100644 index 705e918..0000000 --- a/Foxnouns.Frontend/src/routes/settings/flags/+page.server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError from "$api/error"; -import type { PrideFlag } from "$api/models/user"; -import log from "$lib/log"; -import { encode } from "base64-arraybuffer"; - -export const load = async ({ fetch, cookies }) => { - const resp = await apiRequest("GET", "/users/@me/flags", { fetch, cookies }); - - return { - flags: resp, - }; -}; - -export const actions = { - upload: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const name = body.get("name") as string; - const description = body.get("desc") as string; - const image = body.get("image") as File; - - const buffer = await image.arrayBuffer(); - const base64 = encode(buffer); - - try { - await fastRequest("POST", "/users/@me/flags", { - body: { - name, - description: description ? description : null, - image: `data:${image.type};base64,${base64}`, - }, - fetch, - cookies, - }); - return { ok: true, error: null }; - } catch (e) { - if (e instanceof ApiError) return { ok: false, error: e.obj }; - log.error("error uploading flag:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte b/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte deleted file mode 100644 index 1b15927..0000000 --- a/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte +++ /dev/null @@ -1,157 +0,0 @@ - - -

    {$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, - })} -

    - -
    - -
    - - - -{#if arr.length === 0} -
    -

    - -

    -

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

    -
    -{:else} - - {#each arr as flag (flag.id)} - - - - {flag.name} - - - {#if lastEditedFlag === flag.id}{/if} - - - {/each} - -{/if} - - diff --git a/Foxnouns.Frontend/src/routes/settings/force-delete/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/force-delete/+page.server.ts deleted file mode 100644 index 1816ce7..0000000 --- a/Foxnouns.Frontend/src/routes/settings/force-delete/+page.server.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import { clearToken } from "$lib"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent }) => { - const { meUser } = await parent(); - if (!meUser) redirect(303, "/"); - - if (!meUser.deleted) - throw new ApiError({ - message: "You cannot use this page.", - status: 403, - code: ErrorCode.Forbidden, - }); - - return { user: meUser! }; -}; - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const username = body.get("username") as string; - const currentUsername = body.get("current-username") as string; - const confirmed = !!body.get("confirm"); - - if (!username || username !== currentUsername) { - return { - ok: false, - error: { - message: "Username doesn't match your username.", - status: 400, - code: ErrorCode.BadRequest, - } as RawApiError, - }; - } - - if (!confirmed) { - return { - ok: false, - error: { - message: "You must check the box to continue.", - status: 400, - code: ErrorCode.BadRequest, - } as RawApiError, - }; - } - - await fastRequest("POST", "/self-delete/force", { fetch, cookies, isInternal: true }); - clearToken(cookies); - redirect(303, "/settings/force-delete/success"); - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/force-delete/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/force-delete/+page@.svelte deleted file mode 100644 index 4b39e62..0000000 --- a/Foxnouns.Frontend/src/routes/settings/force-delete/+page@.svelte +++ /dev/null @@ -1,68 +0,0 @@ - - - - {$t("settings.force-delete-page-header")} • pronouns.cc - - -
    -
    -

    {$t("settings.force-delete-page-header")}

    - -

    - {$t("settings.force-delete-page-explanation")} -

    - -
      -
    • {$t("settings.force-delete-immediate-delete")}
    • -
    • {$t("settings.force-delete-username-available")}
    • -
    • {$t("settings.force-delete-irreversible")}
    • -
    - -

    - {$t("settings.force-delete-export-hint")} - {$t("settings.force-delete-export-link")} -

    - -
    - -

    - {$t("settings.force-delete-input-label", { username: data.user.username })} - - -

    -
    - - -
    -
    - - {$t("settings.force-delete-page-cancel")} -
    - -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/force-delete/success/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/force-delete/success/+page@.svelte deleted file mode 100644 index 7fd5bd5..0000000 --- a/Foxnouns.Frontend/src/routes/settings/force-delete/success/+page@.svelte +++ /dev/null @@ -1,20 +0,0 @@ - - - - {$t("settings.force-delete-page-header")} • pronouns.cc - - -
    -
    -

    {$t("settings.account-is-deleted-header")}

    -

    - {$t("settings.account-is-deleted-permanently-description")} -

    -

    {$t("settings.account-is-deleted-close-page")}

    -

    - {$t("error.back-to-main-page-button")} -

    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/force-log-out/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/force-log-out/+page.server.ts deleted file mode 100644 index 0fb9f8f..0000000 --- a/Foxnouns.Frontend/src/routes/settings/force-log-out/+page.server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { fastRequest } from "$api"; -import { clearToken } from "$lib"; -import { redirect } from "@sveltejs/kit"; - -export const actions = { - default: async ({ fetch, cookies }) => { - await fastRequest("POST", "/auth/force-log-out", { isInternal: true, fetch, cookies }, true); - clearToken(cookies); - redirect(303, "/"); - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/force-log-out/+page.svelte b/Foxnouns.Frontend/src/routes/settings/force-log-out/+page.svelte deleted file mode 100644 index 6e31ddb..0000000 --- a/Foxnouns.Frontend/src/routes/settings/force-log-out/+page.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -

    {$t("settings.force-log-out-title")}

    - -

    - {$t("settings.force-log-out-confirmation")} - {$t("settings.force-log-out-warning")} -

    - -
    - -
    diff --git a/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts deleted file mode 100644 index 715610b..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -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, - ); - - return { members: data, currentPage, pageCount }; -}; diff --git a/Foxnouns.Frontend/src/routes/settings/members/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/+page.svelte deleted file mode 100644 index 6963c0c..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/+page.svelte +++ /dev/null @@ -1,49 +0,0 @@ - - -

    {$t("settings.members-tab")} ({data.user.members.length})

    - - - - - {#if canCreateMember} - - - {$t("profile.create-member-button")} - - {/if} - {#each data.members as member (member.id)} - - - {member.display_name} - {#if member.display_name !== member.name}({member.name}){/if} - - {/each} - - - diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout.server.ts b/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout.server.ts deleted file mode 100644 index 0cdf2e1..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { apiRequest } from "$api"; -import ApiError from "$api/error"; -import type { Member } from "$api/models"; -import log from "$lib/log"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent, params, fetch, cookies }) => { - const { meUser, token } = await parent(); - if (!meUser) redirect(303, "/"); - - try { - const member = await apiRequest("GET", `/users/@me/members/${params.id}`, { - fetch, - cookies, - }); - return { user: meUser, token: token!, member }; - } catch (e) { - if (e instanceof ApiError) throw e.obj; - log.error("Error trying to fetch member %s:", params.id, e); - throw e; - } -}; diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte deleted file mode 100644 index f3f4301..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte +++ /dev/null @@ -1,69 +0,0 @@ - - - - {$t("edit-profile.member-header", { name })} • pronouns.cc - - - diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts deleted file mode 100644 index 78c2c16..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { apiRequest, fastRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import type { Member } from "$api/models/member"; -import log from "$lib/log.js"; - -export const load = async ({ params, fetch, cookies }) => { - try { - const member = await apiRequest("GET", `/users/@me/members/${params.id}`, { - fetch, - cookies, - }); - return { member }; - } catch (e) { - if (e instanceof ApiError) throw e.obj; - log.error("Error trying to fetch member %s:", params.id, e); - throw e; - } -}; - -export const actions = { - changeName: async ({ params, request, fetch, cookies }) => { - const body = await request.formData(); - const name = body.get("name") as string | null; - if (!name) - return { - error: { - message: "You must pass a name.", - status: 403, - code: ErrorCode.BadRequest, - } as RawApiError, - ok: false, - }; - - try { - await fastRequest("PATCH", `/users/@me/members/${params.id}`, { - body: { name }, - fetch, - cookies, - }); - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - log.error("Error updating name for member %s:", params.id, e); - throw e; - } - }, - changeDisplayName: async ({ params, request, fetch, cookies }) => { - const body = await request.formData(); - let displayName = body.get("display-name") as string | null; - if (!displayName || displayName === "") displayName = null; - - try { - await fastRequest("PATCH", `/users/@me/members/${params.id}`, { - body: { display_name: displayName }, - fetch, - cookies, - }); - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - log.error("Error updating name for member %s:", params.id, e); - throw e; - } - }, - bio: async ({ params, request, fetch, cookies }) => { - const body = await request.formData(); - let bio = body.get("bio") as string | null; - if (!bio || bio === "") bio = null; - - try { - await fastRequest("PATCH", `/users/@me/members/${params.id}`, { - body: { bio }, - fetch, - cookies, - }); - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - log.error("Error updating bio for member %s:", params.id, e); - throw e; - } - }, - options: async ({ params, request, fetch, cookies }) => { - const body = await request.formData(); - const unlisted = !!body.get("unlisted"); - - try { - await fastRequest("PATCH", `/users/@me/members/${params.id}`, { - body: { unlisted }, - fetch, - cookies, - }); - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - log.error("Error patching member %s:", params.id, e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte deleted file mode 100644 index 0e34638..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte +++ /dev/null @@ -1,161 +0,0 @@ - - -{#if error} - -{/if} - -{#if form} - -{/if} - -
    -
    -

    {$t("settings.avatar")}

    - -
    -
    -

    {$t("edit-profile.member-name")}

    -
    - - - - -
    - -

    {$t("edit-profile.display-name")}

    -
    - - - - -
    - -

    {$t("edit-profile.sid")}

    - -
    -
    -

    {$t("edit-profile.profile-options-header")}

    -
    -
    - - -
    -

    - - {$t("edit-profile.unlisted-note")} - - {PUBLIC_BASE_URL.substring("https://".length)}/@{data.member.user.username}/{data.member - .name} - -

    -
    - -
    -
    -
    -
    -

    {$t("edit-profile.bio-tab")}

    -
    - - -
    -
    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 507289d..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte +++ /dev/null @@ -1,39 +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 e9e1c2d..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte deleted file mode 100644 index 9aa19bb..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - -
    - -
    -
    - -
    -
    - -
    diff --git a/Foxnouns.Frontend/src/routes/settings/members/new/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/new/+page.server.ts deleted file mode 100644 index 3fdf2e0..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/new/+page.server.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { apiRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import type { Member } from "$api/models/member"; -import log from "$lib/log.js"; -import { isRedirect, redirect } from "@sveltejs/kit"; - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const name = body.get("name") as string | null; - if (!name) - return { - error: { - message: "No name supplied.", - status: 403, - code: ErrorCode.BadRequest, - } as RawApiError, - }; - - try { - const member = await apiRequest("POST", "/users/@me/members", { - body: { name }, - fetch, - cookies, - }); - redirect(303, `/settings/members/${member.id}`); - } catch (e) { - if (isRedirect(e)) throw e; - if (e instanceof ApiError) return { error: e.obj }; - log.error("Could not create member:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/members/new/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/new/+page.svelte deleted file mode 100644 index 2c1fc0b..0000000 --- a/Foxnouns.Frontend/src/routes/settings/members/new/+page.svelte +++ /dev/null @@ -1,23 +0,0 @@ - - -

    {$t("settings.create-member-title")}

    - -{#if form?.error} - -{/if} - -
    -
    - - -
    - -
    diff --git a/Foxnouns.Frontend/src/routes/settings/notifications/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/notifications/+page.server.ts deleted file mode 100644 index 40470bd..0000000 --- a/Foxnouns.Frontend/src/routes/settings/notifications/+page.server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { apiRequest } from "$api"; -import type { Notification } from "$api/models/moderation"; -import { alertKey } from "$lib"; - -export const load = async ({ url, fetch, cookies }) => { - const notifications = await apiRequest("GET", "/notifications", { - fetch, - cookies, - }); - return { notifications, alertKey: alertKey(url) }; -}; diff --git a/Foxnouns.Frontend/src/routes/settings/notifications/+page.svelte b/Foxnouns.Frontend/src/routes/settings/notifications/+page.svelte deleted file mode 100644 index f3e7e27..0000000 --- a/Foxnouns.Frontend/src/routes/settings/notifications/+page.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - - -{#each data.notifications as notification (notification.id)} - -{:else} - {$t("notification.no-notifications")} -{/each} diff --git a/Foxnouns.Frontend/src/routes/settings/notifications/ack/[id]/+server.ts b/Foxnouns.Frontend/src/routes/settings/notifications/ack/[id]/+server.ts deleted file mode 100644 index a9a22fc..0000000 --- a/Foxnouns.Frontend/src/routes/settings/notifications/ack/[id]/+server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { redirect } from "@sveltejs/kit"; -import type { RequestHandler } from "./$types"; -import { fastRequest } from "$api"; -import log from "$lib/log"; - -export const GET: RequestHandler = async ({ params, fetch, cookies }) => { - try { - await fastRequest("PUT", `/notifications/${params.id}/ack`, { fetch, cookies }); - } catch (e) { - log.error("error acking notification %s:", params.id, e); - redirect(303, "/settings/notifications?alert=notif-ack-fail"); - } - redirect(303, "/settings/notifications?alert=notif-ack-successful"); -}; diff --git a/Foxnouns.Frontend/src/routes/settings/prefs/+page.svelte b/Foxnouns.Frontend/src/routes/settings/prefs/+page.svelte deleted file mode 100644 index 74f5154..0000000 --- a/Foxnouns.Frontend/src/routes/settings/prefs/+page.svelte +++ /dev/null @@ -1,111 +0,0 @@ - - -

    - {$t("settings.custom-preferences-title")} -
    - - -
    - {#if !canAdd} -
    - - {$t("editor.max-custom-preferences", { - current: customPreferences.length, - max: data.meta.limits.custom_preferences, - })} -
    - {/if} -

    - - - -
    - {#each customPreferences as _, idx} - remove(idx)} /> - {/each} -
    - -
    diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+layout.server.ts b/Foxnouns.Frontend/src/routes/settings/profile/+layout.server.ts deleted file mode 100644 index 9d7ef68..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/+layout.server.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent }) => { - const { meUser, token } = await parent(); - if (!meUser) redirect(303, "/"); - return { user: meUser!, token: token! }; -}; diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte b/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte deleted file mode 100644 index f0edb81..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte +++ /dev/null @@ -1,67 +0,0 @@ - - - - {$t("edit-profile.user-header")} • pronouns.cc - - - diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts deleted file mode 100644 index 59c7011..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError from "$api/error"; -import log from "$lib/log.js"; - -export const actions = { - options: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - let memberTitle = body.get("member-title") as string | null; - if (!memberTitle || memberTitle === "") memberTitle = null; - - let timezone = body.get("timezone") as string | null; - if (!timezone || timezone === "") timezone = null; - - const hideMemberList = !!body.get("hide-member-list"); - - try { - await fastRequest("PATCH", "/users/@me", { - body: { timezone, member_title: memberTitle, member_list_hidden: hideMemberList }, - fetch, - cookies, - }); - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - log.error("Error patching user:", e); - throw e; - } - }, - changeDisplayName: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - let displayName = body.get("display-name") as string | null; - if (!displayName || displayName === "") displayName = null; - - try { - await fastRequest("PATCH", "/users/@me", { - body: { display_name: displayName }, - fetch, - cookies, - }); - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - log.error("Error patching user:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte deleted file mode 100644 index 8355ec1..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte +++ /dev/null @@ -1,200 +0,0 @@ - - -{#if error} - -{/if} - -{#if form} -
    - -
    -{/if} - -
    -
    -

    {$t("settings.avatar")}

    - -
    -
    -

    {$t("edit-profile.username")}

    - -

    - - {$t("edit-profile.change-username-info")} - {$t("edit-profile.change-username-link")} -

    - -

    {$t("edit-profile.display-name")}

    -
    - - - - -
    - -

    {$t("edit-profile.sid")}

    - -
    -
    - -
    -

    {$t("edit-profile.profile-options-header")}

    -
    -
    - - -

    - - {$t("edit-profile.member-header-info")} -

    -
    -
    - - - - - {#each validTimezones as timezone}{/each} - - - - {#if tz && tz !== "" && validTimezones.includes(tz)} -
    - {$t("edit-profile.timezone-preview")} - - {currentTime} (UTC{displayTimezone}) -
    - {/if} -

    - - {$t("edit-profile.timezone-info")} -

    -
    -
    - - -
    -

    - - {$t("edit-profile.hide-member-list-info")} -

    -
    - -
    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.server.ts deleted file mode 100644 index bb86f7e..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.server.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError from "$api/error"; -import log from "$lib/log.js"; - -export const actions = { - default: async ({ request, fetch, cookies }) => { - const body = await request.formData(); - const bio = body.get("bio") as string | null; - - try { - await fastRequest("PATCH", "/users/@me", { body: { bio }, fetch, cookies }); - return { error: null, ok: true }; - } catch (e) { - if (e instanceof ApiError) return { error: e.obj, ok: false }; - log.error("Error updating bio:", e); - throw e; - } - }, -}; diff --git a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte deleted file mode 100644 index 91e452b..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - -

    {$t("edit-profile.bio-tab")}

    - -
    - - diff --git a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte deleted file mode 100644 index b15d7c4..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - 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 b56d0c2..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte +++ /dev/null @@ -1,56 +0,0 @@ - - - - - diff --git a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte deleted file mode 100644 index 2703748..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - -
    - -
    -
    - -
    -
    - -
    diff --git a/Foxnouns.Frontend/src/routes/settings/reactivate/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/reactivate/+page.server.ts deleted file mode 100644 index 0ac29ae..0000000 --- a/Foxnouns.Frontend/src/routes/settings/reactivate/+page.server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { fastRequest } from "$api"; -import ApiError, { ErrorCode } from "$api/error"; -import { redirect } from "@sveltejs/kit"; - -export const load = async ({ parent, fetch, cookies }) => { - const { meUser } = await parent(); - if (!meUser) redirect(303, "/"); - - if (meUser.suspended || !meUser.deleted) - throw new ApiError({ - message: "You cannot use this page.", - status: 403, - code: ErrorCode.Forbidden, - }); - - await fastRequest("POST", "/self-delete/undelete", { - fetch, - cookies, - isInternal: true, - }); - - return { user: meUser! }; -}; diff --git a/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte b/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte deleted file mode 100644 index cf70c4b..0000000 --- a/Foxnouns.Frontend/src/routes/settings/reactivate/+page@.svelte +++ /dev/null @@ -1,22 +0,0 @@ - - -
    -
    -

    {$t("settings.reactivated-header")}

    - -

    {$t("settings.reactivated-explanation")}

    - - -
    -
    diff --git a/Foxnouns.Frontend/static/favicon.svg b/Foxnouns.Frontend/static/favicon.svg deleted file mode 100644 index 11e664f..0000000 --- a/Foxnouns.Frontend/static/favicon.svg +++ /dev/null @@ -1,2 +0,0 @@ - -image/svg+xml \ No newline at end of file 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/Foxnouns.Frontend/svelte.config.js b/Foxnouns.Frontend/svelte.config.js deleted file mode 100644 index f4ddf37..0000000 --- a/Foxnouns.Frontend/svelte.config.js +++ /dev/null @@ -1,39 +0,0 @@ -import adapter from "@sveltejs/adapter-node"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; -import * as path from "node:path"; - -import { config as dotenv } from "dotenv"; -dotenv({ - path: [path.resolve(process.cwd(), ".env"), path.resolve(process.cwd(), ".env.local")], -}); - -console.log(process.env.NODE_ENV); -const isProd = process.env.NODE_ENV === "production"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://svelte.dev/docs/kit/integrations - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://svelte.dev/docs/kit/adapters for more information about adapters. - adapter: adapter(), - alias: { - $api: "src/lib/api", - $components: "src/lib/components", - }, - csrf: { - // using Caddy as a reverse proxy + CSRF protection breaks forms *in development*, not in production - // we only disable it during development, during building NODE_ENV == production - checkOrigin: process.env.NODE_ENV !== "development", - }, - paths: { - assets: isProd ? process.env.PRIVATE_ASSETS_PREFIX || "" : "", - }, - }, -}; - -export default config; diff --git a/Foxnouns.Frontend/tsconfig.json b/Foxnouns.Frontend/tsconfig.json deleted file mode 100644 index 0b2d886..0000000 --- a/Foxnouns.Frontend/tsconfig.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler" - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in -} diff --git a/Foxnouns.Frontend/vite.config.ts b/Foxnouns.Frontend/vite.config.ts deleted file mode 100644 index 6b9eb5d..0000000 --- a/Foxnouns.Frontend/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [sveltekit()], -}); diff --git a/Foxnouns.NET.sln b/Foxnouns.NET.sln index de091c2..3b119c5 100644 --- a/Foxnouns.NET.sln +++ b/Foxnouns.NET.sln @@ -5,8 +5,6 @@ VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foxnouns.Backend", "Foxnouns.Backend\Foxnouns.Backend.csproj", "{439E3E38-5AEF-4F73-AD57-E32057B3FC7F}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foxnouns.DataMigrator", "Foxnouns.DataMigrator\Foxnouns.DataMigrator.csproj", "{1909CCB6-F9B0-4C36-9618-82CC3A41C5B0}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -20,9 +18,5 @@ Global {439E3E38-5AEF-4F73-AD57-E32057B3FC7F}.Debug|Any CPU.Build.0 = Debug|Any CPU {439E3E38-5AEF-4F73-AD57-E32057B3FC7F}.Release|Any CPU.ActiveCfg = Release|Any CPU {439E3E38-5AEF-4F73-AD57-E32057B3FC7F}.Release|Any CPU.Build.0 = Release|Any CPU - {1909CCB6-F9B0-4C36-9618-82CC3A41C5B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1909CCB6-F9B0-4C36-9618-82CC3A41C5B0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1909CCB6-F9B0-4C36-9618-82CC3A41C5B0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1909CCB6-F9B0-4C36-9618-82CC3A41C5B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Foxnouns.NET.sln.DotSettings b/Foxnouns.NET.sln.DotSettings deleted file mode 100644 index 7b2efd3..0000000 --- a/Foxnouns.NET.sln.DotSettings +++ /dev/null @@ -1,19 +0,0 @@ - - UseVarWhenEvident - Copyright (C) 2023-present sam/u1f320 (vulpine.solutions) - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published -by the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see <https://www.gnu.org/licenses/>. - True - True - True \ No newline at end of file diff --git a/Foxnouns.RateLimiter/Dockerfile b/Foxnouns.RateLimiter/Dockerfile deleted file mode 100644 index b88d028..0000000 --- a/Foxnouns.RateLimiter/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM docker.io/golang:latest AS builder -WORKDIR /build -EXPOSE 5003 - -COPY . ./ -RUN go mod download -x -ENV CGO_ENABLED 0 -RUN go build -v -o rate - -FROM alpine:latest -RUN apk --no-cache add ca-certificates - -WORKDIR /app -COPY --from=builder /build/rate rate - -CMD ["/app/rate"] diff --git a/Foxnouns.RateLimiter/README.md b/Foxnouns.RateLimiter/README.md deleted file mode 100644 index d65f0f1..0000000 --- a/Foxnouns.RateLimiter/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Rate limiting proxy - -This is a service that's meant to sit between nginx (or another reverse proxy) and Foxnouns.Backend. -To configure, copy `proxy-config.example.json` to your working directory, rename it to `proxy-config.json`, -and change any keys you need to. - -Build with `go build -v .` and run with `./rate` diff --git a/Foxnouns.RateLimiter/go.mod b/Foxnouns.RateLimiter/go.mod deleted file mode 100644 index 4636156..0000000 --- a/Foxnouns.RateLimiter/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module code.vulpine.solutions/sam/Foxnouns.NET - -go 1.20 - -require ( - github.com/go-chi/httprate v0.14.1 -) - -require github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/Foxnouns.RateLimiter/go.sum b/Foxnouns.RateLimiter/go.sum deleted file mode 100644 index cfbff79..0000000 --- a/Foxnouns.RateLimiter/go.sum +++ /dev/null @@ -1,7 +0,0 @@ -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs= -github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/Foxnouns.RateLimiter/handler.go b/Foxnouns.RateLimiter/handler.go deleted file mode 100644 index 311b5b8..0000000 --- a/Foxnouns.RateLimiter/handler.go +++ /dev/null @@ -1,197 +0,0 @@ -package main - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "net/http/httputil" - "strconv" - "strings" - "time" -) - -const notFoundError = `{"status":404,"code":"NOT_FOUND","message":"Not found"}` -const internalServerErrorTemplate = `{"status":500,"code":"INTERNAL_SERVER_ERROR","error_id":"%v","message":"Internal server error"}` -const rateLimitedErrorTemplate = `{"status":429,"code":"RATE_LIMITED","reset":%v,"message":"You are being rate limited"}` - -// error ID chosen by fair duckduckgo search, guaranteed to be random -// (we just need to return *any* error ID, this will do) -const errorID = "951c03eadb6b474db35c23d47a10f6c0" - -type Handler struct { - Port int `json:"port"` - ProxyTarget string `json:"proxy_target"` - Debug bool `json:"debug"` - PoweredBy string `json:"powered_by"` - - limiter *Limiter - proxy *httputil.ReverseProxy - client *http.Client -} - -func (hn *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if hn.PoweredBy != "" { - w.Header().Set("X-Powered-By", hn.PoweredBy) - } - - // all public api endpoints are prefixed with this - if !strings.HasPrefix(r.URL.Path, "/api/v2") && !strings.HasPrefix(r.URL.Path, "/api/v1") { - w.WriteHeader(http.StatusNotFound) - return - } - - data, err := hn.requestData(r) - if err != nil { - if rde, ok := err.(requestDataError); ok { - switch rde.Type { - case "badRequest": - if hn.Debug { - log.Printf("Bad request error for path %v\n", r.URL.Path) - } - - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("Content-Length", strconv.Itoa(len(notFoundError))) - w.WriteHeader(http.StatusNotFound) - _, _ = w.Write([]byte(notFoundError)) - - return - case "internalServerError": - respBody := fmt.Sprintf(internalServerErrorTemplate, rde.ErrorID) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("Content-Length", strconv.Itoa(len(respBody))) - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(respBody)) - - return - } - } - - log.Printf("internal error while parsing request data response: %v\n", err) - - respBody := fmt.Sprintf(internalServerErrorTemplate, errorID) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("Content-Length", strconv.Itoa(len(respBody))) - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(respBody)) - - return - } - - if hn.Debug { - log.Printf("proxying request to %v %v", r.Method, data.Template) - } - - isLimited, err := hn.limiter.TryLimit(w, r, data.UserID, data.Template) - if err != nil { - log.Printf("error checking rate limit: %v\n", err) - - respBody := fmt.Sprintf(internalServerErrorTemplate, errorID) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("Content-Length", strconv.Itoa(len(respBody))) - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(respBody)) - return - } - - if isLimited { - var resetTime time.Time - if reset := getReset(w); reset == 0 { - resetTime = time.Now().UTC().Add(10 * time.Second) - } else { - resetTime = time.Unix(reset, 0) - } - - respBody := fmt.Sprintf(rateLimitedErrorTemplate, resetTime.UnixMilli()) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Header().Set("Content-Length", strconv.Itoa(len(respBody))) - w.WriteHeader(http.StatusTooManyRequests) - _, _ = w.Write([]byte(respBody)) - return - } - - hn.proxy.ServeHTTP(w, r) -} - -func (hn *Handler) requestData(r *http.Request) (data requestDataResponse, err error) { - var token *string - if header := r.Header.Get("Authorization"); header != "" { - token = &header - } - - url := hn.ProxyTarget + "/api/internal/request-data" - - reqData, err := json.Marshal(requestDataRequest{Token: token, Method: r.Method, Path: r.URL.Path}) - if err != nil { - return data, fmt.Errorf("marshaling request json: %w", err) - } - - req, err := http.NewRequestWithContext(r.Context(), "POST", url, bytes.NewReader(reqData)) - if err != nil { - return data, fmt.Errorf("creating request: %w", err) - } - req.Header.Set("User-Agent", "Foxnouns.NET/rate") - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - resp, err := hn.client.Do(req) - if err != nil { - return data, fmt.Errorf("making request: %w", err) - } - defer resp.Body.Close() - - // If we got a bad request error, that means the endpoint wasn't found, and we should tell the client that - if resp.StatusCode == http.StatusBadRequest { - return data, requestDataError{Type: "badRequest"} - } - - // If we got an internal server error we have to forward the error ID - if resp.StatusCode == http.StatusInternalServerError { - b, err := io.ReadAll(resp.Body) - if err != nil { - return data, fmt.Errorf("reading internal server error body: %w", err) - } - - var fne foxnounsError - err = json.Unmarshal(b, &fne) - if err != nil { - return data, fmt.Errorf("unmarshaling internal server error: %w", err) - } - - return data, requestDataError{Type: "internalServerError", ErrorID: fne.ErrorId} - } - - b, err := io.ReadAll(resp.Body) - if err != nil { - return data, fmt.Errorf("reading body: %w", err) - } - - err = json.Unmarshal(b, &data) - if err != nil { - return data, fmt.Errorf("unmarshaling data: %w", err) - } - return data, nil -} - -type requestDataRequest struct { - Token *string `json:"token"` - Method string `json:"method"` - Path string `json:"path"` -} - -type requestDataResponse struct { - UserID *string `json:"user_id"` - Template string `json:"template"` -} - -type foxnounsError struct { - ErrorId string `json:"error_id"` -} - -type requestDataError struct { - Type string - ErrorID string -} - -func (e requestDataError) Error() string { return "request data error: " + e.Type } diff --git a/Foxnouns.RateLimiter/main.go b/Foxnouns.RateLimiter/main.go deleted file mode 100644 index 6901085..0000000 --- a/Foxnouns.RateLimiter/main.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "net/http" - "net/http/httputil" - "net/url" - "os" - "strconv" -) - -func main() { - // read config file and parse it - confB, err := os.ReadFile("proxy-config.json") - if err != nil { - log.Fatalf("reading config.json: %v", err) - } - - hn := &Handler{} - err = json.Unmarshal(confB, hn) - if err != nil { - log.Fatalf("unmarshaling config.json: %v", err) - } - - // Override port from environment if it's set - if portEnv := os.Getenv("PORT"); portEnv != "" { - port, err := strconv.Atoi(portEnv) - if err == nil { - hn.Port = port - } - } - - proxyURL, err := url.Parse(hn.ProxyTarget) - if err != nil { - log.Fatalf("parsing proxy_target as URL: %v", err) - } - hn.proxy = &httputil.ReverseProxy{ - Rewrite: func(pr *httputil.ProxyRequest) { - pr.SetURL(proxyURL) - pr.Out.Host = pr.In.Host - }, - } - hn.limiter = NewLimiter() - hn.client = &http.Client{} - - log.Printf("serving on port %v", hn.Port) - - err = http.ListenAndServe(":"+strconv.Itoa(hn.Port), hn) - if err != nil { - log.Fatalf("listening on port %v: %v", hn.Port, err) - } - -} diff --git a/Foxnouns.RateLimiter/proxy-config.example.json b/Foxnouns.RateLimiter/proxy-config.example.json deleted file mode 100644 index 1ec9e59..0000000 --- a/Foxnouns.RateLimiter/proxy-config.example.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "port": 5003, - "proxy_target": "http://localhost:6000", - "debug": true, - "powered_by": "5 gay rats" -} diff --git a/Foxnouns.RateLimiter/rate_limiter.go b/Foxnouns.RateLimiter/rate_limiter.go deleted file mode 100644 index 6284243..0000000 --- a/Foxnouns.RateLimiter/rate_limiter.go +++ /dev/null @@ -1,139 +0,0 @@ -package main - -import ( - "encoding/hex" - "fmt" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/go-chi/httprate" -) - -func userOrIp(r *http.Request, userId *string) (string, error) { - if userId != nil { - return *userId, nil - } - - return httprate.KeyByRealIP(r) -} - -type Limiter struct { - bucketRateLimiters map[bucketKey]*httprate.RateLimiter - globalRateLimiters map[string]*httprate.RateLimiter - - bucketMu *sync.Mutex - globalMu *sync.Mutex -} - -type LimitData struct { - Global bool - Bucket string - Remaining int - Reset int -} - -type bucketKey struct { - user, bucket string -} - -func NewLimiter() *Limiter { - return &Limiter{ - bucketRateLimiters: make(map[bucketKey]*httprate.RateLimiter), - globalRateLimiters: make(map[string]*httprate.RateLimiter), - - bucketMu: new(sync.Mutex), - globalMu: new(sync.Mutex), - } -} - -// TryLimit handles rate limiting for a request. -// If this function returns true, a rate limit was hit, and the request should be aborted with a 429 status. -func (l *Limiter) TryLimit(w http.ResponseWriter, r *http.Request, userID *string, template string) (bool, error) { - user, err := userOrIp(r, userID) - if err != nil { - return false, fmt.Errorf("getting user or IP: %w", err) - } - - // Check the globalLimiter rate limit for this user - globalLimiter := l.globalLimiter(user) - if globalLimiter.OnLimit(w, r, "") { - w.Header().Add("X-RateLimit-Global", "true") - return true, nil - } - - bucket := requestBucket(r.Method, template) - w.Header().Add("X-RateLimit-Bucket", bucket) - - bucketLimiter := l.bucketLimiter(user, r.Method, bucket) - if bucketLimiter.OnLimit(w, r, "") { - return true, nil - } - - return false, nil -} - -func getReset(w http.ResponseWriter) int64 { - header := w.Header().Get("X-RateLimit-Reset") - if header == "" { - return 0 - } - - i, err := strconv.ParseInt(header, 10, 64) - if err != nil { - return 0 - } - - return i -} - -func requestBucket(method, template string) string { - return hex.EncodeToString([]byte(method + "-" + template)) -} - -func (l *Limiter) globalLimiter(user string) *httprate.RateLimiter { - l.globalMu.Lock() - defer l.globalMu.Unlock() - - limiter, ok := l.globalRateLimiters[user] - if ok { - return limiter - } - - limiter = httprate.NewRateLimiter(20, time.Second) - l.globalRateLimiters[user] = limiter - return limiter -} - -func (l *Limiter) bucketLimiter(user, method, bucket string) *httprate.RateLimiter { - l.bucketMu.Lock() - defer l.bucketMu.Unlock() - - limiter, ok := l.bucketRateLimiters[bucketKey{user, bucket}] - if ok { - return limiter - } - - requestLimit, windowLength := requestLimitFor(method) - - limiter = httprate.NewRateLimiter(requestLimit, windowLength) - l.bucketRateLimiters[bucketKey{user, bucket}] = limiter - return limiter -} - -// returns the request limit and window length for the given method. -// methods that change state have lower rate limits -func requestLimitFor(method string) (int, time.Duration) { - switch strings.ToUpper(method) { - case "PATCH", "POST": - return 3, time.Second - case "DELETE": - return 2, 5 * time.Second - case "GET": - return 10, time.Second - default: - return 5, time.Second - } -} diff --git a/README.md b/README.md deleted file mode 100644 index 146591f..0000000 --- a/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Foxnouns.NET - -Rewrite of pronouns.cc's codebase in C#, using SvelteKit for the frontend. -Still very work-in-progress, but a large portion of the backend is functional. - -## License - - Copyright (C) 2024 sam - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -## Acknowledgements - -Codebases I've used for inspiration/figuring things out: - -- [Iceshrimp.NET](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET) -- [PluralKit](https://github.com/PluralKit/PluralKit) - -Code taken entirely or almost entirely from external sources: - -- The functions in the `AddSids` migration, - 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 diff --git a/STYLE.md b/STYLE.md deleted file mode 100644 index b7a3c00..0000000 --- a/STYLE.md +++ /dev/null @@ -1,23 +0,0 @@ -# Code style - -## C# code style - -- Code should be formatted with `dotnet format` or Rider's built-in formatter. -- Variables should always be declared with their type name, unless the type is obvious from the declaration. - (For example, `var stream = new Stream()` or `var db = services.GetRequiredService()`) - -### Naming - -- Service values should be named the same as the type, but camel case, if the name of the service does *not* - in a verb (i.e. a variable of type `KeyCacheService` should be named `keyCacheService`). - If the name of the service *does* end in a verb, the final "service" should be omitted - (i.e. a variable of type `UserRendererService` should be named `userRenderer`). -- Interface values should be named the same as the type, but camel case, without the leading `I` - (i.e. a variable of type `ISnowflakeGenerator` should be named `snowflakeGenerator`). -- Values of type `DatabaseContext` should always be named `db`. - -There are some exceptions to this. For example Sentry's `IHub` should be named `sentry` as the name `hub` isn't clear. - -## TypeScript code style - -Use `prettier` for formatting the frontend code. \ No newline at end of file diff --git a/build.sh b/build.sh deleted file mode 100755 index 949e0b1..0000000 --- a/build.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash -set -euxo pipefail - -ROOT_DIR=$(pwd) - -echo "Cleaning output directory ($ROOT_DIR/build)" - -[ -d "$ROOT_DIR/build" ] && rm -r "$ROOT_DIR/build" -mkdir "$ROOT_DIR/build" - -echo "Building .NET backend" - -cd "$ROOT_DIR/Foxnouns.Backend" -[ -d "$ROOT_DIR/Foxnouns.Backend/out" ] && rm -r "$ROOT_DIR/Foxnouns.Backend/out" -dotnet publish --artifacts-path "$ROOT_DIR/Foxnouns.Backend/out" -mv "$ROOT_DIR/Foxnouns.Backend/out/publish/Foxnouns.Backend/"* "$ROOT_DIR/build/bin" - -echo "Building Go rate limiter" - -cd "$ROOT_DIR/Foxnouns.RateLimiter" -go build -o rate -v . -mv rate "$ROOT_DIR/build/rate" - -echo "Building Node.js frontend" - -cd "$ROOT_DIR/Foxnouns.Frontend" -[ -d "$ROOT_DIR/Foxnouns.Frontend/build" ] && rm -r "$ROOT_DIR/Foxnouns.Frontend/build" -npm ci -npm run build - -mkdir "$ROOT_DIR/build/fe" -cp -r build .env* package.json package-lock.json "$ROOT_DIR/build/fe" -cd "$ROOT_DIR/build/fe" -NODE_ENV=production npm ci - -echo "Finished building Foxnouns.NET" diff --git a/docker-compose.local.yml b/docker-compose.local.yml deleted file mode 100644 index 084bcd9..0000000 --- a/docker-compose.local.yml +++ /dev/null @@ -1,77 +0,0 @@ -services: - backend: - image: backend - build: - context: . - dockerfile: ./Dockerfile.backend - environment: - - "Database:Url=Host=postgres;Database=postgres;Username=postgres;Password=postgres" - - "Database:EnablePooling=true" - - "Database:Redis=redis:6379" - - "Host=0.0.0.0" - - "Port=5000" - - "Logging:MetricsPort=5001" - restart: unless-stopped - ports: - - "5006:5000" - - "5007:5001" - volumes: - - ./docker/config.ini:/app/config.ini - - ./docker/static-pages:/app/static-pages - - frontend: - image: frontend - build: - context: ./ - dockerfile: ./Foxnouns.Frontend/Dockerfile - restart: unless-stopped - env_file: ./docker/frontend.env - environment: - - "PRIVATE_API_HOST=http://rate:5003/api" - - "PRIVATE_INTERNAL_API_HOST=http://backend:5000/api" - - rate: - image: rate - build: ./Foxnouns.RateLimiter - environment: - - "PORT=5003" - restart: unless-stopped - volumes: - - ./docker/proxy-config.json:/app/proxy-config.json - - postgres: - image: docker.io/postgres:16 - command: [ "postgres", - "-c", "max-connections=1000", - "-c", "timezone=Etc/UTC", - "-c", "max_wal_size=1GB", - "-c", "min_wal_size=80MB", - "-c", "shared_buffers=128MB" ] - environment: - - "POSTGRES_PASSWORD=postgres" - restart: unless-stopped - volumes: - - postgres_data:/var/lib/postgresql/data - - redis: - image: registry.redict.io/redict:7 - restart: unless-stopped - volumes: - - redict_data:/data - - caddy: - image: docker.io/caddy:2 - restart: unless-stopped - ports: - - "5004:80" - - "5005:81" - volumes: - - ./docker/Caddyfile:/etc/caddy/Caddyfile - - caddy_data:/data - - caddy_config:/config - -volumes: - caddy_data: - caddy_config: - postgres_data: - redict_data: diff --git a/docker-compose.prebuilt.yml b/docker-compose.prebuilt.yml deleted file mode 100644 index 091f1fb..0000000 --- a/docker-compose.prebuilt.yml +++ /dev/null @@ -1,53 +0,0 @@ -services: - backend: - image: code.vulpine.solutions/sam/foxnouns-be:latest - environment: - - "Database:Url=Host=postgres;Database=postgres;Username=postgres;Password=postgres" - - "Database:EnablePooling=true" - - "Database:Redis=redis:6379" - - "Host=0.0.0.0" - - "Port=5000" - - "Logging:MetricsPort=5001" - restart: unless-stopped - ports: - - "5001:5000" - - "5002:5001" - volumes: - - ./docker/config.ini:/app/config.ini - - ./docker/static-pages:/app/static-pages - - rate: - image: code.vulpine.solutions/sam/foxnouns-rate:latest - environment: - - "PORT=5003" - ports: - - "5003:5003" - restart: unless-stopped - volumes: - - ./docker/proxy-config.json:/app/proxy-config.json - - postgres: - image: docker.io/postgres:16 - command: [ "postgres", - "-c", "max-connections=1000", - "-c", "timezone=Etc/UTC", - "-c", "max_wal_size=1GB", - "-c", "min_wal_size=80MB", - "-c", "shared_buffers=128MB" ] - environment: - - "POSTGRES_PASSWORD=postgres" - restart: unless-stopped - volumes: - - postgres_data:/var/lib/postgresql/data - - redis: - image: registry.redict.io/redict:7 - restart: unless-stopped - volumes: - - redict_data:/data - -volumes: - caddy_data: - caddy_config: - postgres_data: - redict_data: diff --git a/docker/Caddyfile b/docker/Caddyfile deleted file mode 100644 index a729fa8..0000000 --- a/docker/Caddyfile +++ /dev/null @@ -1,11 +0,0 @@ -# Frontend and API -http://:80 { - reverse_proxy /api/* http://rate:5003 - reverse_proxy http://frontend:3000 -} - -# prns.cc (profile URL shortener) -http://:81 { - rewrite * /sid{uri} - reverse_proxy http://backend:5000 -} diff --git a/docker/config.example.ini b/docker/config.example.ini deleted file mode 100644 index ba32449..0000000 --- a/docker/config.example.ini +++ /dev/null @@ -1,48 +0,0 @@ -;; This configuration file is specifically for Docker installations. -;; Host, Port, and Database settings are overridden in the compose configuration. - -; The base *external* URL -BaseUrl = https://pronouns.localhost -; The base URL for media, without a trailing slash. This must be publicly accessible. -MediaBaseUrl = https://cdn-staging.pronouns.localhost - -[Logging] -; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal -LogEventLevel = Debug -; The URL to the Seq instance (optional) -SeqLogUrl = http://localhost:5341 -; The Sentry DSN to log to (optional) -SentryUrl = https://examplePublicKey@o0.ingest.sentry.io/0 -; Whether to trace performance with Sentry (optional) -SentryTracing = true -; Percentage of performance traces to send to Sentry (optional). Defaults to 0.0 (no traces at all) -SentryTracesSampleRate = 1.0 -; Whether to log SQL queries. Note that this is very verbose. Defaults to false. -LogQueries = false -; Whether metrics are enabled. If this is set to true, Foxnouns.NET will rely on Prometheus scraping metrics to update stats. -; If set to false, a background service will be used instead. Does not actually disable the /metrics endpoint. -; Defaults to false. -EnableMetrics = true -; The port the /metrics endpoint will listen on. Defaults to 5001. -MetricsPort = 5001 - -[Storage] -Endpoint = -AccessKey = -SecretKey = -Bucket = pronounscc - -[EmailAuth] -; The address that emails will be sent from. If not set, email auth is disabled. -From = noreply@accounts.pronouns.cc - -; The Coravel mail driver configuration. Keys should be self-explanatory. -[Coravel:Mail] -Host = localhost -Port = 1025 -Username = smtp-username -Password = smtp-password - -[DiscordAuth] -ClientId = -ClientSecret = diff --git a/docker/frontend.example.env b/docker/frontend.example.env deleted file mode 100644 index b68a330..0000000 --- a/docker/frontend.example.env +++ /dev/null @@ -1,4 +0,0 @@ -PUBLIC_LANGUAGE=en -PUBLIC_BASE_URL=https://pronouns.cc -PUBLIC_SHORT_URL=https://prns.cc -PUBLIC_API_BASE=https://pronouns.cc/api diff --git a/docker/proxy-config.example.json b/docker/proxy-config.example.json deleted file mode 100644 index dbf9482..0000000 --- a/docker/proxy-config.example.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "port": 5003, - "proxy_target": "http://backend:5000", - "debug": true, - "powered_by": "5 gay rats" -} diff --git a/docker/static-pages/.gitignore b/docker/static-pages/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/docker/static-pages/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/migration-tools/avatar-migrator/index.js b/migration-tools/avatar-migrator/index.js deleted file mode 100644 index 1415fa8..0000000 --- a/migration-tools/avatar-migrator/index.js +++ /dev/null @@ -1,82 +0,0 @@ -// TODO: i'm not even sure if this code works. it's not easy to test either. woops - -import postgres from "postgres"; -import { config } from "dotenv"; -import { Client } from "minio"; -import { Logger } from "tslog"; -import axios from "axios"; -config(); -const log = new Logger(); - -const env = (key) => { - const value = process.env[key]; - if (value) return value; - throw `No env variable with key $${key} found`; -}; - -const oldBaseUrl = env("OLD_BASE_URL"); -const bucket = env("MINIO_BUCKET"); - -const sql = postgres(env("DATABASE_URL")); -const minio = new Client({ - endPoint: env("MINIO_ENDPOINT"), - useSSL: true, - accessKey: env("MINIO_ACCESS_KEY"), - secretKey: env("MINIO_SECRET_KEY"), -}); - -const users = - await sql`select id::text, username, legacy_id, avatar from users where avatar is not null order by id asc`; -log.info("have to migrate %d users", users.length); - -const migrate = async (user) => { - log.debug( - "copying /users/%s/%s.webp to /users/%s/avatars/%s.webp", - user.legacy_id, - user.avatar, - user.id, - user.avatar - ); - - try { - const file = await axios.get( - `${oldBaseUrl}/users/${user.legacy_id}/${user.avatar}.webp`, - { responseType: "stream" } - ); - await minio.putObject( - bucket, - `users/${user.id}/avatars/${user.avatar}.webp`, - file.data, - file.headers["Content-Length"] - ); - - log.info("copied avatar for user %s", user.id); - - await sql`update users set avatar_migrated = true where id = ${user.id}::bigint`; - } catch (e) { - if ("status" in e && e.status === 404) { - log.warn( - "avatar for user %s/%s is not found. marking it as migrated.", - user.id, - user.username - ); - await sql`update users set avatar_migrated = true where id = ${user.id}::bigint`; - return; - } - - log.error( - "could not migrate avatar for user %s/%s:", - user.id, - user.username, - e - ); - } -}; - -for (let index = 0; index < users.length; index++) { - const user = users[index]; - await migrate(user); -} - -log.info("all users migrated!"); -process.exit(); diff --git a/migration-tools/avatar-migrator/package-lock.json b/migration-tools/avatar-migrator/package-lock.json deleted file mode 100644 index 6aa1e3d..0000000 --- a/migration-tools/avatar-migrator/package-lock.json +++ /dev/null @@ -1,866 +0,0 @@ -{ - "name": "avatar-migrator", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "axios": "^1.8.4", - "dotenv": "^16.5.0", - "minio": "^8.0.5", - "postgres": "^3.4.5", - "tslog": "^4.9.3" - } - }, - "node_modules/@zxing/text-encoding": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz", - "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==", - "license": "(Unlicense OR Apache-2.0)", - "optional": true - }, - "node_modules/async": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/block-stream2": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/block-stream2/-/block-stream2-2.1.0.tgz", - "integrity": "sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==", - "license": "MIT", - "dependencies": { - "readable-stream": "^3.4.0" - } - }, - "node_modules/browser-or-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/browser-or-node/-/browser-or-node-2.1.1.tgz", - "integrity": "sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==", - "license": "MIT" - }, - "node_modules/buffer-crc32": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", - "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", - "license": "MIT", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", - "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/dotenv": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", - "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "license": "MIT" - }, - "node_modules/fast-xml-parser": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.3.tgz", - "integrity": "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^1.1.1" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/filter-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", - "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minio": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/minio/-/minio-8.0.5.tgz", - "integrity": "sha512-/vAze1uyrK2R/DSkVutE4cjVoAowvIQ18RAwn7HrqnLecLlMazFnY0oNBqfuoAWvu7mZIGX75AzpuV05TJeoHg==", - "license": "Apache-2.0", - "dependencies": { - "async": "^3.2.4", - "block-stream2": "^2.1.0", - "browser-or-node": "^2.1.1", - "buffer-crc32": "^1.0.0", - "eventemitter3": "^5.0.1", - "fast-xml-parser": "^4.4.1", - "ipaddr.js": "^2.0.1", - "lodash": "^4.17.21", - "mime-types": "^2.1.35", - "query-string": "^7.1.3", - "stream-json": "^1.8.0", - "through2": "^4.0.2", - "web-encoding": "^1.1.5", - "xml2js": "^0.5.0 || ^0.6.2" - }, - "engines": { - "node": "^16 || ^18 || >=20" - } - }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postgres": { - "version": "3.4.5", - "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.5.tgz", - "integrity": "sha512-cDWgoah1Gez9rN3H4165peY9qfpEo+SA61oQv65O3cRUE1pOEoJWwddwcqKE8XZYjbblOJlYDlLV4h67HrEVDg==", - "license": "Unlicense", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/porsager" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/query-string": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", - "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", - "license": "MIT", - "dependencies": { - "decode-uri-component": "^0.2.2", - "filter-obj": "^1.1.0", - "split-on-first": "^1.0.0", - "strict-uri-encode": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/split-on-first": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", - "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/stream-chain": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/stream-chain/-/stream-chain-2.2.5.tgz", - "integrity": "sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==", - "license": "BSD-3-Clause" - }, - "node_modules/stream-json": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/stream-json/-/stream-json-1.9.1.tgz", - "integrity": "sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==", - "license": "BSD-3-Clause", - "dependencies": { - "stream-chain": "^2.2.5" - } - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/strnum": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz", - "integrity": "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, - "node_modules/through2": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz", - "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==", - "license": "MIT", - "dependencies": { - "readable-stream": "3" - } - }, - "node_modules/tslog": { - "version": "4.9.3", - "resolved": "https://registry.npmjs.org/tslog/-/tslog-4.9.3.tgz", - "integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/fullstack-build/tslog?sponsor=1" - } - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/web-encoding": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/web-encoding/-/web-encoding-1.1.5.tgz", - "integrity": "sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==", - "license": "MIT", - "dependencies": { - "util": "^0.12.3" - }, - "optionalDependencies": { - "@zxing/text-encoding": "0.9.0" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - } - } -} diff --git a/migration-tools/avatar-migrator/package.json b/migration-tools/avatar-migrator/package.json deleted file mode 100644 index e9d3463..0000000 --- a/migration-tools/avatar-migrator/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "type": "module", - "dependencies": { - "axios": "^1.8.4", - "dotenv": "^16.5.0", - "minio": "^8.0.5", - "postgres": "^3.4.5", - "tslog": "^4.9.3" - } -} diff --git a/migration-tools/avatar-proxy/config.example.json b/migration-tools/avatar-proxy/config.example.json deleted file mode 100644 index d04cb71..0000000 --- a/migration-tools/avatar-proxy/config.example.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "old_avatar_base": "https://pronounscc-legacy.your-s3-host.com/", - "new_avatar_base": "http://pronounscc.your-s3-host.com/", - "flag_base": "http://pronounscc.your-s3-host.com/", - "port": 6100, - "database": "postgresql://postgres:password@localhost/postgres" -} diff --git a/migration-tools/avatar-proxy/go.mod b/migration-tools/avatar-proxy/go.mod deleted file mode 100644 index 3009181..0000000 --- a/migration-tools/avatar-proxy/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module code.vulpine.solutions/sam/Foxnouns.NET/migration-tools/avatar-proxy - -go 1.24.2 - -require ( - github.com/go-chi/chi/v5 v5.2.1 - github.com/jackc/pgx/v5 v5.7.4 -) - -require ( - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/puddle/v2 v2.2.2 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/text v0.21.0 // indirect -) diff --git a/migration-tools/avatar-proxy/go.sum b/migration-tools/avatar-proxy/go.sum deleted file mode 100644 index b7ffae7..0000000 --- a/migration-tools/avatar-proxy/go.sum +++ /dev/null @@ -1,30 +0,0 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= -github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg= -github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= -github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= -github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/migration-tools/avatar-proxy/main.go b/migration-tools/avatar-proxy/main.go deleted file mode 100644 index f43f35a..0000000 --- a/migration-tools/avatar-proxy/main.go +++ /dev/null @@ -1,103 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "log" - "net/http" - "os" - "strconv" - - "github.com/go-chi/chi/v5" - "github.com/jackc/pgx/v5" - "github.com/jackc/pgx/v5/pgxpool" -) - -type Proxy struct { - OldAvatarBase string `json:"old_avatar_base"` - NewAvatarBase string `json:"new_avatar_base"` - FlagBase string `json:"flag_base"` - - Port int `json:"port"` - Database string `json:"database"` - - db *pgxpool.Pool -} - -type EntityWithAvatar struct { - ID uint64 - LegacyID string - Avatar string - AvatarMigrated bool -} - -func main() { - b, err := os.ReadFile("config.json") - if err != nil { - log.Fatalln("error reading config:", err) - } - - p := &Proxy{} - err = json.Unmarshal(b, p) - if err != nil { - log.Fatalln("error parsing config:", err) - } - - p.db, err = pgxpool.New(context.Background(), p.Database) - if err != nil { - log.Fatalln("error connecting to database:", err) - } - - r := chi.NewRouter() - - r.HandleFunc(`/flags/{hash:[\da-f]+}.webp`, func(w http.ResponseWriter, r *http.Request) { - http.Redirect(w, r, fmt.Sprintf("%s%s", p.FlagBase, r.URL.Path), http.StatusTemporaryRedirect) - }) - r.Get(`/members/{id:[\d]+}/avatars/{hash:[\da-f]+}.webp`, p.proxyHandler("member")) - r.Get(`/users/{id:[\d]+}/avatars/{hash:[\da-f]+}.webp`, p.proxyHandler("user")) - r.NotFound(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("file not found")) - }) - - log.Printf("serving on port %v", p.Port) - - err = http.ListenAndServe(":"+strconv.Itoa(p.Port), r) - if err != nil { - log.Fatalf("listening on port %v: %v", p.Port, err) - } -} - -func (p *Proxy) proxyHandler(avatarType string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - id := chi.URLParam(r, "id") - hash := chi.URLParam(r, "hash") - - var e EntityWithAvatar - // don't do this normally, kids. avatarType can only be "user" or "member" so it's fine here but this is a BAD idea otherwise. - err := p.db.QueryRow( - r.Context(), "SELECT id, legacy_id, avatar, avatar_migrated FROM "+avatarType+"s WHERE id = $1 AND avatar = $2", - id, hash, - ).Scan(&e.ID, &e.LegacyID, &e.Avatar, &e.AvatarMigrated) - if err != nil { - if errors.Is(err, pgx.ErrNoRows) { - w.WriteHeader(http.StatusNotFound) - w.Write([]byte("avatar not found")) - return - } - - log.Printf("error getting avatar for %s %s: %v", avatarType, id, err) - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte("internal server error")) - return - } - - if e.AvatarMigrated { - http.Redirect(w, r, fmt.Sprintf("%s/%ss/%d/avatars/%s.webp", p.NewAvatarBase, avatarType, e.ID, e.Avatar), http.StatusTemporaryRedirect) - } else { - http.Redirect(w, r, fmt.Sprintf("%s/%ss/%s/%s.webp", p.OldAvatarBase, avatarType, e.LegacyID, e.Avatar), http.StatusTemporaryRedirect) - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 12db760..0000000 --- a/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "devDependencies": { - "concurrently": "^9.0.1" - }, - "scripts": { - "watch:be": "dotnet watch --no-hot-reload --project Foxnouns.Backend -- --migrate-and-start", - "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'npm run watch:be' 'cd Foxnouns.Frontend && npm run dev' 'cd rate && go run -v .'", - "format": "dotnet csharpier . && cd Foxnouns.Frontend && npm run format" - } -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index c744fbb..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,211 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - concurrently: - specifier: ^9.0.1 - version: 9.1.0 - -packages: - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - cliui@8.0.1: - resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} - engines: {node: '>=12'} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - concurrently@9.1.0: - resolution: {integrity: sha512-VxkzwMAn4LP7WyMnJNbHN5mKV9L2IbyDjpzemKr99sXNR3GqRNMMHdm7prV1ws9wg7ETj6WUkNOigZVsptwbgg==} - engines: {node: '>=18'} - hasBin: true - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - rxjs@7.8.1: - resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} - - shell-quote@1.8.1: - resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - tree-kill@1.2.2: - resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} - hasBin: true - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@21.1.1: - resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} - engines: {node: '>=12'} - - yargs@17.7.2: - resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} - engines: {node: '>=12'} - -snapshots: - - ansi-regex@5.0.1: {} - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - cliui@8.0.1: - dependencies: - string-width: 4.2.3 - strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - concurrently@9.1.0: - dependencies: - chalk: 4.1.2 - lodash: 4.17.21 - rxjs: 7.8.1 - shell-quote: 1.8.1 - supports-color: 8.1.1 - tree-kill: 1.2.2 - yargs: 17.7.2 - - emoji-regex@8.0.0: {} - - escalade@3.2.0: {} - - get-caller-file@2.0.5: {} - - has-flag@4.0.0: {} - - is-fullwidth-code-point@3.0.0: {} - - lodash@4.17.21: {} - - require-directory@2.1.1: {} - - rxjs@7.8.1: - dependencies: - tslib: 2.8.1 - - shell-quote@1.8.1: {} - - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-color@8.1.1: - dependencies: - has-flag: 4.0.0 - - tree-kill@1.2.2: {} - - tslib@2.8.1: {} - - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - y18n@5.0.8: {} - - yargs-parser@21.1.1: {} - - yargs@17.7.2: - dependencies: - cliui: 8.0.1 - escalade: 3.2.0 - get-caller-file: 2.0.5 - require-directory: 2.1.1 - string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 21.1.1