From 24155a149ca12f8e07edd2b77ca84723e355310e Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 4 Jun 2024 17:43:48 +0200 Subject: [PATCH 001/261] fix: fix BuildInfo not being initialized --- Foxnouns.Backend/Foxnouns.Backend.csproj | 4 ++++ Foxnouns.Backend/Program.cs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 8438390..f92814a 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -29,4 +29,8 @@ + + + + diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 4eca8cb..5025f70 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -6,6 +6,9 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +// Read version information from .version in the repository root +await BuildInfo.ReadBuildInfo(); + var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration(); From 401e268281f4aacff2c9c94df3c8f291cc3981f0 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 4 Jun 2024 17:47:16 +0200 Subject: [PATCH 002/261] chore: add back Properties/launchSettings.json --- .../Properties/launchSettings.json | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Foxnouns.Backend/Properties/launchSettings.json diff --git a/Foxnouns.Backend/Properties/launchSettings.json b/Foxnouns.Backend/Properties/launchSettings.json new file mode 100644 index 0000000..b680651 --- /dev/null +++ b/Foxnouns.Backend/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "Development": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "externalUrlConfiguration": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Production": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "externalUrlConfiguration": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Production" + } + } + } +} \ No newline at end of file From 14f8e77e6a0c7150d6cccc398a80114ab4738f9c Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 8 Jun 2024 21:02:12 +0200 Subject: [PATCH 003/261] add sveltekit template --- .../.idea/codeStyles/codeStyleConfig.xml | 5 + .../inspectionProfiles/Project_Default.xml | 6 + .../.idea/jsLinters/eslint.xml | 6 + .idea/.idea.Foxnouns.NET/.idea/prettier.xml | 9 + .../Controllers/DebugController.cs | 2 +- .../Controllers/MetaController.cs | 21 + Foxnouns.Frontend/.env.example | 4 + Foxnouns.Frontend/.gitignore | 10 + Foxnouns.Frontend/.npmrc | 1 + Foxnouns.Frontend/.prettierignore | 4 + Foxnouns.Frontend/.prettierrc | 6 + Foxnouns.Frontend/README.md | 38 + Foxnouns.Frontend/eslint.config.js | 33 + Foxnouns.Frontend/package.json | 36 + Foxnouns.Frontend/src/app.d.ts | 16 + Foxnouns.Frontend/src/app.html | 12 + Foxnouns.Frontend/src/hooks.server.ts | 15 + Foxnouns.Frontend/src/lib/index.ts | 1 + Foxnouns.Frontend/src/routes/+page.svelte | 2 + Foxnouns.Frontend/static/favicon.png | Bin 0 -> 1571 bytes Foxnouns.Frontend/svelte.config.js | 21 + Foxnouns.Frontend/tsconfig.json | 19 + Foxnouns.Frontend/vite.config.ts | 6 + Foxnouns.Frontend/yarn.lock | 1885 +++++++++++++++++ 24 files changed, 2157 insertions(+), 1 deletion(-) create mode 100644 .idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml create mode 100644 .idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml create mode 100644 .idea/.idea.Foxnouns.NET/.idea/prettier.xml create mode 100644 Foxnouns.Backend/Controllers/MetaController.cs create mode 100644 Foxnouns.Frontend/.env.example create mode 100644 Foxnouns.Frontend/.gitignore create mode 100644 Foxnouns.Frontend/.npmrc create mode 100644 Foxnouns.Frontend/.prettierignore create mode 100644 Foxnouns.Frontend/.prettierrc create mode 100644 Foxnouns.Frontend/README.md create mode 100644 Foxnouns.Frontend/eslint.config.js create mode 100644 Foxnouns.Frontend/package.json create mode 100644 Foxnouns.Frontend/src/app.d.ts create mode 100644 Foxnouns.Frontend/src/app.html create mode 100644 Foxnouns.Frontend/src/hooks.server.ts create mode 100644 Foxnouns.Frontend/src/lib/index.ts create mode 100644 Foxnouns.Frontend/src/routes/+page.svelte create mode 100644 Foxnouns.Frontend/static/favicon.png create mode 100644 Foxnouns.Frontend/svelte.config.js create mode 100644 Foxnouns.Frontend/tsconfig.json create mode 100644 Foxnouns.Frontend/vite.config.ts create mode 100644 Foxnouns.Frontend/yarn.lock diff --git a/.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml b/.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..79ee123 --- /dev/null +++ b/.idea/.idea.Foxnouns.NET/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03d9549 --- /dev/null +++ b/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,6 @@ + + + + \ 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 new file mode 100644 index 0000000..204acf7 --- /dev/null +++ b/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/prettier.xml b/.idea/.idea.Foxnouns.NET/.idea/prettier.xml new file mode 100644 index 0000000..653a9e0 --- /dev/null +++ b/.idea/.idea.Foxnouns.NET/.idea/prettier.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs index 328bf3d..4746d95 100644 --- a/Foxnouns.Backend/Controllers/DebugController.cs +++ b/Foxnouns.Backend/Controllers/DebugController.cs @@ -28,5 +28,5 @@ public class DebugController(DatabaseContext db, AuthService authSvc, IClock clo public record CreateUserRequest(string Username, string Password, string Email); - public record AuthResponse(Snowflake Id, string Username, string Token); + private record AuthResponse(Snowflake Id, string Username, string Token); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs new file mode 100644 index 0000000..d43749e --- /dev/null +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -0,0 +1,21 @@ +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Mvc; + +namespace Foxnouns.Backend.Controllers; + +[Route("/api/v2/meta")] +public class MetaController(DatabaseContext db) : ApiControllerBase +{ + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MetaResponse))] + public async Task GetMeta() + { + var userCount = await db.Users.CountAsync(); + var memberCount = await db.Members.CountAsync(); + + return Ok(new MetaResponse(userCount, memberCount, BuildInfo.Version, BuildInfo.Hash)); + } + + private record MetaResponse(int Users, int Members, string Version, string Hash); +} \ No newline at end of file diff --git a/Foxnouns.Frontend/.env.example b/Foxnouns.Frontend/.env.example new file mode 100644 index 0000000..91687be --- /dev/null +++ b/Foxnouns.Frontend/.env.example @@ -0,0 +1,4 @@ +# The API base that the server itself should call, this should not be behind a reverse proxy. +PRIVATE_API_BASE=http://localhost:5000/api +# The API base that clients should call, behind a reverse proxy. +PUBLIC_API_BASE=https://pronouns.cc/api diff --git a/Foxnouns.Frontend/.gitignore b/Foxnouns.Frontend/.gitignore new file mode 100644 index 0000000..6635cf5 --- /dev/null +++ b/Foxnouns.Frontend/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/Foxnouns.Frontend/.npmrc b/Foxnouns.Frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/Foxnouns.Frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/Foxnouns.Frontend/.prettierignore b/Foxnouns.Frontend/.prettierignore new file mode 100644 index 0000000..cc41cea --- /dev/null +++ b/Foxnouns.Frontend/.prettierignore @@ -0,0 +1,4 @@ +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/Foxnouns.Frontend/.prettierrc b/Foxnouns.Frontend/.prettierrc new file mode 100644 index 0000000..9b685c1 --- /dev/null +++ b/Foxnouns.Frontend/.prettierrc @@ -0,0 +1,6 @@ +{ + "useTabs": true, + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/Foxnouns.Frontend/README.md b/Foxnouns.Frontend/README.md new file mode 100644 index 0000000..5ce6766 --- /dev/null +++ b/Foxnouns.Frontend/README.md @@ -0,0 +1,38 @@ +# create-svelte + +Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). + +## 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 +npm create svelte@latest + +# create a new project in my-app +npm create svelte@latest 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://kit.svelte.dev/docs/adapters) for your target environment. diff --git a/Foxnouns.Frontend/eslint.config.js b/Foxnouns.Frontend/eslint.config.js new file mode 100644 index 0000000..a351fa9 --- /dev/null +++ b/Foxnouns.Frontend/eslint.config.js @@ -0,0 +1,33 @@ +import js from '@eslint/js'; +import ts from 'typescript-eslint'; +import svelte from 'eslint-plugin-svelte'; +import prettier from 'eslint-config-prettier'; +import globals from 'globals'; + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + 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/'] + } +]; diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json new file mode 100644 index 0000000..fb3ce1f --- /dev/null +++ b/Foxnouns.Frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "foxnouns.frontend", + "version": "0.0.1", + "private": true, + "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", + "lint": "prettier --check . && eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@sveltejs/adapter-node": "^5.0.1", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltestrap/sveltestrap": "^6.2.7", + "@types/eslint": "^8.56.7", + "bootstrap": "^5.3.3", + "eslint": "^9.0.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0", + "globals": "^15.0.0", + "prettier": "^3.1.1", + "prettier-plugin-svelte": "^3.1.2", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0-alpha.20", + "vite": "^5.0.3" + }, + "type": "module", + "dependencies": {} +} diff --git a/Foxnouns.Frontend/src/app.d.ts b/Foxnouns.Frontend/src/app.d.ts new file mode 100644 index 0000000..f7864c3 --- /dev/null +++ b/Foxnouns.Frontend/src/app.d.ts @@ -0,0 +1,16 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + interface Locals { + token?: string; + } + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html new file mode 100644 index 0000000..77a5ff5 --- /dev/null +++ b/Foxnouns.Frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/Foxnouns.Frontend/src/hooks.server.ts b/Foxnouns.Frontend/src/hooks.server.ts new file mode 100644 index 0000000..59efc4b --- /dev/null +++ b/Foxnouns.Frontend/src/hooks.server.ts @@ -0,0 +1,15 @@ +import { PRIVATE_API_BASE } from "$env/static/private"; +import { PUBLIC_API_BASE } from "$env/static/public"; + +export async function handle({ event, resolve }) { + event.locals.token = event.cookies.get("pronounscc-token"); + return await resolve(event); +} + +export function handleFetch({ event, request, fetch }) { + if (request.url.startsWith(PUBLIC_API_BASE)) + request = new Request(request.url.replace(PUBLIC_API_BASE, PRIVATE_API_BASE), request); + if (event.locals.token) request.headers.set("Authorization", event.locals.token); + + return fetch(request); +} diff --git a/Foxnouns.Frontend/src/lib/index.ts b/Foxnouns.Frontend/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte new file mode 100644 index 0000000..5982b0a --- /dev/null +++ b/Foxnouns.Frontend/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit kit.svelte.dev to read the documentation

diff --git a/Foxnouns.Frontend/static/favicon.png b/Foxnouns.Frontend/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH=0.36.0 <1.0.0" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-scope@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc" + integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" + integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== + +eslint@^9.0.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.4.0.tgz#79150c3610ae606eb131f1d648d5f43b3d45f3cd" + integrity sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/config-array" "^0.15.1" + "@eslint/eslintrc" "^3.1.0" + "@eslint/js" "9.4.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.3.0" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.0.1" + eslint-visitor-keys "^4.0.0" + espree "^10.0.1" + esquery "^1.4.2" + 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" + is-path-inside "^3.0.3" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +esm-env@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/esm-env/-/esm-env-1.0.0.tgz#b124b40b180711690a4cb9b00d16573391950413" + integrity sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA== + +espree@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" + integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== + dependencies: + acorn "^8.11.3" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.0.0" + +espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +estree-walker@^3.0.0, estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +esutils@^2.0.2, esutils@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + 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.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^8.0.3: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^15.0.0: + version "15.4.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.4.0.tgz#3e36ea6e4d9ddcf1cb42d92f5c4a145a8a2ddc1c" + integrity sha512-unnwvMZpv0eDUyjNyh9DH/yxUaRYrEjW/qK4QcdrHg3oO11igUQrCSgODHEqxlKg8v2CD2Sd7UkqqEBoz5U7TQ== + +globalyzer@0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" + integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +graceful-fs@^4.1.3: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +ignore@^5.2.0, ignore@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-meta-resolve@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#f9db8bead9fafa61adb811db77a2bf22c5399706" + integrity sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw== + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-reference@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" + integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== + dependencies: + "@types/estree" "*" + +is-reference@^3.0.0, is-reference@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" + integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== + dependencies: + "@types/estree" "*" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kleur@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" + integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== + +known-css-properties@^0.31.0: + version "0.31.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.31.0.tgz#5c8d9d8777b3ca09482b2397f6a241e5d69a1023" + integrity sha512-sBPIUGTNF0czz0mwGGUoKKJC8Q7On1GPbCSFPfyEsfHb2DyBG0Y4QtV+EVWpINSaiGKZblDNuF5AezxSgOhesQ== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +locate-character@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974" + integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +magic-string@^0.30.10, magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +mdn-data@2.0.30: + version "2.0.30" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" + integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^0.5.1: + version "0.5.6" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +mrmime@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" + integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + 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" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +periscopic@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" + integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^3.0.0" + is-reference "^3.0.0" + +picocolors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss-load-config@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" + integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== + dependencies: + lilconfig "^2.0.5" + yaml "^1.10.2" + +postcss-safe-parser@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" + integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== + +postcss-scss@^4.0.9: + version "4.0.9" + resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.9.tgz#a03c773cd4c9623cb04ce142a52afcec74806685" + integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A== + +postcss-selector-parser@^6.0.16: + version "6.1.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" + integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-plugin-svelte@^3.1.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.4.tgz#50366d550b2fe64b736ec0c90998805cfc395a2c" + integrity sha512-tZv+ADfeOWFNQkXkRh6zUXE16w3Vla8x2Ug0B/EnSmjR4EnwdwZbGgL/liSwR1kcEALU5mAAyua98HBxheCxgg== + +prettier@^3.1.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.1.tgz#e68935518dd90bb7ec4821ba970e68f8de16e1ac" + integrity sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.22.1: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^2.5.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rollup@^4.13.0, rollup@^4.9.5: + version "4.18.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" + integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.18.0" + "@rollup/rollup-android-arm64" "4.18.0" + "@rollup/rollup-darwin-arm64" "4.18.0" + "@rollup/rollup-darwin-x64" "4.18.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.18.0" + "@rollup/rollup-linux-arm-musleabihf" "4.18.0" + "@rollup/rollup-linux-arm64-gnu" "4.18.0" + "@rollup/rollup-linux-arm64-musl" "4.18.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0" + "@rollup/rollup-linux-riscv64-gnu" "4.18.0" + "@rollup/rollup-linux-s390x-gnu" "4.18.0" + "@rollup/rollup-linux-x64-gnu" "4.18.0" + "@rollup/rollup-linux-x64-musl" "4.18.0" + "@rollup/rollup-win32-arm64-msvc" "4.18.0" + "@rollup/rollup-win32-ia32-msvc" "4.18.0" + "@rollup/rollup-win32-x64-msvc" "4.18.0" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +sade@^1.7.4, sade@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + +sander@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad" + integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA== + dependencies: + es6-promise "^3.1.2" + graceful-fs "^4.1.3" + mkdirp "^0.5.1" + rimraf "^2.5.2" + +semver@^7.5.4, semver@^7.6.0: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +set-cookie-parser@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" + integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +sirv@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" + integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== + dependencies: + "@polka/url" "^1.0.0-next.24" + mrmime "^2.0.0" + totalist "^3.0.0" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +sorcery@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.0.tgz#310c80ee993433854bb55bb9aa4003acd147fca8" + integrity sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.14" + buffer-crc32 "^0.2.5" + minimist "^1.2.0" + sander "^0.5.0" + +source-map-js@^1.0.1, source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svelte-check@^3.6.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.8.0.tgz#e0850b876d3d32760465bfb26d06b32c4c9f98a1" + integrity sha512-7Nxn+3X97oIvMzYJ7t27w00qUf1Y52irE2RU2dQAd5PyvfGp4E7NLhFKVhb6PV2fx7dCRMpNKDIuazmGthjpSQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + chokidar "^3.4.1" + fast-glob "^3.2.7" + import-fresh "^3.2.1" + picocolors "^1.0.0" + sade "^1.7.4" + svelte-preprocess "^5.1.3" + typescript "^5.0.3" + +"svelte-eslint-parser@>=0.36.0 <1.0.0": + version "0.36.0" + resolved "https://registry.yarnpkg.com/svelte-eslint-parser/-/svelte-eslint-parser-0.36.0.tgz#5390d86181180f2707c374b33c7d2fe42c1e1be2" + integrity sha512-/6YmUSr0FAVxW8dXNdIMydBnddPMHzaHirAZ7RrT21XYdgGGZMh0LQG6CZsvAFS4r2Y4ItUuCQc8TQ3urB30mQ== + dependencies: + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + postcss "^8.4.38" + postcss-scss "^4.0.9" + +svelte-hmr@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.16.0.tgz#9f345b7d1c1662f1613747ed7e82507e376c1716" + integrity sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA== + +svelte-preprocess@^5.1.3: + version "5.1.4" + resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz#14ada075c94bbd2b71c5ec70ff72f8ebe1c95b91" + integrity sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA== + dependencies: + "@types/pug" "^2.0.6" + detect-indent "^6.1.0" + magic-string "^0.30.5" + sorcery "^0.11.0" + strip-indent "^3.0.0" + +svelte@^4.2.7: + version "4.2.18" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.18.tgz#33dbce74e83eb6dcc54dbea25f9758b1d8e8bb78" + integrity sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA== + dependencies: + "@ampproject/remapping" "^2.2.1" + "@jridgewell/sourcemap-codec" "^1.4.15" + "@jridgewell/trace-mapping" "^0.3.18" + "@types/estree" "^1.0.1" + acorn "^8.9.0" + aria-query "^5.3.0" + axobject-query "^4.0.0" + code-red "^1.0.3" + css-tree "^2.3.1" + estree-walker "^3.0.3" + is-reference "^3.0.1" + locate-character "^3.0.0" + magic-string "^0.30.4" + periscopic "^3.1.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tiny-glob@^0.2.9: + version "0.2.9" + resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" + integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== + dependencies: + globalyzer "0.1.0" + globrex "^0.1.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +totalist@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" + integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== + +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + +tslib@^2.4.1: + version "2.6.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" + integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +typescript-eslint@^8.0.0-alpha.20: + version "8.0.0-alpha.29" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.0.0-alpha.29.tgz#939c996f681e3ae6bb36c1159b1af16e4a84f2fd" + integrity sha512-NASQjd4tP+wukSs/Cj8vHjK/Ogk0nhVOr/kwzwg0AaXOWiz0g+rtE+lvqAaV+nhsCfMskuzKzc1TywFrhJlbvw== + dependencies: + "@typescript-eslint/eslint-plugin" "8.0.0-alpha.29" + "@typescript-eslint/parser" "8.0.0-alpha.29" + "@typescript-eslint/utils" "8.0.0-alpha.29" + +typescript@^5.0.0, typescript@^5.0.3: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +vite@^5.0.3: + version "5.2.13" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.13.tgz#945ababcbe3d837ae2479c29f661cd20bc5e1a80" + integrity sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +vitefu@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969" + integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== From a2f001392b315b53d210909f45caa1a559722ecc Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 9 Jun 2024 15:48:26 +0200 Subject: [PATCH 004/261] stuff --- .editorconfig | 3 + .../Controllers/MetaController.cs | 9 ++- Foxnouns.Frontend/src/app.html | 7 ++ Foxnouns.Frontend/src/lib/api/meta.ts | 11 +++ Foxnouns.Frontend/src/lib/api/user.ts | 9 +++ Foxnouns.Frontend/src/lib/nav/Logo.svelte | 34 +++++++++ Foxnouns.Frontend/src/lib/nav/Navbar.svelte | 57 +++++++++++++++ Foxnouns.Frontend/src/lib/request.ts | 72 +++++++++++++++++++ .../src/routes/+layout.server.ts | 13 ++++ Foxnouns.Frontend/src/routes/+layout.svelte | 11 +++ Foxnouns.Frontend/src/routes/+page.svelte | 17 +++++ 11 files changed, 241 insertions(+), 2 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/api/meta.ts create mode 100644 Foxnouns.Frontend/src/lib/api/user.ts create mode 100644 Foxnouns.Frontend/src/lib/nav/Logo.svelte create mode 100644 Foxnouns.Frontend/src/lib/nav/Navbar.svelte create mode 100644 Foxnouns.Frontend/src/lib/request.ts create mode 100644 Foxnouns.Frontend/src/routes/+layout.server.ts create mode 100644 Foxnouns.Frontend/src/routes/+layout.svelte diff --git a/.editorconfig b/.editorconfig index 2a1f655..0229143 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,5 @@ [*.cs] +# We use PostgresSQL which doesn't recommend more specific string types 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 diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index d43749e..451960e 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -14,8 +14,13 @@ public class MetaController(DatabaseContext db) : ApiControllerBase var userCount = await db.Users.CountAsync(); var memberCount = await db.Members.CountAsync(); - return Ok(new MetaResponse(userCount, memberCount, BuildInfo.Version, BuildInfo.Hash)); + return Ok(new MetaResponse( + BuildInfo.Version, BuildInfo.Hash, memberCount, + new UserInfo(userCount, 0, 0, 0)) + ); } - private record MetaResponse(int Users, int Members, string Version, string Hash); + private record MetaResponse(string Version, string Hash, int Members, UserInfo Users); + + private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); } \ No newline at end of file diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html index 77a5ff5..562d998 100644 --- a/Foxnouns.Frontend/src/app.html +++ b/Foxnouns.Frontend/src/app.html @@ -4,6 +4,13 @@ + %sveltekit.head% diff --git a/Foxnouns.Frontend/src/lib/api/meta.ts b/Foxnouns.Frontend/src/lib/api/meta.ts new file mode 100644 index 0000000..89f1aa3 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/api/meta.ts @@ -0,0 +1,11 @@ +export default interface Meta { + version: string; + hash: string; + users: { + total: number; + active_month: number; + active_week: number; + active_day: number; + }; + members: number; +} diff --git a/Foxnouns.Frontend/src/lib/api/user.ts b/Foxnouns.Frontend/src/lib/api/user.ts new file mode 100644 index 0000000..3832872 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/api/user.ts @@ -0,0 +1,9 @@ +export type User = { + id: string; + username: string; + display_name: string | null; + bio: string | null; + member_title: string | null; + avatar_url: string | null; + links: string[]; +}; diff --git a/Foxnouns.Frontend/src/lib/nav/Logo.svelte b/Foxnouns.Frontend/src/lib/nav/Logo.svelte new file mode 100644 index 0000000..9da9d99 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/nav/Logo.svelte @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/Foxnouns.Frontend/src/lib/nav/Navbar.svelte b/Foxnouns.Frontend/src/lib/nav/Navbar.svelte new file mode 100644 index 0000000..588bb44 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/nav/Navbar.svelte @@ -0,0 +1,57 @@ + + + + + + + + + + + diff --git a/Foxnouns.Frontend/src/lib/request.ts b/Foxnouns.Frontend/src/lib/request.ts new file mode 100644 index 0000000..9536ca9 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/request.ts @@ -0,0 +1,72 @@ +import { PUBLIC_API_BASE } from "$env/static/public"; + +export type RequestParams = { + token?: string; + body?: any; + headers?: Record; +}; + +/** + * Fetch a path from the API and parse the response. + * To make sure the request is authenticated in load functions, + * pass `fetch` from the request object into opts. + * + * @param fetchFn A function like `fetch`, such as from the `load` function + * @param method The HTTP method, i.e. GET, POST, PATCH + * @param path The path to request, minus the leading `/api/v2` + * @param params Extra options for this request + * @returns T + * @throws APIError + */ +export default async function request( + fetchFn: typeof fetch, + method: string, + path: string, + params: RequestParams = {}, +) { + const url = `${PUBLIC_API_BASE}/v2${path}`; + const resp = await fetchFn(url, { + method, + body: params.body ? JSON.stringify(params.body) : undefined, + headers: { + ...params.headers, + ...(params.token ? { Authorization: params.token } : {}), + "Content-Type": "application/json", + }, + }); + + if (resp.status < 200 || resp.status >= 400) throw await resp.json(); + return (await resp.json()) as T; +} + +/** + * Fetch a path from the API and discard the response. + * To make sure the request is authenticated in load functions, + * pass `fetch` from the request object into opts. + * + * @param fetchFn A function like `fetch`, such as from the `load` function + * @param method The HTTP method, i.e. GET, POST, PATCH + * @param path The path to request, minus the leading `/api/v2` + * @param params Extra options for this request + * @returns T + * @throws APIError + */ +export async function fastRequest( + fetchFn: typeof fetch, + method: string, + path: string, + params: RequestParams = {}, +): Promise { + const url = `${PUBLIC_API_BASE}/v2${path}`; + const resp = await fetchFn(url, { + method, + body: params.body ? JSON.stringify(params.body) : undefined, + headers: { + ...params.headers, + ...(params.token ? { Authorization: params.token } : {}), + "Content-Type": "application/json", + }, + }); + + if (resp.status < 200 || resp.status >= 400) throw await resp.json(); +} diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts new file mode 100644 index 0000000..2d1e4ba --- /dev/null +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -0,0 +1,13 @@ +import type Meta from "$lib/api/meta"; +import type { User } from "$lib/api/user"; +import request from "$lib/request"; + +export async function load({ fetch }) { + const meta = await request(fetch, "GET", "/meta"); + let user: User | undefined; + try { + user = await request(fetch, "GET", "/users/@me"); + } catch {} + + return { meta, user }; +} diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..64a204f --- /dev/null +++ b/Foxnouns.Frontend/src/routes/+layout.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte index 5982b0a..0f5264b 100644 --- a/Foxnouns.Frontend/src/routes/+page.svelte +++ b/Foxnouns.Frontend/src/routes/+page.svelte @@ -1,2 +1,19 @@ + +

Welcome to SvelteKit

Visit kit.svelte.dev to read the documentation

+ +

+ are you logged in? {data.user !== undefined} + {#if data.user} +
hello, {data.user.username}! +
your ID: {data.user.id} + {/if} +

+ +

+ stats: {data.meta.users.total} users, {data.meta.members} members +

From 50257d61f8c6596c7640df4430e7bf7feb1f71f5 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 9 Jun 2024 23:21:28 +0200 Subject: [PATCH 005/261] switch frontend css from bootstrap to bulma --- .../Controllers/UsersController.cs | 1 - Foxnouns.Backend/Database/Models/Cache.cs | 11 ++ Foxnouns.Frontend/package.json | 6 +- Foxnouns.Frontend/src/app.html | 4 +- Foxnouns.Frontend/src/app.scss | 7 ++ Foxnouns.Frontend/src/lib/nav/Dropdown.svelte | 10 ++ .../src/lib/nav/DropdownItem.svelte | 10 ++ Foxnouns.Frontend/src/lib/nav/Navbar.svelte | 103 ++++++++++-------- Foxnouns.Frontend/src/lib/store.ts | 9 ++ Foxnouns.Frontend/src/routes/+layout.svelte | 5 +- Foxnouns.Frontend/src/routes/+page.svelte | 2 +- .../src/routes/menu/+page.server.ts | 0 .../src/routes/menu/+page.svelte | 0 Foxnouns.Frontend/yarn.lock | 53 ++++++--- 14 files changed, 150 insertions(+), 71 deletions(-) create mode 100644 Foxnouns.Backend/Database/Models/Cache.cs create mode 100644 Foxnouns.Frontend/src/app.scss create mode 100644 Foxnouns.Frontend/src/lib/nav/Dropdown.svelte create mode 100644 Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte create mode 100644 Foxnouns.Frontend/src/lib/store.ts create mode 100644 Foxnouns.Frontend/src/routes/menu/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/menu/+page.svelte diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 26ae497..c6101bb 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using Foxnouns.Backend.Database; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; diff --git a/Foxnouns.Backend/Database/Models/Cache.cs b/Foxnouns.Backend/Database/Models/Cache.cs new file mode 100644 index 0000000..81d4b2b --- /dev/null +++ b/Foxnouns.Backend/Database/Models/Cache.cs @@ -0,0 +1,11 @@ +using NodaTime; + +namespace Foxnouns.Backend.Database.Models; + +public class Cache +{ + public long Id { get; init; } + public required string Key { get; init; } + public required string Value { get; set; } + public Instant Expires { get; init; } +} \ No newline at end of file diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index fb3ce1f..5b9442e 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -12,18 +12,20 @@ "format": "prettier --write ." }, "devDependencies": { + "@fontsource/firago": "^5.0.11", "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@sveltestrap/sveltestrap": "^6.2.7", + "@tabler/icons-svelte": "^3.5.0", "@types/eslint": "^8.56.7", - "bootstrap": "^5.3.3", + "bulma": "^1.0.1", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.36.0", "globals": "^15.0.0", "prettier": "^3.1.1", "prettier-plugin-svelte": "^3.1.2", + "sass": "^1.77.4", "svelte": "^4.2.7", "svelte-check": "^3.6.0", "tslib": "^2.4.1", diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html index 562d998..1bf633d 100644 --- a/Foxnouns.Frontend/src/app.html +++ b/Foxnouns.Frontend/src/app.html @@ -6,10 +6,10 @@ %sveltekit.head% diff --git a/Foxnouns.Frontend/src/app.scss b/Foxnouns.Frontend/src/app.scss new file mode 100644 index 0000000..e8dcaf9 --- /dev/null +++ b/Foxnouns.Frontend/src/app.scss @@ -0,0 +1,7 @@ +@use "bulma/sass" with ( + $family-primary: "FiraGO" +); + +@import "@fontsource/firago/400.css"; +@import "@fontsource/firago/400-italic.css"; +@import "@fontsource/firago/700.css"; diff --git a/Foxnouns.Frontend/src/lib/nav/Dropdown.svelte b/Foxnouns.Frontend/src/lib/nav/Dropdown.svelte new file mode 100644 index 0000000..00c70c5 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/nav/Dropdown.svelte @@ -0,0 +1,10 @@ + + +
+ + +
diff --git a/Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte b/Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte new file mode 100644 index 0000000..be22533 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte @@ -0,0 +1,10 @@ + + +{#if divider} + +{:else} + +{/if} diff --git a/Foxnouns.Frontend/src/lib/nav/Navbar.svelte b/Foxnouns.Frontend/src/lib/nav/Navbar.svelte index 588bb44..7effb19 100644 --- a/Foxnouns.Frontend/src/lib/nav/Navbar.svelte +++ b/Foxnouns.Frontend/src/lib/nav/Navbar.svelte @@ -1,57 +1,70 @@ - - - - - - - diff --git a/Foxnouns.Frontend/src/lib/store.ts b/Foxnouns.Frontend/src/lib/store.ts new file mode 100644 index 0000000..d4449ad --- /dev/null +++ b/Foxnouns.Frontend/src/lib/store.ts @@ -0,0 +1,9 @@ +import { writable } from "svelte/store"; +import { browser } from "$app/environment"; + +const defaultThemeValue = "light"; +const initialThemeValue = browser + ? window.localStorage.getItem("pronouns-theme") ?? defaultThemeValue + : defaultThemeValue; + +export const themeStore = writable(initialThemeValue); diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte index 64a204f..608c9f3 100644 --- a/Foxnouns.Frontend/src/routes/+layout.svelte +++ b/Foxnouns.Frontend/src/routes/+layout.svelte @@ -1,11 +1,10 @@ - diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte index 0f5264b..88f5279 100644 --- a/Foxnouns.Frontend/src/routes/+page.svelte +++ b/Foxnouns.Frontend/src/routes/+page.svelte @@ -15,5 +15,5 @@

- stats: {data.meta.users.total} users, {data.meta.members} members + stats: {data.meta.users.total} users, {data.meta.members} members

diff --git a/Foxnouns.Frontend/src/routes/menu/+page.server.ts b/Foxnouns.Frontend/src/routes/menu/+page.server.ts new file mode 100644 index 0000000..e69de29 diff --git a/Foxnouns.Frontend/src/routes/menu/+page.svelte b/Foxnouns.Frontend/src/routes/menu/+page.svelte new file mode 100644 index 0000000..e69de29 diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index 7eccad0..93fffb6 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -171,6 +171,11 @@ resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.3.tgz#e65ae80ee2927b4fd8c5c26b15ecacc2b2a6cc2a" integrity sha512-HAbhAYKfsAC2EkTqve00ibWIZlaU74Z1EHwAjYr4PXF0YU2VEA1zSIKSSpKszRLRWwHzzRZXvK632u+uXzvsvw== +"@fontsource/firago@^5.0.11": + version "5.0.11" + resolved "https://registry.yarnpkg.com/@fontsource/firago/-/firago-5.0.11.tgz#8fe3c8b47cc1d8148bc50c80189ed3aac8555cb7" + integrity sha512-XfFsLxSFMTbJTN+94yFTJyuFGmoxtykt+6rL0fj9unCeXslllirpH6KetIlbZO73NzTUmKYRvtOJdOgVbBGtaQ== + "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" @@ -239,11 +244,6 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== -"@popperjs/core@^2.11.8": - version "2.11.8" - resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" - integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== - "@rollup/plugin-commonjs@^25.0.7": version "25.0.8" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz#c77e608ab112a666b7f2a6bea625c73224f7dd34" @@ -412,12 +412,17 @@ svelte-hmr "^0.16.0" vitefu "^0.2.5" -"@sveltestrap/sveltestrap@^6.2.7": - version "6.2.7" - resolved "https://registry.yarnpkg.com/@sveltestrap/sveltestrap/-/sveltestrap-6.2.7.tgz#5b2736cbee2db973f02b09d2e9d5bf819418f1f9" - integrity sha512-WwLLfAFUb42BGuRrf3Vbct30bQMzlEMMipN/MfxhjuLTmLQeW9muVJfPyvjtWS+mY+RjkSCoHvAp/ZobP1NLlQ== +"@tabler/icons-svelte@^3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@tabler/icons-svelte/-/icons-svelte-3.5.0.tgz#02efede4ce0ed680e0835878c6c02cd63daf9d9a" + integrity sha512-mc5ardGEM7cnUA4/q6Mz5bmW9B6t28vAAOf4Wl6+KXiTwG00EjImfnIr3pS3Ihi9sFIiXvJPYRl4H5IHlgvJvQ== dependencies: - "@popperjs/core" "^2.11.8" + "@tabler/icons" "3.5.0" + +"@tabler/icons@3.5.0": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.5.0.tgz#29d0dbf100c8cb392dd64f1fe8efdcfcd1f57e44" + integrity sha512-I53dC3ZSHQ2MZFGvDYJelfXm91L2bTTixS4w5jTAulLhHbCZso5Bih4Rk/NYZxlngLQMKHvEYwZQ+6w/WluKiA== "@types/cookie@^0.6.0": version "0.6.0" @@ -607,11 +612,6 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -bootstrap@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.3.tgz#de35e1a765c897ac940021900fcbb831602bac38" - integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -644,6 +644,11 @@ builtin-modules@^3.3.0: resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== +bulma@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/bulma/-/bulma-1.0.1.tgz#e37261d6f8e1a3494c9378803d9958effb2715ce" + integrity sha512-+xv/BIAEQakHkR0QVz+s+RjNqfC53Mx9ZYexyaFNFo9wx5i76HXArNdwW7bccyJxa5mgV/T5DcVGqsAB19nBJQ== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -657,7 +662,7 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.4.1: +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -1144,6 +1149,11 @@ ignore@^5.2.0, ignore@^5.3.1: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== +immutable@^4.0.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" + integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== + import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1630,6 +1640,15 @@ sander@^0.5.0: mkdirp "^0.5.1" rimraf "^2.5.2" +sass@^1.77.4: + version "1.77.4" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.4.tgz#92059c7bfc56b827c56eb116778d157ec017a5cd" + integrity sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + semver@^7.5.4, semver@^7.6.0: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" @@ -1676,7 +1695,7 @@ sorcery@^0.11.0: minimist "^1.2.0" sander "^0.5.0" -source-map-js@^1.0.1, source-map-js@^1.2.0: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== From 493a6e4d298cd767ffa48789891d97cb4f80323c Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 10 Jun 2024 16:25:49 +0200 Subject: [PATCH 006/261] feat(backend): add skeleton discord auth controller --- .../Authentication/DiscordAuthController.cs | 14 +++++++++ Foxnouns.Backend/Database/DatabaseContext.cs | 2 ++ .../Database/DatabaseQueryExtensions.cs | 31 +++++++++++++++++++ .../Models/{Cache.cs => TemporaryKey.cs} | 2 +- 4 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs rename Foxnouns.Backend/Database/Models/{Cache.cs => TemporaryKey.cs} (89%) diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs new file mode 100644 index 0000000..4934ff5 --- /dev/null +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -0,0 +1,14 @@ +using Foxnouns.Backend.Database; +using Microsoft.AspNetCore.Mvc; + +namespace Foxnouns.Backend.Controllers.Authentication; + +[Route("/api/v2/auth/discord")] +public class DiscordAuthController(Config config, DatabaseContext db) : ApiControllerBase +{ + [HttpPost("url")] + public async Task AuthenticationUrl() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index f9ec686..03e2c42 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -17,6 +17,7 @@ public class DatabaseContext : DbContext public DbSet FediverseApplications { get; set; } public DbSet Tokens { get; set; } public DbSet Applications { get; set; } + public DbSet TemporaryKeys { get; set; } public DatabaseContext(Config config) { @@ -47,6 +48,7 @@ public class DatabaseContext : DbContext { modelBuilder.Entity().HasIndex(u => u.Username).IsUnique(); modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); + modelBuilder.Entity().HasIndex(k => k.Key).IsUnique(); modelBuilder.Entity() .OwnsOne(u => u.Fields, f => f.ToJson()) diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 40e333c..0b77ddb 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -1,7 +1,9 @@ using System.Security.Cryptography; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Foxnouns.Backend.Database; @@ -84,4 +86,33 @@ public static class DatabaseQueryExtensions await context.SaveChangesAsync(); return app; } + + public static Task SetKeyAsync(this DatabaseContext context, string key, string value, Duration expireAfter) => + context.SetKeyAsync(key, value, SystemClock.Instance.GetCurrentInstant() + expireAfter); + + public static async Task SetKeyAsync(this DatabaseContext context, string key, string value, Instant expires) + { + context.TemporaryKeys.Add(new TemporaryKey + { + Expires = expires, + Key = key, + Value = value, + }); + await context.SaveChangesAsync(); + } + + public static async Task GetKeyAsync(this DatabaseContext context, string key, + bool delete = false) + { + var value = await context.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key); + if (value == null) return null; + + if (delete) + { + await context.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(); + await context.SaveChangesAsync(); + } + + return value.Value; + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/Cache.cs b/Foxnouns.Backend/Database/Models/TemporaryKey.cs similarity index 89% rename from Foxnouns.Backend/Database/Models/Cache.cs rename to Foxnouns.Backend/Database/Models/TemporaryKey.cs index 81d4b2b..d3dbfc8 100644 --- a/Foxnouns.Backend/Database/Models/Cache.cs +++ b/Foxnouns.Backend/Database/Models/TemporaryKey.cs @@ -2,7 +2,7 @@ using NodaTime; namespace Foxnouns.Backend.Database.Models; -public class Cache +public class TemporaryKey { public long Id { get; init; } public required string Key { get; init; } From 25540f2de2184a4b13e054624f4c1e552f849c95 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 12 Jun 2024 03:47:20 +0200 Subject: [PATCH 007/261] feat(backend): start authentication controllers --- .gitignore | 1 + Foxnouns.Backend/Config.cs | 25 +- .../Authentication/AuthController.cs | 38 ++ .../Authentication/DiscordAuthController.cs | 8 +- .../Authentication/EmailAuthController.cs | 31 ++ .../Controllers/DebugController.cs | 2 +- ...611225328_AddTemporaryKeyCache.Designer.cs | 511 ++++++++++++++++++ .../20240611225328_AddTemporaryKeyCache.cs | 44 ++ .../DatabaseContextModelSnapshot.cs | 33 ++ .../Extensions/WebApplicationExtensions.cs | 3 +- Foxnouns.Backend/Program.cs | 23 +- Foxnouns.Backend/Services/AuthService.cs | 18 + Foxnouns.Backend/Services/KeyCacheService.cs | 51 ++ .../Services/UserRendererService.cs | 2 +- .../{config.ini => config.example.ini} | 4 + 15 files changed, 777 insertions(+), 17 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/Authentication/AuthController.cs create mode 100644 Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs create mode 100644 Foxnouns.Backend/Services/KeyCacheService.cs rename Foxnouns.Backend/{config.ini => config.example.ini} (88%) diff --git a/.gitignore b/.gitignore index cd1b080..56d5d08 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ bin/ obj/ .version +config.ini diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 39a417f..bb2add8 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -4,21 +4,28 @@ namespace Foxnouns.Backend; public class Config { - public string Host { get; set; } = "localhost"; - public int Port { get; set; } = 3000; - public string BaseUrl { get; set; } = null!; + public string Host { get; init; } = "localhost"; + public int Port { get; init; } = 3000; + public string BaseUrl { get; init; } = null!; public string Address => $"http://{Host}:{Port}"; - public string? SeqLogUrl { get; set; } - public LogEventLevel LogEventLevel { get; set; } = LogEventLevel.Debug; + public string? SeqLogUrl { get; init; } + public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug; - public DatabaseConfig Database { get; set; } = new(); + public DatabaseConfig Database { get; init; } = new(); + public DiscordAuthConfig DiscordAuth { get; init; } = new(); public class DatabaseConfig { - public string Url { get; set; } = string.Empty; - public int? Timeout { get; set; } - public int? MaxPoolSize { get; set; } + public string Url { get; init; } = string.Empty; + public int? Timeout { get; init; } + public int? MaxPoolSize { get; init; } + } + + public class DiscordAuthConfig + { + public string? ClientId { get; init; } + public string? ClientSecret { get; init; } } } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs new file mode 100644 index 0000000..96b10c3 --- /dev/null +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -0,0 +1,38 @@ +using System.Web; +using Foxnouns.Backend.Services; +using Microsoft.AspNetCore.Mvc; +using NodaTime; + +namespace Foxnouns.Backend.Controllers.Authentication; + +[Route("/api/v2/auth")] +public class AuthController(Config config, KeyCacheService keyCacheSvc) : ApiControllerBase +{ + [HttpPost("urls")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UrlsResponse))] + public async Task UrlsAsync() + { + var state = HttpUtility.UrlEncode(await keyCacheSvc.GenerateAuthStateAsync()); + string? discord = null; + if (config.DiscordAuth.ClientId != null && config.DiscordAuth.ClientSecret != 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/login/discord")}"; + + return Ok(new UrlsResponse(discord, null, null)); + } + + private record UrlsResponse( + string? Discord, + string? Google, + string? Tumblr + ); + + internal record AuthResponse( + UserRendererService.UserResponse User, + string Token, + Instant ExpiresAt + ); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 4934ff5..2fb8c54 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -6,9 +6,11 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/discord")] public class DiscordAuthController(Config config, DatabaseContext db) : ApiControllerBase { - [HttpPost("url")] - public async Task AuthenticationUrl() + private void CheckRequirements() { - throw new NotImplementedException(); + if (config.DiscordAuth.ClientId == null || config.DiscordAuth.ClientSecret == null) + { + throw new ApiError.BadRequest("Discord authentication is not enabled on this instance."); + } } } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs new file mode 100644 index 0000000..3ba92ba --- /dev/null +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -0,0 +1,31 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Services; +using Microsoft.AspNetCore.Mvc; +using NodaTime; + +namespace Foxnouns.Backend.Controllers.Authentication; + +[Route("/api/v2/auth/email")] +public class EmailAuthController(DatabaseContext db, AuthService authSvc, UserRendererService userRendererSvc, IClock clock, ILogger logger) : ApiControllerBase +{ + [HttpPost("login")] + public async Task LoginAsync([FromBody] LoginRequest req) + { + var user = await authSvc.AuthenticateUserAsync(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 AuthController.AuthResponse( + await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false), + tokenStr, + token.ExpiresAt + )); + } + + public record LoginRequest(string Email, string Password); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs index 4746d95..3ef189b 100644 --- a/Foxnouns.Backend/Controllers/DebugController.cs +++ b/Foxnouns.Backend/Controllers/DebugController.cs @@ -10,7 +10,7 @@ public class DebugController(DatabaseContext db, AuthService authSvc, IClock clo { [HttpPost("users")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))] - public async Task CreateUser([FromBody] CreateUserRequest req) + public async Task CreateUserAsync([FromBody] CreateUserRequest req) { logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email); diff --git a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs new file mode 100644 index 0000000..af4f52a --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs @@ -0,0 +1,511 @@ +// +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("20240611225328_AddTemporaryKeyCache")] + partial class AddTemporaryKeyCache + { + /// + 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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (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/20240611225328_AddTemporaryKeyCache.cs b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs new file mode 100644 index 0000000..ccf736b --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + 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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index d0cb607..13c10dd 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -175,6 +175,39 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("members", (string)null); }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (string)null); + }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => { b.Property("Id") diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 84381c4..158eb10 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -66,7 +66,8 @@ public static class WebApplicationExtensions .AddSnowflakeGenerator() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped(); public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services .AddScoped() diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 5025f70..b3c623b 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -2,9 +2,11 @@ using Foxnouns.Backend; using Foxnouns.Backend.Database; using Serilog; using Foxnouns.Backend.Extensions; +using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using NodaTime; // Read version information from .version in the repository root await BuildInfo.ReadBuildInfo(); @@ -60,6 +62,23 @@ app.MapControllers(); app.Urls.Clear(); app.Urls.Add(config.Address); -app.Run(); +// Fire off the periodic tasks loop in the background +_ = new Timer(_ => +{ + var __ = RunPeriodicTasksAsync(); +}, null, TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(1)); -Log.CloseAndFlush(); \ No newline at end of file +app.Run(); +Log.CloseAndFlush(); + +return; + +async Task RunPeriodicTasksAsync() +{ + await using var scope = app.Services.CreateAsyncScope(); + var logger = scope.ServiceProvider.GetRequiredService(); + logger.Debug("Running periodic tasks"); + + var keyCacheSvc = scope.ServiceProvider.GetRequiredService(); + await keyCacheSvc.DeleteExpiredKeysAsync(); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index 87494ed..0949a6f 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -3,6 +3,7 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; using NodaTime; namespace Foxnouns.Backend.Services; @@ -30,6 +31,23 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator return user; } + public async Task AuthenticateUserAsync(string email, string password) + { + var user = await db.Users.FirstOrDefaultAsync(u => u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email)); + if (user == null) throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); + + var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password)); + if (pwResult == PasswordVerificationResult.Failed) + throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); + if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) + { + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password)); + await db.SaveChangesAsync(); + } + + return user; + } + public (string, Token) GenerateToken(User user, Application application, string[] scopes, Instant expires) { if (!OauthUtils.ValidateScopes(application, scopes)) diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs new file mode 100644 index 0000000..253cc9f --- /dev/null +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -0,0 +1,51 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Utils; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Foxnouns.Backend.Services; + +public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) +{ + public Task SetKeyAsync(string key, string value, Duration expireAfter) => + db.SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter); + + public async Task SetKeyAsync(string key, string value, Instant expires) + { + db.TemporaryKeys.Add(new TemporaryKey + { + Expires = expires, + Key = key, + Value = value, + }); + await db.SaveChangesAsync(); + } + + public async Task GetKeyAsync(string key, bool delete = false) + { + var value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key); + if (value == null) return null; + + if (delete) + { + await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(); + await db.SaveChangesAsync(); + } + + return value.Value; + } + + public async Task DeleteExpiredKeysAsync() + { + var count = await db.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()).ExecuteDeleteAsync(); + if (count != 0) logger.Information("Removed {Count} expired keys from the database", count); + } + + public async Task GenerateAuthStateAsync() + { + var state = OauthUtils.RandomToken(); + await SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); + return state; + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 0f22021..0ac7f90 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -7,7 +7,7 @@ namespace Foxnouns.Backend.Services; public class UserRendererService(DatabaseContext db, MemberRendererService memberRendererService) { - public async Task RenderUserAsync(User user, User? selfUser = null, bool renderMembers = true) + public async Task RenderUserAsync(User user, User? selfUser = null, bool renderMembers = true) { renderMembers = renderMembers && (!user.ListHidden || selfUser?.Id == user.Id); diff --git a/Foxnouns.Backend/config.ini b/Foxnouns.Backend/config.example.ini similarity index 88% rename from Foxnouns.Backend/config.ini rename to Foxnouns.Backend/config.example.ini index e0e13c4..d361d33 100644 --- a/Foxnouns.Backend/config.ini +++ b/Foxnouns.Backend/config.example.ini @@ -18,3 +18,7 @@ Url = "Host=localhost;Database=foxnouns_net;Username=pronouns;Password=pronouns" Timeout = 5 ; The maximum number of open connections. Defaults to 50. MaxPoolSize = 50 + +[DiscordAuth] +ClientId = +ClientSecret = From 2a7bd746aaf0926f49306e2f0eaad30ecbf9ad00 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 12 Jun 2024 03:54:25 +0200 Subject: [PATCH 008/261] feat(frontend): start auth pages --- Foxnouns.Frontend/package.json | 2 + Foxnouns.Frontend/src/app.html | 2 +- Foxnouns.Frontend/src/app.scss | 1 + Foxnouns.Frontend/src/error.html | 73 ++++++++++++++++++ Foxnouns.Frontend/src/lib/nav/Dropdown.svelte | 3 +- Foxnouns.Frontend/src/lib/nav/Navbar.svelte | 10 +-- Foxnouns.Frontend/src/routes/+page.svelte | 19 +++-- .../src/routes/auth/login/+page.server.ts | 12 +++ .../src/routes/auth/login/+page.svelte | 53 +++++++++++++ .../src/routes/menu/+page.server.ts | 0 .../src/routes/menu/+page.svelte | 0 Foxnouns.Frontend/static/default/512.webp | Bin 0 -> 43736 bytes Foxnouns.Frontend/static/favicon.png | Bin 1571 -> 0 bytes Foxnouns.Frontend/static/favicon.svg | 2 + Foxnouns.Frontend/static/logo.svg | 34 ++++++++ Foxnouns.Frontend/static/robots.txt | 5 ++ Foxnouns.Frontend/yarn.lock | 17 ++++ 17 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 Foxnouns.Frontend/src/error.html create mode 100644 Foxnouns.Frontend/src/routes/auth/login/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/login/+page.svelte delete mode 100644 Foxnouns.Frontend/src/routes/menu/+page.server.ts delete mode 100644 Foxnouns.Frontend/src/routes/menu/+page.svelte create mode 100644 Foxnouns.Frontend/static/default/512.webp delete mode 100644 Foxnouns.Frontend/static/favicon.png create mode 100644 Foxnouns.Frontend/static/favicon.svg create mode 100644 Foxnouns.Frontend/static/logo.svg create mode 100644 Foxnouns.Frontend/static/robots.txt diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 5b9442e..34152a0 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -16,8 +16,10 @@ "@sveltejs/adapter-node": "^5.0.1", "@sveltejs/kit": "^2.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltestrap/sveltestrap": "^6.2.7", "@tabler/icons-svelte": "^3.5.0", "@types/eslint": "^8.56.7", + "bootstrap-icons": "^1.11.3", "bulma": "^1.0.1", "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html index 1bf633d..53007b2 100644 --- a/Foxnouns.Frontend/src/app.html +++ b/Foxnouns.Frontend/src/app.html @@ -2,7 +2,7 @@ - + -
+
+ +
+ {#if hasUrls} +
+

Log in with third-party provider

+ +
+ {/if} +
+ + diff --git a/Foxnouns.Frontend/src/routes/menu/+page.server.ts b/Foxnouns.Frontend/src/routes/menu/+page.server.ts deleted file mode 100644 index e69de29..0000000 diff --git a/Foxnouns.Frontend/src/routes/menu/+page.svelte b/Foxnouns.Frontend/src/routes/menu/+page.svelte deleted file mode 100644 index e69de29..0000000 diff --git a/Foxnouns.Frontend/static/default/512.webp b/Foxnouns.Frontend/static/default/512.webp new file mode 100644 index 0000000000000000000000000000000000000000..3701f7d02a08806325fea477614df265fb65839d GIT binary patch literal 43736 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 Y@gbNeZwVDKs+N>u!N1=V2x71R0K5_gS^xk5 literal 0 HcmV?d00001 diff --git a/Foxnouns.Frontend/static/favicon.png b/Foxnouns.Frontend/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH +image/svg+xml \ No newline at end of file diff --git a/Foxnouns.Frontend/static/logo.svg b/Foxnouns.Frontend/static/logo.svg new file mode 100644 index 0000000..9371e6c --- /dev/null +++ b/Foxnouns.Frontend/static/logo.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/Foxnouns.Frontend/static/robots.txt b/Foxnouns.Frontend/static/robots.txt new file mode 100644 index 0000000..496b815 --- /dev/null +++ b/Foxnouns.Frontend/static/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Disallow: /@* +Disallow: /auth +Disallow: /settings +Disallow: /edit diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index 93fffb6..dcc8b33 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -244,6 +244,11 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== +"@popperjs/core@^2.11.8": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@rollup/plugin-commonjs@^25.0.7": version "25.0.8" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz#c77e608ab112a666b7f2a6bea625c73224f7dd34" @@ -412,6 +417,13 @@ svelte-hmr "^0.16.0" vitefu "^0.2.5" +"@sveltestrap/sveltestrap@^6.2.7": + version "6.2.7" + resolved "https://registry.yarnpkg.com/@sveltestrap/sveltestrap/-/sveltestrap-6.2.7.tgz#5b2736cbee2db973f02b09d2e9d5bf819418f1f9" + integrity sha512-WwLLfAFUb42BGuRrf3Vbct30bQMzlEMMipN/MfxhjuLTmLQeW9muVJfPyvjtWS+mY+RjkSCoHvAp/ZobP1NLlQ== + dependencies: + "@popperjs/core" "^2.11.8" + "@tabler/icons-svelte@^3.5.0": version "3.5.0" resolved "https://registry.yarnpkg.com/@tabler/icons-svelte/-/icons-svelte-3.5.0.tgz#02efede4ce0ed680e0835878c6c02cd63daf9d9a" @@ -612,6 +624,11 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +bootstrap-icons@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz#03f9cb754ec005c52f9ee616e2e84a82cab3084b" + integrity sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww== + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" From 6186eda092aa6d00241477c6d5390c0be473221c Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 12 Jun 2024 16:19:49 +0200 Subject: [PATCH 009/261] feat(backend): add RequestDiscordTokenAsync method --- Foxnouns.Backend/Config.cs | 20 +++++++ .../Authentication/AuthController.cs | 12 +++- .../Authentication/DiscordAuthController.cs | 53 ++++++++++++++++-- .../Authentication/EmailAuthController.cs | 19 ++++++- .../Controllers/DebugController.cs | 18 ++++-- .../Controllers/MetaController.cs | 3 + .../Extensions/WebApplicationExtensions.cs | 3 +- Foxnouns.Backend/Services/AuthService.cs | 55 +++++++++++++++++-- Foxnouns.Backend/Services/KeyCacheService.cs | 6 ++ .../Services/RemoteAuthService.cs | 48 ++++++++++++++++ .../routes/auth/login/discord/+page.server.ts | 10 ++++ .../routes/auth/login/discord/+page.svelte | 5 ++ 12 files changed, 230 insertions(+), 22 deletions(-) create mode 100644 Foxnouns.Backend/Services/RemoteAuthService.cs create mode 100644 Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index bb2add8..e4d81c7 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -15,6 +15,8 @@ public class Config public DatabaseConfig Database { get; init; } = new(); public DiscordAuthConfig DiscordAuth { get; init; } = new(); + public GoogleAuthConfig GoogleAuth { get; init; } = new(); + public TumblrAuthConfig TumblrAuth { get; init; } = new(); public class DatabaseConfig { @@ -25,6 +27,24 @@ public class Config 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; } } diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 96b10c3..e224be3 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -6,12 +6,16 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth")] -public class AuthController(Config config, KeyCacheService keyCacheSvc) : ApiControllerBase -{ +public class AuthController(Config config, KeyCacheService keyCacheSvc, ILogger logger) : ApiControllerBase +{ [HttpPost("urls")] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(UrlsResponse))] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task UrlsAsync() { + logger.Debug("Generating auth URLs for Discord: {Discord}, Google: {Google}, Tumblr: {Tumblr}", + config.DiscordAuth.Enabled, + config.GoogleAuth.Enabled, + config.TumblrAuth.Enabled); var state = HttpUtility.UrlEncode(await keyCacheSvc.GenerateAuthStateAsync()); string? discord = null; if (config.DiscordAuth.ClientId != null && config.DiscordAuth.ClientSecret != null) @@ -35,4 +39,6 @@ public class AuthController(Config config, KeyCacheService keyCacheSvc) : ApiCon string Token, Instant ExpiresAt ); + + public record CallbackRequest(string Code, string State); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 2fb8c54..b3f93ae 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -1,16 +1,61 @@ using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; +using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/discord")] -public class DiscordAuthController(Config config, DatabaseContext db) : ApiControllerBase +public class DiscordAuthController( + Config config, + ILogger logger, + IClock clock, + DatabaseContext db, + KeyCacheService keyCacheSvc, + AuthService authSvc, + RemoteAuthService remoteAuthSvc, + UserRendererService userRendererSvc) : ApiControllerBase { + [HttpPost("callback")] + public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req) + { + CheckRequirements(); + await keyCacheSvc.ValidateAuthStateAsync(req.State); + + var remoteUser = await remoteAuthSvc.RequestDiscordTokenAsync(req.Code, req.State); + var user = await authSvc.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); + if (user != null) return Ok(await GenerateUserTokenAsync(user)); + + logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, + remoteUser.Id); + + throw new NotImplementedException(); + } + + private async Task GenerateUserTokenAsync(User user) + { + var frontendApp = await db.GetFrontendApplicationAsync(); + logger.Debug("Logging user {Id} in with Discord", user.Id); + + var (tokenStr, token) = + authSvc.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(); + + return new AuthController.AuthResponse( + await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false), + tokenStr, + token.ExpiresAt + ); + } + private void CheckRequirements() { - if (config.DiscordAuth.ClientId == null || config.DiscordAuth.ClientSecret == null) - { + if (!config.DiscordAuth.Enabled) throw new ApiError.BadRequest("Discord authentication is not enabled on this instance."); - } } } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 3ba92ba..e1146c5 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -6,18 +6,31 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/email")] -public class EmailAuthController(DatabaseContext db, AuthService authSvc, UserRendererService userRendererSvc, IClock clock, ILogger logger) : ApiControllerBase +public class EmailAuthController( + DatabaseContext db, + AuthService authSvc, + UserRendererService userRendererSvc, + IClock clock, + ILogger logger) : ApiControllerBase { [HttpPost("login")] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task LoginAsync([FromBody] LoginRequest req) { - var user = await authSvc.AuthenticateUserAsync(req.Email, req.Password); + var (user, authenticationResult) = await authSvc.AuthenticateUserAsync(req.Email, req.Password); + if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired) + throw new NotImplementedException("MFA is not implemented yet"); + var frontendApp = await db.GetFrontendApplicationAsync(); - + + logger.Debug("Logging user {Id} in with email and password", user.Id); + var (tokenStr, token) = authSvc.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(); return Ok(new AuthController.AuthResponse( diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs index 3ef189b..94a0ff2 100644 --- a/Foxnouns.Backend/Controllers/DebugController.cs +++ b/Foxnouns.Backend/Controllers/DebugController.cs @@ -1,3 +1,4 @@ +using Foxnouns.Backend.Controllers.Authentication; using Foxnouns.Backend.Database; using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; @@ -6,10 +7,15 @@ using NodaTime; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/debug")] -public class DebugController(DatabaseContext db, AuthService authSvc, IClock clock, ILogger logger) : ApiControllerBase +public class DebugController( + DatabaseContext db, + AuthService authSvc, + UserRendererService userRendererSvc, + IClock clock, + ILogger logger) : ApiControllerBase { [HttpPost("users")] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(AuthResponse))] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task CreateUserAsync([FromBody] CreateUserRequest req) { logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email); @@ -23,10 +29,12 @@ public class DebugController(DatabaseContext db, AuthService authSvc, IClock clo await db.SaveChangesAsync(); - return Ok(new AuthResponse(user.Id, user.Username, tokenStr)); + return Ok(new AuthController.AuthResponse( + await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false), + tokenStr, + token.ExpiresAt + )); } public record CreateUserRequest(string Username, string Password, string Email); - - private record AuthResponse(Snowflake Id, string Username, string Token); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 451960e..f39810e 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -20,6 +20,9 @@ public class MetaController(DatabaseContext db) : ApiControllerBase ); } + [HttpGet("coffee")] + public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); + private record MetaResponse(string Version, string Hash, int Members, UserInfo Users); private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 158eb10..b03a9eb 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -67,7 +67,8 @@ public static class WebApplicationExtensions .AddScoped() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped(); public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services .AddScoped() diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index 0949a6f..69c8dc0 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -22,7 +22,11 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator { Id = snowflakeGenerator.GenerateSnowflake(), Username = username, - AuthMethods = { new AuthMethod { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } } + AuthMethods = + { + new AuthMethod + { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } + } }; db.Add(user); @@ -31,11 +35,21 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator return user; } - public async Task AuthenticateUserAsync(string email, string password) + /// + /// Authenticates a user with email and password. + /// + /// The user's email address + /// The user's password, in plain text + /// 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) { - var user = await db.Users.FirstOrDefaultAsync(u => u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email)); - if (user == null) throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); - + var user = await db.Users.FirstOrDefaultAsync(u => + u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email)); + if (user == null) + throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); + var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password)); if (pwResult == PasswordVerificationResult.Failed) throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); @@ -45,7 +59,36 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator await db.SaveChangesAsync(); } - return user; + return (user, EmailAuthenticationResult.AuthSuccessful); + } + + public enum EmailAuthenticationResult + { + AuthSuccessful, + MfaRequired, + } + + /// + /// 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. + /// 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) + { + 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."); + + return await db.Users.FirstOrDefaultAsync(u => + u.AuthMethods.Any(a => + a.AuthType == authType && a.RemoteId == remoteId && a.FediverseApplication == instance)); } public (string, Token) GenerateToken(User user, Application application, string[] scopes, Instant expires) diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 253cc9f..fabc316 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -48,4 +48,10 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) await SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); return state; } + + public async Task ValidateAuthStateAsync(string state) + { + var val = await GetKeyAsync($"oauth_state:{state}", delete: true); + if (val == null) throw new ApiError.BadRequest("Invalid OAuth state"); + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/RemoteAuthService.cs b/Foxnouns.Backend/Services/RemoteAuthService.cs new file mode 100644 index 0000000..48d6b84 --- /dev/null +++ b/Foxnouns.Backend/Services/RemoteAuthService.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using System.Web; + +namespace Foxnouns.Backend.Services; + +public class RemoteAuthService(Config config) +{ + private readonly HttpClient _httpClient = new(); + + 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, string state) + { + var redirectUri = $"{config.BaseUrl}/auth/login/discord"; + var resp = await _httpClient.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 } + } + )); + resp.EnsureSuccessStatusCode(); + var token = await resp.Content.ReadFromJsonAsync(); + if (token == null) throw new FoxnounsError("Discord token response was null"); + + var req = new HttpRequestMessage(HttpMethod.Get, _discordUserUri); + req.Headers.Add("Authorization", $"{token.token_type} {token.access_token}"); + + var resp2 = await _httpClient.SendAsync(req); + resp2.EnsureSuccessStatusCode(); + var user = await resp2.Content.ReadFromJsonAsync(); + if (user == null) throw new FoxnounsError("Discord user response was null"); + + return new RemoteUser(user.id, user.username); + } + + [SuppressMessage("ReSharper", "InconsistentNaming")] + private record DiscordTokenResponse(string access_token, string token_type); + + [SuppressMessage("ReSharper", "InconsistentNaming")] + private record DiscordUserResponse(string id, string username); + + public record RemoteUser(string Id, string Username); +} \ No newline at end of file diff --git a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts new file mode 100644 index 0000000..2ebde3c --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts @@ -0,0 +1,10 @@ +import { fastRequest } from "$lib/request"; + +export const load = async ({ fetch, url }) => { + await fastRequest(fetch, "POST", "/auth/discord/callback", { + body: { + code: url.searchParams.get("code"), + state: url.searchParams.get("state"), + }, + }); +}; diff --git a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte new file mode 100644 index 0000000..3cb3fbc --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte @@ -0,0 +1,5 @@ + + +

omg its a login page

From a7950671e1cbf66750d7abb3b6b2faef46840538 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 13 Jun 2024 02:23:55 +0200 Subject: [PATCH 010/261] feat: initial working discord authentication --- .../Authentication/AuthController.cs | 11 ++- .../Authentication/DiscordAuthController.cs | 31 +++++++- .../Extensions/KeyCacheExtensions.cs | 21 +++++ Foxnouns.Backend/Program.cs | 1 - Foxnouns.Backend/Services/AuthService.cs | 44 ++++++++++- Foxnouns.Backend/Services/KeyCacheService.cs | 17 +++-- Foxnouns.Frontend/src/lib/api/auth.ts | 18 +++++ .../src/routes/+layout.server.ts | 4 +- .../src/routes/auth/login/+page.svelte | 2 +- .../routes/auth/login/discord/+page.server.ts | 59 ++++++++++++-- .../routes/auth/login/discord/+page.svelte | 76 ++++++++++++++++++- Foxnouns.Frontend/svelte.config.js | 3 + 12 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 Foxnouns.Backend/Extensions/KeyCacheExtensions.cs create mode 100644 Foxnouns.Frontend/src/lib/api/auth.ts diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index e224be3..d944dd8 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -1,4 +1,5 @@ using System.Web; +using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; using NodaTime; @@ -34,11 +35,19 @@ public class AuthController(Config config, KeyCacheService keyCacheSvc, ILogger string? Tumblr ); - internal record AuthResponse( + public record AuthResponse( UserRendererService.UserResponse User, string Token, Instant ExpiresAt ); + public record CallbackResponse( + bool HasAccount, // If true, user has an account, but it's deleted + string Ticket, + string? RemoteUsername + ); + + public record OauthRegisterRequest(string Ticket, string Username); + public record CallbackRequest(string Code, string State); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index b3f93ae..1a46798 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -1,7 +1,10 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; @@ -18,6 +21,10 @@ public class DiscordAuthController( UserRendererService userRendererSvc) : ApiControllerBase { [HttpPost("callback")] + // TODO: duplicating attribute doesn't work, find another way to mark both as possible response + // leaving it here for documentation purposes + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req) { CheckRequirements(); @@ -30,7 +37,29 @@ public class DiscordAuthController( logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, remoteUser.Id); - throw new NotImplementedException(); + var ticket = OauthUtils.RandomToken(); + await keyCacheSvc.SetKeyAsync($"discord:{ticket}", remoteUser, Duration.FromMinutes(20)); + + return Ok(new AuthController.CallbackResponse(false, ticket, remoteUser.Username)); + } + + [HttpPost("register")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req) + { + var remoteUser = await keyCacheSvc.GetKeyAsync($"discord:{req.Ticket}"); + if (remoteUser == null) throw new ApiError.BadRequest("Invalid 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 FoxnounsError("Discord ticket was issued for user with existing link"); + } + + var user = await authSvc.CreateUserWithRemoteAuthAsync(req.Username, AuthType.Discord, remoteUser.Id, + remoteUser.Username); + + return Ok(await GenerateUserTokenAsync(user)); } private async Task GenerateUserTokenAsync(User user) diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs new file mode 100644 index 0000000..bea0ec6 --- /dev/null +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -0,0 +1,21 @@ +using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; +using NodaTime; + +namespace Foxnouns.Backend.Extensions; + +public static class KeyCacheExtensions +{ + public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheSvc) + { + var state = OauthUtils.RandomToken(); + await keyCacheSvc.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); + return state; + } + + public static async Task ValidateAuthStateAsync(this KeyCacheService keyCacheSvc, string state) + { + var val = await keyCacheSvc.GetKeyAsync($"oauth_state:{state}", delete: true); + if (val == null) throw new ApiError.BadRequest("Invalid OAuth state"); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index b3c623b..4f31480 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -6,7 +6,6 @@ using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using NodaTime; // Read version information from .version in the repository root await BuildInfo.ReadBuildInfo(); diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index 69c8dc0..a61fbb9 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -34,6 +34,37 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator 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) + { + AssertValidAuthType(authType, instance); + + if (await db.Users.AnyAsync(u => u.Username == username)) + throw new ApiError.BadRequest("Username is already taken"); + + var user = new User + { + Id = snowflakeGenerator.GenerateSnowflake(), + Username = username, + AuthMethods = + { + new AuthMethod + { + Id = snowflakeGenerator.GenerateSnowflake(), AuthType = authType, RemoteId = remoteId, + RemoteUsername = remoteUsername, FediverseApplication = instance + } + } + }; + + db.Add(user); + return user; + } ///
/// Authenticates a user with email and password. @@ -81,10 +112,7 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator public async Task AuthenticateUserAsync(AuthType authType, string remoteId, FediverseApplication? instance = null) { - 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."); + AssertValidAuthType(authType, instance); return await db.Users.FirstOrDefaultAsync(u => u.AuthMethods.Any(a => @@ -115,4 +143,12 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator 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."); + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index fabc316..4b0d4b3 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -2,6 +2,7 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using NodaTime; namespace Foxnouns.Backend.Services; @@ -42,16 +43,18 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) if (count != 0) logger.Information("Removed {Count} expired keys from the database", count); } - public async Task GenerateAuthStateAsync() + public Task SetKeyAsync(string key, T obj, Duration expiresAt) where T : class => + SetKeyAsync(key, obj, clock.GetCurrentInstant() + expiresAt); + + public async Task SetKeyAsync(string key, T obj, Instant expires) where T : class { - var state = OauthUtils.RandomToken(); - await SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); - return state; + var value = JsonConvert.SerializeObject(obj); + await SetKeyAsync(key, value, expires); } - public async Task ValidateAuthStateAsync(string state) + public async Task GetKeyAsync(string key, bool delete = false) where T : class { - var val = await GetKeyAsync($"oauth_state:{state}", delete: true); - if (val == null) throw new ApiError.BadRequest("Invalid OAuth state"); + var value = await GetKeyAsync(key, delete: false); + return value == null ? default : JsonConvert.DeserializeObject(value); } } \ No newline at end of file diff --git a/Foxnouns.Frontend/src/lib/api/auth.ts b/Foxnouns.Frontend/src/lib/api/auth.ts new file mode 100644 index 0000000..9e5b2d1 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/api/auth.ts @@ -0,0 +1,18 @@ +import type { User } from "./user"; + +export type CallbackRequest = { + code: string; + state: string; +}; + +export type CallbackResponse = { + has_account: boolean; + ticket: string; + remote_username: string | null; +}; + +export type AuthResponse = { + user: User; + token: string; + expires_at: string; +}; diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts index 2d1e4ba..78f6517 100644 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -2,12 +2,12 @@ import type Meta from "$lib/api/meta"; import type { User } from "$lib/api/user"; import request from "$lib/request"; -export async function load({ fetch }) { +export async function load({ fetch, locals }) { const meta = await request(fetch, "GET", "/meta"); let user: User | undefined; try { user = await request(fetch, "GET", "/users/@me"); } catch {} - return { meta, user }; + return { meta, user, token: locals.token }; } diff --git a/Foxnouns.Frontend/src/routes/auth/login/+page.svelte b/Foxnouns.Frontend/src/routes/auth/login/+page.svelte index 14e4040..bead6b4 100644 --- a/Foxnouns.Frontend/src/routes/auth/login/+page.svelte +++ b/Foxnouns.Frontend/src/routes/auth/login/+page.svelte @@ -9,7 +9,7 @@

Log in with email address

-
+
diff --git a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts index 2ebde3c..75e97ec 100644 --- a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts @@ -1,10 +1,55 @@ -import { fastRequest } from "$lib/request"; +import request from "$lib/request"; +import type { AuthResponse, CallbackResponse } from "$lib/api/auth"; -export const load = async ({ fetch, url }) => { - await fastRequest(fetch, "POST", "/auth/discord/callback", { - body: { - code: url.searchParams.get("code"), - state: url.searchParams.get("state"), +export const load = async ({ fetch, url, cookies, parent }) => { + const data = await parent(); + if (data.user) { + return { loggedIn: true, token: data.token, user: data.user }; + } + + const resp = await request( + fetch, + "POST", + "/auth/discord/callback", + { + body: { + code: url.searchParams.get("code"), + state: url.searchParams.get("state"), + }, }, - }); + ); + + console.log(JSON.stringify(resp)); + + if ("token" in resp) { + const authResp = resp as AuthResponse; + cookies.set("pronounscc-token", authResp.token, { path: "/" }); + return { loggedIn: true, token: authResp.token, user: authResp.user }; + } + + const callbackResp = resp as CallbackResponse; + return { + loggedIn: false, + hasAccount: callbackResp.has_account, + ticket: resp.ticket, + remoteUsername: resp.remote_username, + }; +}; + +export const actions = { + register: async ({ cookies, request: req, fetch, locals }) => { + const data = await req.formData(); + const username = data.get("username"); + const ticket = data.get("ticket"); + + console.log(JSON.stringify({ username, ticket })); + + const resp = await request(fetch, "POST", "/auth/discord/register", { + body: { username, ticket }, + }); + cookies.set("pronounscc-token", resp.token, { path: "/" }); + locals.token = resp.token; + + return { token: resp.token, user: resp.user }; + }, }; diff --git a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte index 3cb3fbc..c7968b1 100644 --- a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte +++ b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte @@ -1,5 +1,79 @@ -

omg its a login page

+
diff --git a/Foxnouns.Frontend/svelte.config.js b/Foxnouns.Frontend/svelte.config.js index 626d6f1..446cf25 100644 --- a/Foxnouns.Frontend/svelte.config.js +++ b/Foxnouns.Frontend/svelte.config.js @@ -15,6 +15,9 @@ const config = { env: { privatePrefix: "PRIVATE_", }, + csrf: { + checkOrigin: false, + }, }, }; From d6c9345dba54f2c023f322c739a167b3d528ff3d Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 8 Jul 2024 19:03:04 +0200 Subject: [PATCH 011/261] too many things to list (notably, user avatar update) --- Foxnouns.Backend/Config.cs | 29 +++- .../Authentication/DiscordAuthController.cs | 2 +- .../Controllers/UsersController.cs | 14 +- .../Database/DatabaseQueryExtensions.cs | 2 +- .../DatabaseContextModelSnapshot.cs | 25 ++-- .../Database/Models/Application.cs | 6 +- Foxnouns.Backend/Database/Snowflake.cs | 1 + .../Extensions/KeyCacheExtensions.cs | 2 +- .../Extensions/WebApplicationExtensions.cs | 11 +- Foxnouns.Backend/Foxnouns.Backend.csproj | 7 + Foxnouns.Backend/Jobs/AvatarUpdateJob.cs | 137 ++++++++++++++++++ .../Middleware/AuthenticationMiddleware.cs | 34 ++++- .../Middleware/ErrorHandlerMiddleware.cs | 27 +++- Foxnouns.Backend/Program.cs | 34 ++++- Foxnouns.Backend/Services/AuthService.cs | 8 +- .../Utils/{OauthUtils.cs => AuthUtils.cs} | 8 +- Foxnouns.Backend/config.example.ini | 23 ++- Foxnouns.Frontend/src/routes/+error.svelte | 14 ++ .../routes/auth/login/discord/+page.server.ts | 4 - .../src/routes/neofox_confused_2048.png | Bin 0 -> 146626 bytes 20 files changed, 341 insertions(+), 47 deletions(-) create mode 100644 Foxnouns.Backend/Jobs/AvatarUpdateJob.cs rename Foxnouns.Backend/Utils/{OauthUtils.cs => AuthUtils.cs} (92%) create mode 100644 Foxnouns.Frontend/src/routes/+error.svelte create mode 100755 Foxnouns.Frontend/src/routes/neofox_confused_2048.png diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index e4d81c7..6db3888 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -10,14 +10,23 @@ public class Config public string Address => $"http://{Host}:{Port}"; - public string? SeqLogUrl { get; init; } - public LogEventLevel LogEventLevel { get; init; } = LogEventLevel.Debug; - + public LoggingConfig Logging { get; init; } = new(); public DatabaseConfig Database { get; init; } = new(); + public JobsConfig Jobs { get; init; } = new(); + public StorageConfig Storage { get; init; } = new(); public DiscordAuthConfig DiscordAuth { get; init; } = new(); public GoogleAuthConfig GoogleAuth { get; init; } = new(); public TumblrAuthConfig TumblrAuth { get; init; } = new(); + 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 class DatabaseConfig { public string Url { get; init; } = string.Empty; @@ -25,6 +34,20 @@ public class Config public int? MaxPoolSize { get; init; } } + public class JobsConfig + { + public string Redis { get; init; } = string.Empty; + public int Workers { get; init; } = 5; + } + + 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 DiscordAuthConfig { public bool Enabled => ClientId != null && ClientSecret != null; diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 1a46798..29700f2 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -37,7 +37,7 @@ public class DiscordAuthController( logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, remoteUser.Id); - var ticket = OauthUtils.RandomToken(); + var ticket = AuthUtils.RandomToken(); await keyCacheSvc.SetKeyAsync($"discord:{ticket}", remoteUser, Duration.FromMinutes(20)); return Ok(new AuthController.CallbackResponse(false, ticket, remoteUser.Username)); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index c6101bb..acd9439 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,4 +1,5 @@ using Foxnouns.Backend.Database; +using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; @@ -9,7 +10,7 @@ namespace Foxnouns.Backend.Controllers; public class UsersController(DatabaseContext db, UserRendererService userRendererService) : ApiControllerBase { [HttpGet("{userRef}")] - public async Task GetUser(string userRef) + public async Task GetUserAsync(string userRef) { var user = await db.ResolveUserAsync(userRef); return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); @@ -17,17 +18,20 @@ public class UsersController(DatabaseContext db, UserRendererService userRendere [HttpGet("@me")] [Authorize("identify")] - public async Task GetMe() + public async Task GetMeAsync() { var user = await db.ResolveUserAsync(CurrentUser!.Id); return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); } [HttpPatch("@me")] - public Task UpdateUser([FromBody] UpdateUserRequest req) + public async Task UpdateUserAsync([FromBody] UpdateUserRequest req) { - throw new NotImplementedException(); + if (req.Avatar != null) + AvatarUpdateJob.QueueUpdateUserAvatar(CurrentUser!.Id, req.Avatar); + + return NoContent(); } - public record UpdateUserRequest(string? Username, string? DisplayName); + public record UpdateUserRequest(string? Username, string? DisplayName, string? Avatar); } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 0b77ddb..fc63c05 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -76,7 +76,7 @@ public static class DatabaseQueryExtensions { Id = new Snowflake(0), ClientId = RandomNumberGenerator.GetHexString(32, true), - ClientSecret = OauthUtils.RandomToken(48), + ClientSecret = AuthUtils.RandomToken(48), Name = "pronouns.cc", Scopes = ["*"], RedirectUris = [], diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 13c10dd..360d43d 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -1,4 +1,5 @@ // +using System; using Foxnouns.Backend.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -334,7 +335,7 @@ namespace Foxnouns.Backend.Database.Migrations .IsRequired() .HasConstraintName("fk_members_users_user_id"); - b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Fields#System.Collections.Generic.List", "Fields", b1 => { b1.Property("MemberId") .HasColumnType("bigint"); @@ -344,7 +345,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("MemberId"); - b1.ToTable("members"); + b1.ToTable("members", (string)null); b1.ToJson("fields"); @@ -353,7 +354,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_members_members_id"); }); - b.OwnsOne("System.Collections.Generic.List", "Names", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Names#System.Collections.Generic.List", "Names", b1 => { b1.Property("MemberId") .HasColumnType("bigint"); @@ -363,7 +364,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("MemberId"); - b1.ToTable("members"); + b1.ToTable("members", (string)null); b1.ToJson("names"); @@ -372,7 +373,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_members_members_id"); }); - b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Pronouns#System.Collections.Generic.List", "Pronouns", b1 => { b1.Property("MemberId") .HasColumnType("bigint"); @@ -382,7 +383,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("MemberId"); - b1.ToTable("members"); + b1.ToTable("members", (string)null); b1.ToJson("pronouns"); @@ -426,7 +427,7 @@ namespace Foxnouns.Backend.Database.Migrations modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => { - b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => { b1.Property("UserId") .HasColumnType("bigint"); @@ -437,7 +438,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("UserId") .HasName("pk_users"); - b1.ToTable("users"); + b1.ToTable("users", (string)null); b1.ToJson("fields"); @@ -446,7 +447,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_users_users_user_id"); }); - b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => { b1.Property("UserId") .HasColumnType("bigint"); @@ -457,7 +458,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("UserId") .HasName("pk_users"); - b1.ToTable("users"); + b1.ToTable("users", (string)null); b1.ToJson("names"); @@ -466,7 +467,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_users_users_user_id"); }); - b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => { b1.Property("UserId") .HasColumnType("bigint"); @@ -477,7 +478,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("UserId") .HasName("pk_users"); - b1.ToTable("users"); + b1.ToTable("users", (string)null); b1.ToJson("pronouns"); diff --git a/Foxnouns.Backend/Database/Models/Application.cs b/Foxnouns.Backend/Database/Models/Application.cs index 95416f1..f64bfc9 100644 --- a/Foxnouns.Backend/Database/Models/Application.cs +++ b/Foxnouns.Backend/Database/Models/Application.cs @@ -15,14 +15,14 @@ public class Application : BaseModel string[] redirectUrls) { var clientId = RandomNumberGenerator.GetHexString(32, true); - var clientSecret = OauthUtils.RandomToken(); + var clientSecret = AuthUtils.RandomToken(); - if (scopes.Except(OauthUtils.ApplicationScopes).Any()) + if (scopes.Except(AuthUtils.ApplicationScopes).Any()) { throw new ArgumentException("Invalid scopes passed to Application.Create", nameof(scopes)); } - if (redirectUrls.Any(s => !OauthUtils.ValidateRedirectUri(s))) + if (redirectUrls.Any(s => !AuthUtils.ValidateRedirectUri(s))) { throw new ArgumentException("Invalid redirect URLs passed to Application.Create", nameof(redirectUrls)); } diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index feaf27b..04937da 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -56,6 +56,7 @@ public readonly struct Snowflake(ulong value) public override bool Equals(object? obj) => obj is 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. diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index bea0ec6..3b8e35c 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -8,7 +8,7 @@ public static class KeyCacheExtensions { public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheSvc) { - var state = OauthUtils.RandomToken(); + var state = AuthUtils.RandomToken(); await keyCacheSvc.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); return state; } diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index b03a9eb..0bf334c 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -1,4 +1,5 @@ using Foxnouns.Backend.Database; +using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Microsoft.EntityFrameworkCore; @@ -19,7 +20,7 @@ public static class WebApplicationExtensions var logCfg = new LoggerConfiguration() .Enrich.FromLogContext() - .MinimumLevel.Is(config.LogEventLevel) + .MinimumLevel.Is(config.Logging.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) @@ -28,9 +29,9 @@ public static class WebApplicationExtensions .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) .WriteTo.Console(); - if (config.SeqLogUrl != null) + if (config.Logging.SeqLogUrl != null) { - logCfg.WriteTo.Seq(config.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); + logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, restrictedToMinimumLevel: LogEventLevel.Verbose); } Log.Logger = logCfg.CreateLogger(); @@ -68,7 +69,9 @@ public static class WebApplicationExtensions .AddScoped() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + // Background job classes + .AddTransient(); public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services .AddScoped() diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index f92814a..d554482 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -7,6 +7,9 @@ + + + @@ -14,14 +17,18 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + + + diff --git a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs new file mode 100644 index 0000000..56aad8a --- /dev/null +++ b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs @@ -0,0 +1,137 @@ +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Utils; +using Hangfire; +using Minio; +using Minio.DataModel.Args; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace Foxnouns.Backend.Jobs; + +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +public class AvatarUpdateJob(DatabaseContext db, IMinioClient minio, Config config, ILogger logger) +{ + private readonly string[] _validContentTypes = ["image/png", "image/webp", "image/jpeg"]; + + public static void QueueUpdateUserAvatar(Snowflake id, string? newAvatar) + { + if (newAvatar != null) + BackgroundJob.Enqueue(job => job.UpdateUserAvatar(id, newAvatar)); + else + BackgroundJob.Enqueue(job => job.ClearUserAvatar(id)); + } + + public static void QueueUpdateMemberAvatar(Snowflake id, string? newAvatar) => + BackgroundJob.Enqueue(job => + newAvatar != null ? job.UpdateMemberAvatar(id, newAvatar) : job.ClearMemberAvatar(id)); + + public async Task UpdateUserAvatar(Snowflake id, string newAvatar) + { + var 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 + { + var image = await ConvertAvatar(newAvatar); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); + image.Seek(0, SeekOrigin.Begin); + var prevHash = user.Avatar; + + await minio.PutObjectAsync(new PutObjectArgs() + .WithBucket(config.Storage.Bucket) + .WithObject(UserAvatarPath(id, hash)) + .WithObjectSize(image.Length) + .WithStreamData(image) + .WithContentType("image/webp") + ); + + user.Avatar = hash; + await db.SaveChangesAsync(); + + if (prevHash != null && prevHash != hash) + await minio.RemoveObjectAsync(new RemoveObjectArgs() + .WithBucket(config.Storage.Bucket) + .WithObject(UserAvatarPath(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); + } + } + + public async Task ClearUserAvatar(Snowflake id) + { + var 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 minio.RemoveObjectAsync(new RemoveObjectArgs() + .WithBucket(config.Storage.Bucket) + .WithObject(UserAvatarPath(user.Id, user.Avatar)) + ); + + user.Avatar = null; + await db.SaveChangesAsync(); + } + + public Task UpdateMemberAvatar(Snowflake id, string newAvatar) + { + throw new NotImplementedException(); + } + + public Task ClearMemberAvatar(Snowflake id) + { + throw new NotImplementedException(); + } + + private async Task ConvertAvatar(string uri) + { + if (!uri.StartsWith("data:image/")) + throw new ArgumentException("Not a data URI", nameof(uri)); + + var split = uri.Remove(0, "data:".Length).Split(";base64,"); + var contentType = split[0]; + var encoded = split[1]; + if (!_validContentTypes.Contains(contentType)) + throw new ArgumentException("Invalid content type for image", nameof(uri)); + + if (!AuthUtils.TryFromBase64String(encoded, out var rawImage)) + throw new ArgumentException("Invalid base64 string", nameof(uri)); + + var image = Image.Load(rawImage); + + var processor = new ResizeProcessor( + new ResizeOptions { Size = new Size(512), Mode = ResizeMode.Crop, 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 }); + return stream; + } + + private static string UserAvatarPath(Snowflake id, string hash) => $"users/{id}/avatars/{hash}.webp"; + private static string MemberAvatarPath(Snowflake id, string hash) => $"members/{id}/avatars/{hash}.webp"; +} \ No newline at end of file diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index 4435fa6..8ad5df7 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; +using Hangfire.Dashboard; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -21,7 +22,7 @@ public class AuthenticationMiddleware(DatabaseContext db, IClock clock) : IMiddl } var header = ctx.Request.Headers.Authorization.ToString(); - if (!OauthUtils.TryFromBase64String(header, out var rawToken)) + if (!AuthUtils.TryFromBase64String(header, out var rawToken)) { await next(ctx); return; @@ -63,4 +64,33 @@ public static class HttpContextExtensions } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AuthenticateAttribute : Attribute; \ No newline at end of file +public class AuthenticateAttribute : Attribute; + +/// +/// Authentication filter for the Hangfire dashboard. Uses the cookie created by the frontend +/// (and otherwise only read by the frontend) to only allow admins to use it. +/// +public class HangfireDashboardAuthorizationFilter(IServiceProvider services) : IDashboardAsyncAuthorizationFilter +{ + public async Task AuthorizeAsync(DashboardContext context) + { + await using var scope = services.CreateAsyncScope(); + + await using var db = scope.ServiceProvider.GetRequiredService(); + var clock = scope.ServiceProvider.GetRequiredService(); + + var httpContext = context.GetHttpContext(); + + if (!httpContext.Request.Cookies.TryGetValue("pronounscc-token", out var cookie)) return false; + + if (!AuthUtils.TryFromBase64String(cookie!, out var rawToken)) return false; + + 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); + + return oauthToken?.User.Role == UserRole.Admin; + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index da4804b..e9b7c89 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -4,7 +4,7 @@ using Newtonsoft.Json.Converters; namespace Foxnouns.Backend.Middleware; -public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware +public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { @@ -22,6 +22,18 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware { logger.Error(e, "Error in {ClassName} ({Path}) after response started being sent", typeName, ctx.Request.Path); + + sentry.CaptureException(e, scope => + { + var user = ctx.GetUser(); + if (user != null) + scope.User = new SentryUser + { + Id = user.Id.ToString(), + Username = user.Username + }; + }); + return; } @@ -59,6 +71,17 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware { logger.Error(e, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path); } + + var errorId = sentry.CaptureException(e, scope => + { + var 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; @@ -67,6 +90,7 @@ public class ErrorHandlerMiddleware(ILogger baseLogger) : IMiddleware { Status = (int)HttpStatusCode.InternalServerError, Code = ErrorCode.InternalServerError, + ErrorId = errorId.ToString(), Message = "Internal server error", })); } @@ -79,6 +103,7 @@ public record HttpApiError [JsonConverter(typeof(StringEnumConverter))] public required ErrorCode Code { get; init; } + public string? ErrorId { get; init; } public required string Message { get; init; } diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 4f31480..ac746f7 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -2,10 +2,16 @@ using Foxnouns.Backend; using Foxnouns.Backend.Database; using Serilog; using Foxnouns.Backend.Extensions; +using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Hangfire; +using Hangfire.Redis.StackExchange; using Microsoft.AspNetCore.Mvc; +using Minio; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using Sentry.Extensibility; +using Sentry.Hangfire; // Read version information from .version in the repository root await BuildInfo.ReadBuildInfo(); @@ -16,6 +22,13 @@ var config = builder.AddConfiguration(); builder.AddSerilog(); +builder.WebHost.UseSentry(opts => +{ + opts.Dsn = config.Logging.SentryUrl; + opts.TracesSampleRate = config.Logging.SentryTracesSampleRate; + opts.MaxRequestBodySize = RequestSize.Small; +}); + builder.Services .AddControllers() .AddNewtonsoftJson(options => @@ -44,7 +57,17 @@ builder.Services .AddCustomServices() .AddCustomMiddleware() .AddEndpointsApiExplorer() - .AddSwaggerGen(); + .AddSwaggerGen() + .AddMinio(c => + c.WithEndpoint(config.Storage.Endpoint) + .WithCredentials(config.Storage.AccessKey, config.Storage.SecretKey) + .Build()); + +builder.Services.AddHangfire(c => c.UseSentry().UseRedisStorage(config.Jobs.Redis, new RedisStorageOptions + { + Prefix = "foxnouns_" + })) + .AddHangfireServer(options => { options.WorkerCount = config.Jobs.Workers; }); var app = builder.Build(); @@ -52,12 +75,21 @@ 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("/hangfire", new DashboardOptions +{ + AppPath = null, + AsyncAuthorization = [new HangfireDashboardAuthorizationFilter(app.Services)] +}); + app.Urls.Clear(); app.Urls.Add(config.Address); diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index a61fbb9..7eedb84 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -34,7 +34,7 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator return user; } - + /// /// Creates a new user with the given username and remote authentication method. /// To create a user with email authentication, use @@ -44,7 +44,7 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator string remoteUsername, FediverseApplication? instance = null) { AssertValidAuthType(authType, instance); - + if (await db.Users.AnyAsync(u => u.Username == username)) throw new ApiError.BadRequest("Username is already taken"); @@ -121,7 +121,7 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator public (string, Token) GenerateToken(User user, Application application, string[] scopes, Instant expires) { - if (!OauthUtils.ValidateScopes(application, scopes)) + if (!AuthUtils.ValidateScopes(application, scopes)) throw new ApiError.BadRequest("Invalid scopes requested for this token"); var (token, hash) = GenerateToken(); @@ -138,7 +138,7 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator private static (string, byte[]) GenerateToken() { - var token = OauthUtils.RandomToken(48); + var token = AuthUtils.RandomToken(48); var hash = SHA512.HashData(Convert.FromBase64String(token)); return (token, hash); diff --git a/Foxnouns.Backend/Utils/OauthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs similarity index 92% rename from Foxnouns.Backend/Utils/OauthUtils.cs rename to Foxnouns.Backend/Utils/AuthUtils.cs index 4cbc83a..45c9ad5 100644 --- a/Foxnouns.Backend/Utils/OauthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -3,7 +3,7 @@ using Foxnouns.Backend.Database.Models; namespace Foxnouns.Backend.Utils; -public static class OauthUtils +public static class AuthUtils { public const string ClientCredentials = "client_credentials"; public const string AuthorizationCode = "authorization_code"; @@ -63,7 +63,6 @@ public static class OauthUtils } } - public static bool TryFromBase64String(string b64, out byte[] bytes) { try @@ -71,8 +70,9 @@ public static class OauthUtils bytes = Convert.FromBase64String(b64); return true; } - catch + catch (Exception e) { + Console.WriteLine($"Error converting string: {e}"); bytes = []; return false; } @@ -80,4 +80,6 @@ public static class OauthUtils public static string RandomToken(int bytes = 48) => Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); + + public const int MaxAuthMethodsPerType = 3; // Maximum of 3 Discord accounts, 3 emails, etc } \ No newline at end of file diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index d361d33..2586a62 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -5,10 +5,17 @@ Port = 5000 ; The base *external* URL BaseUrl = https://pronouns.localhost +[Logging] ; The level to log things at. Valid settings: Verbose, Debug, Information, Warning, Error, Fatal -LogEventLevel = Verbose - +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 [Database] ; The database URL in ADO.NET format. @@ -19,6 +26,18 @@ Timeout = 5 ; The maximum number of open connections. Defaults to 50. MaxPoolSize = 50 +[Jobs] +; The connection string for the Redis server. +Redis = localhost:6379 +; The number of workers to use for background jobs. Defaults to 5. +Workers = 5 + +[Storage] +Endpoint = +AccessKey = +SecretKey = +Bucket = pronounscc + [DiscordAuth] ClientId = ClientSecret = diff --git a/Foxnouns.Frontend/src/routes/+error.svelte b/Foxnouns.Frontend/src/routes/+error.svelte new file mode 100644 index 0000000..9aec269 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/+error.svelte @@ -0,0 +1,14 @@ + + +{#if $page.status === 404} +
+ A very confused-looking fox +

Not found

+

Our foxes can't find the page you're looking for, sorry!

+
+{:else} + div.has-text-centered +{/if} diff --git a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts index 75e97ec..051a0c0 100644 --- a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts @@ -19,8 +19,6 @@ export const load = async ({ fetch, url, cookies, parent }) => { }, ); - console.log(JSON.stringify(resp)); - if ("token" in resp) { const authResp = resp as AuthResponse; cookies.set("pronounscc-token", authResp.token, { path: "/" }); @@ -42,8 +40,6 @@ export const actions = { const username = data.get("username"); const ticket = data.get("ticket"); - console.log(JSON.stringify({ username, ticket })); - const resp = await request(fetch, "POST", "/auth/discord/register", { body: { username, ticket }, }); diff --git a/Foxnouns.Frontend/src/routes/neofox_confused_2048.png b/Foxnouns.Frontend/src/routes/neofox_confused_2048.png new file mode 100755 index 0000000000000000000000000000000000000000..2813f170168d89d8d931beaa4be21b7ff74636a3 GIT binary patch literal 146626 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
jRk%5MsrGD- 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 z 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 b{oGPpTgqo1zdya``qj^BpGiM$ef$3bw1^eH literal 0 HcmV?d00001 From e95e0a79ff74ebd636ca7b8ec638a0b660c356ec Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 12 Jul 2024 17:12:24 +0200 Subject: [PATCH 012/261] feat: add PATCH request support, expand PATCH /users/@me, serialize enums correctly --- Foxnouns.Backend/Config.cs | 3 +- .../Authentication/DiscordAuthController.cs | 2 +- .../Controllers/MetaController.cs | 2 +- .../Controllers/UsersController.cs | 63 +++++++++++++-- .../Database/DatabaseQueryExtensions.cs | 2 +- Foxnouns.Backend/Database/Models/User.cs | 2 + Foxnouns.Backend/ExpectedError.cs | 46 ++++++++++- Foxnouns.Backend/Jobs/AvatarUpdateJob.cs | 67 ++++++++++++++-- .../Middleware/ErrorHandlerMiddleware.cs | 12 ++- Foxnouns.Backend/Program.cs | 35 ++++++--- Foxnouns.Backend/Services/AuthService.cs | 4 +- .../Services/MemberRendererService.cs | 8 +- .../Services/UserRendererService.cs | 59 ++++++++++++-- Foxnouns.Backend/Utils/AuthUtils.cs | 9 ++- Foxnouns.Backend/Utils/PatchRequest.cs | 35 +++++++++ .../Utils/ScreamingSnakeCaseEnumConverter.cs | 18 +++++ Foxnouns.Backend/Utils/ValidationUtils.cs | 76 +++++++++++++++++++ Foxnouns.Backend/config.example.ini | 2 + SCOPES.md | 18 +++++ STYLE.md | 12 +++ 20 files changed, 427 insertions(+), 48 deletions(-) create mode 100644 Foxnouns.Backend/Utils/PatchRequest.cs create mode 100644 Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs create mode 100644 Foxnouns.Backend/Utils/ValidationUtils.cs create mode 100644 SCOPES.md create mode 100644 STYLE.md diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 6db3888..1cb21c6 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -6,7 +6,8 @@ public class Config { public string Host { get; init; } = "localhost"; public int Port { get; init; } = 3000; - public string BaseUrl { get; init; } = null!; + public string BaseUrl { get; set; } = null!; + public string MediaBaseUrl { get; set; } = null!; public string Address => $"http://{Host}:{Port}"; diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 29700f2..35049ae 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -48,7 +48,7 @@ public class DiscordAuthController( public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req) { var remoteUser = await keyCacheSvc.GetKeyAsync($"discord:{req.Ticket}"); - if (remoteUser == null) throw new ApiError.BadRequest("Invalid ticket"); + if (remoteUser == null) throw new ApiError.BadRequest("Invalid ticket", "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", diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index f39810e..9d2c991 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -20,7 +20,7 @@ public class MetaController(DatabaseContext db) : ApiControllerBase ); } - [HttpGet("coffee")] + [HttpGet("/api/v2/coffee")] public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); private record MetaResponse(string Version, string Hash, int Members, UserInfo Users); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index acd9439..28d7ea8 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,8 +1,11 @@ using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; @@ -10,28 +13,76 @@ namespace Foxnouns.Backend.Controllers; public class UsersController(DatabaseContext db, UserRendererService userRendererService) : ApiControllerBase { [HttpGet("{userRef}")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetUserAsync(string userRef) { var user = await db.ResolveUserAsync(userRef); - return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); + return await GetUserInnerAsync(user); } [HttpGet("@me")] [Authorize("identify")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetMeAsync() { var user = await db.ResolveUserAsync(CurrentUser!.Id); - return Ok(await userRendererService.RenderUserAsync(user, selfUser: CurrentUser)); + return await GetUserInnerAsync(user); + } + + private async Task GetUserInnerAsync(User user) + { + return Ok(await userRendererService.RenderUserAsync( + user, + selfUser: CurrentUser, + token: CurrentToken, + renderMembers: true, + renderAuthMethods: true + )); } [HttpPatch("@me")] + [Authorize("user.update")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task UpdateUserAsync([FromBody] UpdateUserRequest req) { - if (req.Avatar != null) - AvatarUpdateJob.QueueUpdateUserAvatar(CurrentUser!.Id, req.Avatar); + await using var tx = await db.Database.BeginTransactionAsync(); + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id); - return NoContent(); + if (req.Username != null && req.Username != user.Username) + { + ValidationUtils.ValidateUsername(req.Username); + user.Username = req.Username; + } + + if (req.HasProperty(nameof(req.DisplayName))) + { + ValidationUtils.ValidateDisplayName(req.DisplayName); + user.DisplayName = req.DisplayName; + } + + if (req.HasProperty(nameof(req.Bio))) + { + ValidationUtils.ValidateBio(req.Bio); + user.Bio = req.Bio; + } + + if (req.HasProperty(nameof(req.Avatar))) + { + ValidationUtils.ValidateAvatar(req.Avatar); + AvatarUpdateJob.QueueUpdateUserAvatar(CurrentUser!.Id, req.Avatar); + } + + await db.SaveChangesAsync(); + await tx.CommitAsync(); + return Ok(await userRendererService.RenderUserAsync(user, CurrentUser, renderMembers: false, + renderAuthMethods: false)); } - public record UpdateUserRequest(string? Username, string? DisplayName, string? Avatar); + public class UpdateUserRequest : PatchRequest + { + public string? Username { get; init; } + public string? DisplayName { get; init; } + public string? Bio { get; init; } + public string? Avatar { get; init; } + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index fc63c05..9b357fa 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -110,7 +110,7 @@ public static class DatabaseQueryExtensions if (delete) { await context.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(); - await context.SaveChangesAsync(); + await context.SaveChangesAsync(); } return value.Value; diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index 238f306..986c779 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -1,3 +1,5 @@ +using System.Text.RegularExpressions; + namespace Foxnouns.Backend.Database.Models; public class User : BaseModel diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index d05571b..7dda1ad 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -1,3 +1,4 @@ +using System.Collections.ObjectModel; using System.Net; using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc.ModelBinding; @@ -21,7 +22,8 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError; public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError; - public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized); + public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized, + errorCode: ErrorCode.AuthenticationError); public class Forbidden(string message, IEnumerable? scopes = null) : ApiError(message, statusCode: HttpStatusCode.Forbidden) @@ -29,7 +31,45 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo public readonly string[] Scopes = scopes?.ToArray() ?? []; } - public class BadRequest(string message, ModelStateDictionary? modelState = null) + public class BadRequest(string message, IReadOnlyDictionary? errors = null) + : ApiError(message, statusCode: HttpStatusCode.BadRequest) + { + public BadRequest(string message, string field) : this(message, + new Dictionary { { field, message } }) + { + } + + public JObject ToJson() + { + var o = new JObject + { + { "status", (int)HttpStatusCode.BadRequest }, + { "message", Message }, + { "code", ErrorCode.BadRequest.ToString() } + }; + if (errors == null) return o; + + var a = new JArray(); + foreach (var error in errors) + { + var errorObj = new JObject + { + { "key", error.Key }, + { "errors", new JArray(new JObject { { "message", 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, statusCode: HttpStatusCode.BadRequest) { public JObject ToJson() @@ -37,6 +77,7 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo var o = new JObject { { "status", (int)HttpStatusCode.BadRequest }, + { "message", Message }, { "code", ErrorCode.BadRequest.ToString() } }; if (modelState == null) return o; @@ -52,7 +93,6 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo new JArray(error.Value!.Errors.Select(e => new JObject { { "message", e.ErrorMessage } })) } }; - a.Add(errorObj); } diff --git a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs index 56aad8a..6404591 100644 --- a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs +++ b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs @@ -12,7 +12,7 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace Foxnouns.Backend.Jobs; -[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "Hangfire jobs need to be public")] public class AvatarUpdateJob(DatabaseContext db, IMinioClient minio, Config config, ILogger logger) { private readonly string[] _validContentTypes = ["image/png", "image/webp", "image/jpeg"]; @@ -61,7 +61,7 @@ public class AvatarUpdateJob(DatabaseContext db, IMinioClient minio, Config conf .WithBucket(config.Storage.Bucket) .WithObject(UserAvatarPath(id, prevHash)) ); - + logger.Information("Updated avatar for user {UserId}", id); } catch (ArgumentException ae) @@ -94,14 +94,69 @@ public class AvatarUpdateJob(DatabaseContext db, IMinioClient minio, Config conf await db.SaveChangesAsync(); } - public Task UpdateMemberAvatar(Snowflake id, string newAvatar) + public async Task UpdateMemberAvatar(Snowflake id, string newAvatar) { - throw new NotImplementedException(); + var 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 + { + var image = await ConvertAvatar(newAvatar); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); + image.Seek(0, SeekOrigin.Begin); + var prevHash = member.Avatar; + + await minio.PutObjectAsync(new PutObjectArgs() + .WithBucket(config.Storage.Bucket) + .WithObject(MemberAvatarPath(id, hash)) + .WithObjectSize(image.Length) + .WithStreamData(image) + .WithContentType("image/webp") + ); + + member.Avatar = hash; + await db.SaveChangesAsync(); + + if (prevHash != null && prevHash != hash) + await minio.RemoveObjectAsync(new RemoveObjectArgs() + .WithBucket(config.Storage.Bucket) + .WithObject(MemberAvatarPath(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); + } } - public Task ClearMemberAvatar(Snowflake id) + public async Task ClearMemberAvatar(Snowflake id) { - throw new NotImplementedException(); + var 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 minio.RemoveObjectAsync(new RemoveObjectArgs() + .WithBucket(config.Storage.Bucket) + .WithObject(MemberAvatarPath(member.Id, member.Avatar)) + ); + + member.Avatar = null; + await db.SaveChangesAsync(); } private async Task ConvertAvatar(string uri) diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index e9b7c89..39dfd85 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,4 +1,5 @@ using System.Net; +using Foxnouns.Backend.Utils; using Newtonsoft.Json; using Newtonsoft.Json.Converters; @@ -54,6 +55,12 @@ public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddlewa return; } + if (ae is ApiError.BadRequest br) + { + await ctx.Response.WriteAsync(br.ToJson().ToString()); + return; + } + await ctx.Response.WriteAsync(JsonConvert.SerializeObject(new HttpApiError { Status = (int)ae.StatusCode, @@ -71,7 +78,7 @@ public class ErrorHandlerMiddleware(ILogger baseLogger, IHub sentry) : IMiddlewa { logger.Error(e, "Exception in {ClassName} ({Path})", typeName, ctx.Request.Path); } - + var errorId = sentry.CaptureException(e, scope => { var user = ctx.GetUser(); @@ -101,8 +108,9 @@ public record HttpApiError { public required int Status { get; init; } - [JsonConverter(typeof(StringEnumConverter))] + [JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] public required ErrorCode Code { get; init; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string? ErrorId { get; init; } public required string Message { get; init; } diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index ac746f7..4a311a7 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -4,6 +4,7 @@ using Serilog; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Hangfire; using Hangfire.Redis.StackExchange; using Microsoft.AspNetCore.Mvc; @@ -22,24 +23,34 @@ var config = builder.AddConfiguration(); builder.AddSerilog(); -builder.WebHost.UseSentry(opts => -{ - opts.Dsn = config.Logging.SentryUrl; - opts.TracesSampleRate = config.Logging.SentryTracesSampleRate; - opts.MaxRequestBodySize = RequestSize.Small; -}); +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; + }); builder.Services .AddControllers() .AddNewtonsoftJson(options => - options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + options.SerializerSettings.ContractResolver = new PatchRequestContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() - }) + }; + }) .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult( - new ApiError.BadRequest("Bad request", actionContext.ModelState).ToJson() + new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() ); }); @@ -64,9 +75,9 @@ builder.Services .Build()); builder.Services.AddHangfire(c => c.UseSentry().UseRedisStorage(config.Jobs.Redis, new RedisStorageOptions - { - Prefix = "foxnouns_" - })) +{ + Prefix = "foxnouns_" +})) .AddHangfireServer(options => { options.WorkerCount = config.Jobs.Workers; }); var app = builder.Build(); diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index 7eedb84..a87a50c 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -46,7 +46,7 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator AssertValidAuthType(authType, instance); if (await db.Users.AnyAsync(u => u.Username == username)) - throw new ApiError.BadRequest("Username is already taken"); + throw new ApiError.BadRequest("Username is already taken", "username"); var user = new User { @@ -122,7 +122,7 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator 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"); + throw new ApiError.BadRequest("Invalid scopes requested for this token", "scopes"); var (token, hash) = GenerateToken(); return (token, new Token diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 73d1998..e151777 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -3,16 +3,20 @@ using Foxnouns.Backend.Database.Models; namespace Foxnouns.Backend.Services; -public class MemberRendererService(DatabaseContext db) +public class MemberRendererService(DatabaseContext db, Config config) { public PartialMember RenderPartialMember(Member member) => new(member.Id, member.Name, - member.DisplayName, member.Bio, member.Names, member.Pronouns); + member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns); + + private string? AvatarUrlFor(Member member) => + member.Avatar != null ? $"{config.MediaBaseUrl}/members/{member.Id}/avatars/{member.Avatar}.webp" : null; public record PartialMember( Snowflake Id, string Name, string? DisplayName, string? Bio, + string? AvatarUrl, IEnumerable Names, IEnumerable Pronouns); } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 0ac7f90..ca5fff4 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -1,24 +1,54 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; namespace Foxnouns.Backend.Services; -public class UserRendererService(DatabaseContext db, MemberRendererService memberRendererService) +public class UserRendererService(DatabaseContext db, MemberRendererService memberRendererService, Config config) { - public async Task RenderUserAsync(User user, User? selfUser = null, bool renderMembers = true) + public async Task RenderUserAsync(User user, User? selfUser = null, + Token? token = null, + bool renderMembers = true, + bool renderAuthMethods = false) { - renderMembers = renderMembers && (!user.ListHidden || selfUser?.Id == user.Id); + var isSelfUser = selfUser?.Id == user.Id; + var tokenCanReadHiddenMembers = token.HasScope("member.read"); + var tokenCanReadAuth = token.HasScope("user.read_privileged"); - var members = renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; + renderMembers = renderMembers && + (!user.ListHidden || (isSelfUser && tokenCanReadHiddenMembers)); + renderAuthMethods = renderAuthMethods && isSelfUser && tokenCanReadAuth; + + IEnumerable members = + renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; + // 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); + + var authMethods = renderAuthMethods + ? await db.AuthMethods + .Where(a => a.UserId == user.Id) + .Include(a => a.FediverseApplication) + .ToListAsync() + : []; return new UserResponse( - user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, user.Avatar, user.Links, user.Names, + user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, user.Pronouns, user.Fields, - renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null); + renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null, + renderAuthMethods + ? authMethods.Select(a => new AuthenticationMethodResponse( + a.Id, a.AuthType, a.RemoteId, + a.RemoteUsername, a.FediverseApplication?.Domain + )) + : null + ); } + private string? AvatarUrlFor(User user) => + user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; + public record UserResponse( Snowflake Id, string Username, @@ -30,7 +60,20 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe IEnumerable Names, IEnumerable Pronouns, IEnumerable Fields, - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - IEnumerable? Members + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + IEnumerable? Members, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + IEnumerable? AuthMethods + ); + + public record AuthenticationMethodResponse( + Snowflake Id, + [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] + AuthType Type, + string RemoteId, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? RemoteUsername, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? FediverseInstance ); } \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 45c9ad5..8dfb137 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -10,20 +10,20 @@ public static class AuthUtils 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_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]; + 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 readonly string[] ApplicationScopes = [.. Scopes, "user", "member"]; public static string[] ExpandScopes(this string[] scopes) { @@ -35,6 +35,9 @@ public static class AuthUtils 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(); diff --git a/Foxnouns.Backend/Utils/PatchRequest.cs b/Foxnouns.Backend/Utils/PatchRequest.cs new file mode 100644 index 0000000..da98615 --- /dev/null +++ b/Foxnouns.Backend/Utils/PatchRequest.cs @@ -0,0 +1,35 @@ +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. +/// +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) + { + var prop = base.CreateProperty(member, memberSerialization); + + prop.SetIsSpecified += (o, _) => + { + if (o is not PatchRequest patchRequest) return; + patchRequest.SetHasProperty(prop.UnderlyingName!); + }; + + return prop; + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs b/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs new file mode 100644 index 0000000..dafc2b8 --- /dev/null +++ b/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs @@ -0,0 +1,18 @@ +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); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs new file mode 100644 index 0000000..182f1bc --- /dev/null +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -0,0 +1,76 @@ +using System.Text.RegularExpressions; + +namespace Foxnouns.Backend.Utils; + +/// +/// Static methods for validating user input (mostly making sure it's not too short or too long) +/// +public static class ValidationUtils +{ + private static readonly Regex UsernameRegex = new("^[\\w-.]{2,40}$", RegexOptions.IgnoreCase); + + private static readonly string[] InvalidUsernames = + [ + "..", + "admin", + "administrator", + "mod", + "moderator", + "api", + "page", + "pronouns", + "settings", + "pronouns.cc", + "pronounscc" + ]; + + /// + /// Validates whether a username is valid. If it is not valid, throws . + /// This does not check if the username is already taken. + /// + public static void ValidateUsername(string username) + { + if (!UsernameRegex.IsMatch(username)) + throw username.Length switch + { + < 2 => new ApiError.BadRequest("Username is too short", "username"), + > 40 => new ApiError.BadRequest("Username is too long", "username"), + _ => new ApiError.BadRequest( + "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", + "username") + }; + + if (InvalidUsernames.Any(u => string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase))) + throw new ApiError.BadRequest("Username is not allowed", "username"); + } + + public static void ValidateDisplayName(string? displayName) + { + if (displayName == null) return; + switch (displayName.Length) + { + case 0: + throw new ApiError.BadRequest("Display name is too short", "display_name"); + case > 100: + throw new ApiError.BadRequest("Display name is too long", "display_name"); + } + } + + public static void ValidateBio(string? bio) + { + if (bio == null) return; + switch (bio.Length) + { + case 0: + throw new ApiError.BadRequest("Bio is too short", "bio"); + case > 1024: + throw new ApiError.BadRequest("Bio is too long", "bio"); + } + } + + public static void ValidateAvatar(string? avatar) + { + if (avatar == null) return; + if (avatar.Length > 1_500_000) throw new ApiError.BadRequest("Avatar is too big", "avatar"); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index 2586a62..c791d1f 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -4,6 +4,8 @@ Host = localhost Port = 5000 ; 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 diff --git a/SCOPES.md b/SCOPES.md new file mode 100644 index 0000000..5cab914 --- /dev/null +++ b/SCOPES.md @@ -0,0 +1,18 @@ +# List of API endpoints and scopes + +## Scopes + +- `identify`: `@me` will refer to token user (always granted) +- `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 + +## Users + +- GET `/users/{userRef}`: `identify` required to use `@me` as user reference. + `user.read_privileged` required to view authentication methods. + `member.read` required to view unlisted members. +- PATCH `/users/@me`: `user.update` required. \ No newline at end of file diff --git a/STYLE.md b/STYLE.md new file mode 100644 index 0000000..6acdab8 --- /dev/null +++ b/STYLE.md @@ -0,0 +1,12 @@ +# Code style + +## C# code style + +Code should be formatted with `dotnet format` or Rider's built-in formatter. +Variables should *always* be declared using `var`, unless the correct type +can't be inferred from the declaration (i.e. if the variable needs to be an +`IEnumerable` instead of a `List`, or if a variable is initialized as `null`). + +## TypeScript code style + +Use `prettier` for formatting the frontend code. \ No newline at end of file From fa49030b063046940939ff1dff7cc7051a6d4358 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 13 Jul 2024 03:09:00 +0200 Subject: [PATCH 013/261] feat: add deleted user columns in database --- ENDPOINTS.md | 45 ++ Foxnouns.Backend/Config.cs | 1 + .../Controllers/MetaController.cs | 19 +- Foxnouns.Backend/Database/DatabaseContext.cs | 9 +- .../Database/DatabaseQueryExtensions.cs | 6 + ...240712233806_AddUserLastActive.Designer.cs | 515 +++++++++++++++++ .../20240712233806_AddUserLastActive.cs | 30 + .../20240713000719_AddDeleted.Designer.cs | 528 ++++++++++++++++++ .../Migrations/20240713000719_AddDeleted.cs | 50 ++ .../DatabaseContextModelSnapshot.cs | 40 +- Foxnouns.Backend/Database/Models/User.cs | 10 +- .../Extensions/WebApplicationExtensions.cs | 6 +- Foxnouns.Backend/Services/AuthService.cs | 8 +- .../Services/UserRendererService.cs | 19 +- Foxnouns.Backend/Utils/AuthUtils.cs | 1 - Foxnouns.Backend/config.example.ini | 3 +- SCOPES.md | 18 - 17 files changed, 1254 insertions(+), 54 deletions(-) create mode 100644 ENDPOINTS.md create mode 100644 Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs delete mode 100644 SCOPES.md diff --git a/ENDPOINTS.md b/ENDPOINTS.md new file mode 100644 index 0000000..4f7fcf5 --- /dev/null +++ b/ENDPOINTS.md @@ -0,0 +1,45 @@ +# List of API endpoints and scopes + +## Scopes + +- `identify`: `@me` will refer to token user (always granted) +- `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 + +- [ ] GET `/meta`: gets stats and server information + +## Users + +- [ ] GET `/users/{userRef}`: views current user. + `identify` required to use `@me` as user reference. + `user.read_privileged` required to view authentication methods. + `member.read` required to view unlisted members. +- [ ] PATCH `/users/@me`: updates current user. `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 + +- [ ] 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. +- [ ] GET `/users/{userRef}/members/{memberRef}`: gets a single member. + will always return a member if it exists, even if the member is unlisted. +- [ ] POST `/users/@me/members`: creates a new member. `member.create` required +- [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required +- [ ] 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. +- \ No newline at end of file diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 1cb21c6..6766b3b 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -26,6 +26,7 @@ public class Config 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 class DatabaseConfig diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 9d2c991..5dded77 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -1,29 +1,38 @@ using Foxnouns.Backend.Database; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; +using NodaTime; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/meta")] -public class MetaController(DatabaseContext db) : ApiControllerBase +public class MetaController(DatabaseContext db, IClock clock) : ApiControllerBase { + private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; + [HttpGet] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MetaResponse))] public async Task GetMeta() { - var userCount = await db.Users.CountAsync(); + var now = clock.GetCurrentInstant(); + var users = await db.Users.Select(u => u.LastActive).ToListAsync(); var memberCount = await db.Members.CountAsync(); return Ok(new MetaResponse( - BuildInfo.Version, BuildInfo.Hash, memberCount, - new UserInfo(userCount, 0, 0, 0)) + Repository, BuildInfo.Version, BuildInfo.Hash, memberCount, + new UserInfo( + users.Count, + users.Count(i => i > now - Duration.FromDays(30)), + users.Count(i => i > now - Duration.FromDays(7)), + users.Count(i => i > now - Duration.FromDays(1)) + )) ); } [HttpGet("/api/v2/coffee")] public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); - private record MetaResponse(string Version, string Hash, int Members, UserInfo Users); + private record MetaResponse(string Repository, string Version, string Hash, int Members, UserInfo Users); private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 03e2c42..5c54ab5 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -10,6 +10,7 @@ namespace Foxnouns.Backend.Database; public class DatabaseContext : DbContext { private readonly NpgsqlDataSource _dataSource; + private readonly ILoggerFactory? _loggerFactory; public DbSet Users { get; set; } public DbSet Members { get; set; } @@ -19,7 +20,7 @@ public class DatabaseContext : DbContext public DbSet Applications { get; set; } public DbSet TemporaryKeys { get; set; } - public DatabaseContext(Config config) + public DatabaseContext(Config config, ILoggerFactory? loggerFactory) { var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { @@ -30,13 +31,15 @@ public class DatabaseContext : DbContext var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); dataSourceBuilder.UseNodaTime(); _dataSource = dataSourceBuilder.Build(); + _loggerFactory = loggerFactory; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) .UseNpgsql(_dataSource, o => o.UseNodaTime()) - .UseSnakeCaseNamingConvention(); + .UseSnakeCaseNamingConvention() + .UseLoggerFactory(_loggerFactory); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { @@ -73,6 +76,6 @@ public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory() ?? new(); - return new DatabaseContext(config); + return new DatabaseContext(config, null); } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 9b357fa..8262e5d 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -15,11 +15,13 @@ public static class DatabaseQueryExtensions if (Snowflake.TryParse(userRef, out var snowflake)) { user = await context.Users + .Where(u => !u.Deleted) .FirstOrDefaultAsync(u => u.Id == snowflake); if (user != null) return user; } user = await context.Users + .Where(u => !u.Deleted) .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); @@ -28,6 +30,7 @@ public static class DatabaseQueryExtensions public static async Task ResolveUserAsync(this DatabaseContext context, Snowflake id) { var user = await context.Users + .Where(u => !u.Deleted) .FirstOrDefaultAsync(u => u.Id == id); if (user != null) return user; throw new ApiError.NotFound("No user with that ID found.", code: ErrorCode.UserNotFound); @@ -37,6 +40,7 @@ public static class DatabaseQueryExtensions { var member = await context.Members .Include(m => m.User) + .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Id == id); if (member != null) return member; throw new ApiError.NotFound("No member with that ID found.", code: ErrorCode.MemberNotFound); @@ -56,12 +60,14 @@ public static class DatabaseQueryExtensions { member = await context.Members .Include(m => m.User) + .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId); if (member != null) return member; } member = await context.Members .Include(m => m.User) + .Where(m => !m.User.Deleted) .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); diff --git a/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs new file mode 100644 index 0000000..0430058 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs @@ -0,0 +1,515 @@ +// +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("20240712233806_AddUserLastActive")] + partial class AddUserLastActive + { + /// + 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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (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("LastActive") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_active"); + + 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/20240712233806_AddUserLastActive.cs b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs new file mode 100644 index 0000000..8d1392c --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + 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.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs new file mode 100644 index 0000000..8c2dcdc --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs @@ -0,0 +1,528 @@ +// +using System; +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("20240713000719_AddDeleted")] + partial class AddDeleted + { + /// + 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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (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("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("LastActive") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_active"); + + 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/20240713000719_AddDeleted.cs b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs new file mode 100644 index 0000000..a14ad5c --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs @@ -0,0 +1,50 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + 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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 360d43d..4440499 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -267,10 +267,26 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("bio"); + 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("LastActive") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_active"); + b.Property("Links") .IsRequired() .HasColumnType("text[]") @@ -335,7 +351,7 @@ namespace Foxnouns.Backend.Database.Migrations .IsRequired() .HasConstraintName("fk_members_users_user_id"); - b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Fields#System.Collections.Generic.List", "Fields", b1 => + b.OwnsOne("System.Collections.Generic.List", "Fields", b1 => { b1.Property("MemberId") .HasColumnType("bigint"); @@ -345,7 +361,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("MemberId"); - b1.ToTable("members", (string)null); + b1.ToTable("members"); b1.ToJson("fields"); @@ -354,7 +370,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_members_members_id"); }); - b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Names#System.Collections.Generic.List", "Names", b1 => + b.OwnsOne("System.Collections.Generic.List", "Names", b1 => { b1.Property("MemberId") .HasColumnType("bigint"); @@ -364,7 +380,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("MemberId"); - b1.ToTable("members", (string)null); + b1.ToTable("members"); b1.ToJson("names"); @@ -373,7 +389,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_members_members_id"); }); - b.OwnsOne("Foxnouns.Backend.Database.Models.Member.Pronouns#System.Collections.Generic.List", "Pronouns", b1 => + b.OwnsOne("System.Collections.Generic.List", "Pronouns", b1 => { b1.Property("MemberId") .HasColumnType("bigint"); @@ -383,7 +399,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("MemberId"); - b1.ToTable("members", (string)null); + b1.ToTable("members"); b1.ToJson("pronouns"); @@ -427,7 +443,7 @@ namespace Foxnouns.Backend.Database.Migrations modelBuilder.Entity("Foxnouns.Backend.Database.Models.User", b => { - b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Fields#List", "Fields", b1 => { b1.Property("UserId") .HasColumnType("bigint"); @@ -438,7 +454,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("UserId") .HasName("pk_users"); - b1.ToTable("users", (string)null); + b1.ToTable("users"); b1.ToJson("fields"); @@ -447,7 +463,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_users_users_user_id"); }); - b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Names#List", "Names", b1 => { b1.Property("UserId") .HasColumnType("bigint"); @@ -458,7 +474,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("UserId") .HasName("pk_users"); - b1.ToTable("users", (string)null); + b1.ToTable("users"); b1.ToJson("names"); @@ -467,7 +483,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasConstraintName("fk_users_users_user_id"); }); - b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => + b.OwnsOne("Foxnouns.Backend.Database.Models.User.Pronouns#List", "Pronouns", b1 => { b1.Property("UserId") .HasColumnType("bigint"); @@ -478,7 +494,7 @@ namespace Foxnouns.Backend.Database.Migrations b1.HasKey("UserId") .HasName("pk_users"); - b1.ToTable("users", (string)null); + b1.ToTable("users"); b1.ToJson("pronouns"); diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index 986c779..b0e060c 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -1,4 +1,5 @@ -using System.Text.RegularExpressions; +using System.ComponentModel.DataAnnotations.Schema; +using NodaTime; namespace Foxnouns.Backend.Database.Models; @@ -21,6 +22,13 @@ public class User : BaseModel public List Members { get; } = []; public List AuthMethods { get; } = []; + + public required Instant LastActive { get; set; } + + public bool Deleted { get; set; } + public Instant? DeletedAt { get; set; } + public Snowflake? DeletedBy { get; set; } + [NotMapped] public bool? SelfDelete => Deleted ? DeletedBy != null : null; } public enum UserRole diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 0bf334c..38279ac 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -24,6 +24,8 @@ public static class WebApplicationExtensions // 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.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) @@ -34,10 +36,8 @@ public static class WebApplicationExtensions logCfg.WriteTo.Seq(config.Logging.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); + builder.Services.AddSerilog().AddSingleton(Log.Logger = logCfg.CreateLogger()); return builder; } diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index a87a50c..b4a1adf 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -8,7 +8,7 @@ using NodaTime; namespace Foxnouns.Backend.Services; -public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) +public class AuthService(ILogger logger, IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) { private readonly PasswordHasher _passwordHasher = new(); @@ -26,7 +26,8 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator { new AuthMethod { Id = snowflakeGenerator.GenerateSnowflake(), AuthType = AuthType.Email, RemoteId = email } - } + }, + LastActive = clock.GetCurrentInstant() }; db.Add(user); @@ -59,7 +60,8 @@ public class AuthService(ILogger logger, DatabaseContext db, ISnowflakeGenerator Id = snowflakeGenerator.GenerateSnowflake(), AuthType = authType, RemoteId = remoteId, RemoteUsername = remoteUsername, FediverseApplication = instance } - } + }, + LastActive = clock.GetCurrentInstant() }; db.Add(user); diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index ca5fff4..2a3754a 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; +using NodaTime; namespace Foxnouns.Backend.Services; @@ -14,12 +15,12 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe bool renderAuthMethods = false) { var isSelfUser = selfUser?.Id == user.Id; - var tokenCanReadHiddenMembers = token.HasScope("member.read"); - var tokenCanReadAuth = token.HasScope("user.read_privileged"); + var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser; + var tokenPrivileged = token.HasScope("user.read_privileged") && isSelfUser; renderMembers = renderMembers && - (!user.ListHidden || (isSelfUser && tokenCanReadHiddenMembers)); - renderAuthMethods = renderAuthMethods && isSelfUser && tokenCanReadAuth; + (!user.ListHidden || tokenCanReadHiddenMembers); + renderAuthMethods = renderAuthMethods && tokenPrivileged; IEnumerable members = renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; @@ -34,7 +35,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe : []; return new UserResponse( - user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, + user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, + user.Names, user.Pronouns, user.Fields, renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null, renderAuthMethods @@ -42,7 +44,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe a.Id, a.AuthType, a.RemoteId, a.RemoteUsername, a.FediverseApplication?.Domain )) - : null + : null, + tokenPrivileged ? user.LastActive : null ); } @@ -63,7 +66,9 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - IEnumerable? AuthMethods + IEnumerable? AuthMethods, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + Instant? LastActive ); public record AuthenticationMethodResponse( diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 8dfb137..39d5870 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -75,7 +75,6 @@ public static class AuthUtils } catch (Exception e) { - Console.WriteLine($"Error converting string: {e}"); bytes = []; return false; } diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index c791d1f..c8d38ca 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -18,11 +18,12 @@ SentryUrl = https://examplePublicKey@o0.ingest.sentry.io/0 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 [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. diff --git a/SCOPES.md b/SCOPES.md deleted file mode 100644 index 5cab914..0000000 --- a/SCOPES.md +++ /dev/null @@ -1,18 +0,0 @@ -# List of API endpoints and scopes - -## Scopes - -- `identify`: `@me` will refer to token user (always granted) -- `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 - -## Users - -- GET `/users/{userRef}`: `identify` required to use `@me` as user reference. - `user.read_privileged` required to view authentication methods. - `member.read` required to view unlisted members. -- PATCH `/users/@me`: `user.update` required. \ No newline at end of file From 16f230b97d6ab6293d5cabfd5ae6ab04ab34b9c9 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 13 Jul 2024 17:23:52 +0200 Subject: [PATCH 014/261] feat(backend): start work on metrics --- Foxnouns.Backend/Config.cs | 2 ++ .../Extensions/WebApplicationExtensions.cs | 35 +++++++++++++++++++ Foxnouns.Backend/Foxnouns.Backend.csproj | 3 ++ Foxnouns.Backend/Metrics.cs | 6 ++++ Foxnouns.Backend/Program.cs | 9 ++--- Foxnouns.Backend/config.example.ini | 2 ++ 6 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 Foxnouns.Backend/Metrics.cs diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 6766b3b..62a107f 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -10,6 +10,7 @@ public class Config public string MediaBaseUrl { get; set; } = null!; public string Address => $"http://{Host}:{Port}"; + public string? MetricsAddress => Logging.MetricsPort != null ? $"http://{Host}:{Logging.MetricsPort}" : null; public LoggingConfig Logging { get; init; } = new(); public DatabaseConfig Database { get; init; } = new(); @@ -27,6 +28,7 @@ public class Config public bool SentryTracing { get; init; } = false; public double SentryTracesSampleRate { get; init; } = 0.0; public bool LogQueries { get; init; } = false; + public int? MetricsPort { get; init; } } public class DatabaseConfig diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 38279ac..bcd29f4 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -1,3 +1,6 @@ +using App.Metrics; +using App.Metrics.AspNetCore; +using App.Metrics.Formatters.Prometheus; using Foxnouns.Backend.Database; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; @@ -6,6 +9,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; using Serilog; using Serilog.Events; +using IClock = NodaTime.IClock; namespace Foxnouns.Backend.Extensions; @@ -29,6 +33,7 @@ public static class WebApplicationExtensions .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) + .MinimumLevel.Override("Hangfire", LogEventLevel.Information) .WriteTo.Console(); if (config.Logging.SeqLogUrl != null) @@ -52,6 +57,36 @@ public static class WebApplicationExtensions return config; } + public static WebApplicationBuilder AddMetrics(this WebApplicationBuilder builder) + { + var config = builder.Configuration.Get() ?? new(); + var metrics = AppMetrics.CreateDefaultBuilder() + .OutputMetrics.AsPrometheusPlainText() + .Build(); + + builder.Services.AddSingleton(metrics); + builder.Services.AddSingleton(metrics); + + builder.WebHost + .ConfigureMetrics(metrics) + .UseMetrics(opts => + { + opts.EndpointOptions = options => + { + // Metrics must listen on a separate port for security reasons. If no metrics port is set, disable the endpoint entirely. + options.MetricsEndpointEnabled = config.Logging.MetricsPort != null; + options.EnvironmentInfoEndpointEnabled = config.Logging.MetricsPort != null; + options.MetricsTextEndpointEnabled = false; + options.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters + .OfType().First(); + }; + }) + .UseMetricsWebTracking() + .ConfigureAppMetricsHostingConfiguration(opts => { opts.AllEndpointsPort = config.Logging.MetricsPort; }); + + return builder; + } + public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder) { var file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini"; diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index d554482..b43e2df 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -6,6 +6,9 @@ + + + diff --git a/Foxnouns.Backend/Metrics.cs b/Foxnouns.Backend/Metrics.cs new file mode 100644 index 0000000..a830ab8 --- /dev/null +++ b/Foxnouns.Backend/Metrics.cs @@ -0,0 +1,6 @@ +namespace Foxnouns.Backend; + +public static class Metrics +{ + +} \ No newline at end of file diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 4a311a7..7230598 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -21,7 +21,7 @@ var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration(); -builder.AddSerilog(); +builder.AddSerilog().AddMetrics(); builder.WebHost .UseSentry(opts => @@ -75,9 +75,9 @@ builder.Services .Build()); builder.Services.AddHangfire(c => c.UseSentry().UseRedisStorage(config.Jobs.Redis, new RedisStorageOptions -{ - Prefix = "foxnouns_" -})) + { + Prefix = "foxnouns_" + })) .AddHangfireServer(options => { options.WorkerCount = config.Jobs.Workers; }); var app = builder.Build(); @@ -103,6 +103,7 @@ app.UseHangfireDashboard("/hangfire", new DashboardOptions app.Urls.Clear(); app.Urls.Add(config.Address); +if (config.MetricsAddress != null) app.Urls.Add(config.MetricsAddress); // Fire off the periodic tasks loop in the background _ = new Timer(_ => diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index c8d38ca..e316c8e 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -20,6 +20,8 @@ SentryTracing = true SentryTracesSampleRate = 1.0 ; Whether to log SQL queries. Note that this is very verbose. Defaults to false. LogQueries = false +; The port the /metrics endpoint will listen on. If not set, metrics will be disabled. +MetricsPort = 5001 [Database] ; The database URL in ADO.NET format. From e7ec0e666186cbca2481443aa97ca6b1ecbac974 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 13 Jul 2024 19:38:40 +0200 Subject: [PATCH 015/261] feat(backend): add member GET endpoints, POST /users/@me/members endpoint --- ENDPOINTS.md | 14 ++-- .../Controllers/MembersController.cs | 68 +++++++++++++++++++ .../Controllers/MetaController.cs | 2 +- .../Controllers/UsersController.cs | 11 +-- .../Database/DatabaseQueryExtensions.cs | 38 ++--------- Foxnouns.Backend/Services/AuthService.cs | 4 +- Foxnouns.Backend/Services/KeyCacheService.cs | 2 +- .../Services/MemberRendererService.cs | 48 +++++++++++++ .../Services/UserRendererService.cs | 16 ++++- Foxnouns.Backend/Utils/AuthUtils.cs | 4 +- 10 files changed, 152 insertions(+), 55 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/MembersController.cs diff --git a/ENDPOINTS.md b/ENDPOINTS.md index 4f7fcf5..41ca62a 100644 --- a/ENDPOINTS.md +++ b/ENDPOINTS.md @@ -3,6 +3,8 @@ ## 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` @@ -12,15 +14,16 @@ ## Meta -- [ ] GET `/meta`: gets stats and server information +- [x] GET `/meta`: gets stats and server information ## Users -- [ ] GET `/users/{userRef}`: views current user. +- [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. -- [ ] PATCH `/users/@me`: updates current user. `user.update` required. +- [x] PATCH `/users/@me`: updates current user. `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 @@ -32,14 +35,13 @@ ## Members -- [ ] GET `/users/{userRef}/members`: gets list of a user's 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. -- [ ] GET `/users/{userRef}/members/{memberRef}`: gets a single member. +- [x] GET `/users/{userRef}/members/{memberRef}`: gets a single member. will always return a member if it exists, even if the member is unlisted. - [ ] POST `/users/@me/members`: creates a new member. `member.create` required - [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required - [ ] 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. -- \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs new file mode 100644 index 0000000..1e30549 --- /dev/null +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -0,0 +1,68 @@ +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; + +[Route("/api/v2/users/{userRef}/members")] +public class MembersController( + ILogger logger, + DatabaseContext db, + MemberRendererService memberRendererService, + ISnowflakeGenerator snowflakeGenerator) : ApiControllerBase +{ + private readonly ILogger _logger = logger.ForContext(); + + [HttpGet] + [ProducesResponseType>(StatusCodes.Status200OK)] + public async Task GetMembersAsync(string userRef) + { + var user = await db.ResolveUserAsync(userRef, CurrentToken); + return Ok(await memberRendererService.RenderUserMembersAsync(user, CurrentToken)); + } + + [HttpGet("{memberRef}")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task GetMemberAsync(string userRef, string memberRef) + { + var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken); + return Ok(memberRendererService.RenderMember(member, CurrentToken)); + } + + [HttpPost("/api/v2/users/@me/members")] + [ProducesResponseType(StatusCodes.Status200OK)] + [Authorize("member.create")] + public async Task CreateMemberAsync([FromBody] CreateMemberRequest req) + { + await using var tx = await db.Database.BeginTransactionAsync(); + + // "Translation of the 'string.Equals' overload with a 'StringComparison' parameter is not supported." + // Member names are case-insensitive, so we need to compare the lowercase forms of both. +#pragma warning disable CA1862 + if (await db.Members.AnyAsync(m => m.UserId == CurrentUser!.Id && m.Name.ToLower() == req.Name.ToLower())) +#pragma warning restore CA1862 + { + throw new ApiError.BadRequest("A member with that name already exists", "name"); + } + + var member = new Member + { + Id = snowflakeGenerator.GenerateSnowflake(), + Name = req.Name, + User = CurrentUser! + }; + db.Add(member); + + _logger.Debug("Creating member {MemberName} ({Id}) for {UserId}", member.Name, member.Id, CurrentUser!.Id); + + await db.SaveChangesAsync(); + await tx.CommitAsync(); + + return Ok(memberRendererService.RenderMember(member, CurrentToken)); + } + + public record CreateMemberRequest(string Name); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 5dded77..6f6b4e1 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -11,7 +11,7 @@ public class MetaController(DatabaseContext db, IClock clock) : ApiControllerBas private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(MetaResponse))] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task GetMeta() { var now = clock.GetCurrentInstant(); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 28d7ea8..e1b5702 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -16,16 +16,7 @@ public class UsersController(DatabaseContext db, UserRendererService userRendere [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetUserAsync(string userRef) { - var user = await db.ResolveUserAsync(userRef); - return await GetUserInnerAsync(user); - } - - [HttpGet("@me")] - [Authorize("identify")] - [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetMeAsync() - { - var user = await db.ResolveUserAsync(CurrentUser!.Id); + var user = await db.ResolveUserAsync(userRef, CurrentToken); return await GetUserInnerAsync(user); } diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 8262e5d..b8f10e9 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -9,8 +9,11 @@ namespace Foxnouns.Backend.Database; public static class DatabaseQueryExtensions { - public static async Task ResolveUserAsync(this DatabaseContext context, string userRef) + public static async Task ResolveUserAsync(this DatabaseContext context, string userRef, Token? token) { + if (userRef == "@me" && token != null) + return await context.Users.FirstAsync(u => u.Id == token.UserId); + User? user; if (Snowflake.TryParse(userRef, out var snowflake)) { @@ -46,9 +49,9 @@ public static class DatabaseQueryExtensions 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) + public static async Task ResolveMemberAsync(this DatabaseContext context, string userRef, string memberRef, Token? token) { - var user = await context.ResolveUserAsync(userRef); + var user = await context.ResolveUserAsync(userRef, token); return await context.ResolveMemberAsync(user.Id, memberRef); } @@ -92,33 +95,4 @@ public static class DatabaseQueryExtensions await context.SaveChangesAsync(); return app; } - - public static Task SetKeyAsync(this DatabaseContext context, string key, string value, Duration expireAfter) => - context.SetKeyAsync(key, value, SystemClock.Instance.GetCurrentInstant() + expireAfter); - - public static async Task SetKeyAsync(this DatabaseContext context, string key, string value, Instant expires) - { - context.TemporaryKeys.Add(new TemporaryKey - { - Expires = expires, - Key = key, - Value = value, - }); - await context.SaveChangesAsync(); - } - - public static async Task GetKeyAsync(this DatabaseContext context, string key, - bool delete = false) - { - var value = await context.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key); - if (value == null) return null; - - if (delete) - { - await context.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(); - await context.SaveChangesAsync(); - } - - return value.Value; - } } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index b4a1adf..5365480 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -8,7 +8,7 @@ using NodaTime; namespace Foxnouns.Backend.Services; -public class AuthService(ILogger logger, IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) +public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) { private readonly PasswordHasher _passwordHasher = new(); @@ -140,7 +140,7 @@ public class AuthService(ILogger logger, IClock clock, DatabaseContext db, ISnow private static (string, byte[]) GenerateToken() { - var token = AuthUtils.RandomToken(48); + var token = AuthUtils.RandomToken(); var hash = SHA512.HashData(Convert.FromBase64String(token)); return (token, hash); diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 4b0d4b3..4523d16 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -10,7 +10,7 @@ namespace Foxnouns.Backend.Services; public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) { public Task SetKeyAsync(string key, string value, Duration expireAfter) => - db.SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter); + SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter); public async Task SetKeyAsync(string key, string value, Instant expires) { diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index e151777..962712f 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -1,16 +1,50 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Utils; +using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; namespace Foxnouns.Backend.Services; public class MemberRendererService(DatabaseContext db, Config config) { + public async Task> RenderUserMembersAsync(User user, Token? token) + { + var canReadHiddenMembers = token != null && token.UserId == user.Id && token.HasScope("member.read"); + var canReadMemberList = !user.ListHidden || canReadHiddenMembers; + + 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(RenderPartialMember); + } + + public MemberResponse RenderMember(Member member, Token? token) + { + var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); + + return new MemberResponse( + member.Id, member.Name, member.DisplayName, member.Bio, + AvatarUrlFor(member), member.Links, member.Names, member.Pronouns, member.Fields, + RenderPartialUser(member.User), renderUnlisted ? member.Unlisted : null); + } + + private UserRendererService.PartialUser RenderPartialUser(User user) => + new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user)); + public PartialMember RenderPartialMember(Member member) => new(member.Id, member.Name, member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns); private 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; + public record PartialMember( Snowflake Id, string Name, @@ -19,4 +53,18 @@ public class MemberRendererService(DatabaseContext db, Config config) string? AvatarUrl, IEnumerable Names, IEnumerable Pronouns); + + public record MemberResponse( + Snowflake Id, + string Name, + string? DisplayName, + string? Bio, + string? AvatarUrl, + string[] Links, + IEnumerable Names, + IEnumerable Pronouns, + IEnumerable Fields, + UserRendererService.PartialUser User, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + bool? Unlisted); } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 2a3754a..c423e59 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -16,6 +16,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe { var isSelfUser = selfUser?.Id == user.Id; var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser; + var tokenHidden = token.HasScope("user.read_hidden") && isSelfUser; var tokenPrivileged = token.HasScope("user.read_privileged") && isSelfUser; renderMembers = renderMembers && @@ -45,10 +46,14 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe a.RemoteUsername, a.FediverseApplication?.Domain )) : null, - tokenPrivileged ? user.LastActive : null + tokenHidden ? user.ListHidden : null, + tokenHidden ? user.LastActive : null ); } + public PartialUser RenderPartialUser(User user) => + new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user)); + private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; @@ -68,6 +73,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? AuthMethods, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + bool? MemberListHidden, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive ); @@ -81,4 +88,11 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? FediverseInstance ); + + public record PartialUser( + Snowflake Id, + string Username, + string? DisplayName, + string? AvatarUrl + ); } \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 39d5870..badc19b 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -10,7 +10,7 @@ public static class AuthUtils private static readonly string[] ForbiddenSchemes = ["javascript", "file", "data", "mailto", "tel"]; public static readonly string[] UserScopes = - ["user.read_privileged", "user.update"]; + ["user.read_hidden", "user.read_privileged", "user.update"]; public static readonly string[] MemberScopes = ["member.read", "member.update", "member.create"]; @@ -73,7 +73,7 @@ public static class AuthUtils bytes = Convert.FromBase64String(b64); return true; } - catch (Exception e) + catch { bytes = []; return false; From fb34464199415c86048250694524b2ce3b9b1dc6 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 14 Jul 2024 16:44:41 +0200 Subject: [PATCH 016/261] feat(backend): improve bad request errors --- .../Authentication/DiscordAuthController.cs | 2 +- .../Controllers/MembersController.cs | 2 +- .../Controllers/UsersController.cs | 17 +++-- Foxnouns.Backend/ExpectedError.cs | 61 +++++++++++++++- Foxnouns.Backend/Services/AuthService.cs | 4 +- Foxnouns.Backend/Utils/ValidationUtils.cs | 73 +++++++++++-------- 6 files changed, 114 insertions(+), 45 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 35049ae..577231c 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -48,7 +48,7 @@ public class DiscordAuthController( public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req) { var remoteUser = await keyCacheSvc.GetKeyAsync($"discord:{req.Ticket}"); - if (remoteUser == null) throw new ApiError.BadRequest("Invalid ticket", "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", diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 1e30549..c189937 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -45,7 +45,7 @@ public class MembersController( if (await db.Members.AnyAsync(m => m.UserId == CurrentUser!.Id && m.Name.ToLower() == req.Name.ToLower())) #pragma warning restore CA1862 { - throw new ApiError.BadRequest("A member with that name already exists", "name"); + throw new ApiError.BadRequest("A member with that name already exists", "name", req.Name); } var member = new Member diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index e1b5702..ab9ad0e 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -38,30 +38,35 @@ public class UsersController(DatabaseContext db, UserRendererService userRendere { await using var tx = await db.Database.BeginTransactionAsync(); var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id); + var errors = new List<(string, ValidationError?)>(); if (req.Username != null && req.Username != user.Username) { - ValidationUtils.ValidateUsername(req.Username); + errors.Add(("username", ValidationUtils.ValidateUsername(req.Username))); user.Username = req.Username; } if (req.HasProperty(nameof(req.DisplayName))) { - ValidationUtils.ValidateDisplayName(req.DisplayName); + errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName))); user.DisplayName = req.DisplayName; } if (req.HasProperty(nameof(req.Bio))) { - ValidationUtils.ValidateBio(req.Bio); + errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio))); user.Bio = req.Bio; } if (req.HasProperty(nameof(req.Avatar))) - { - ValidationUtils.ValidateAvatar(req.Avatar); + errors.Add(("avatar", ValidationUtils.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))) AvatarUpdateJob.QueueUpdateUserAvatar(CurrentUser!.Id, req.Avatar); - } await db.SaveChangesAsync(); await tx.CommitAsync(); diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 7dda1ad..6e39af7 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using System.Net; using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Foxnouns.Backend; @@ -31,11 +32,12 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo public readonly string[] Scopes = scopes?.ToArray() ?? []; } - public class BadRequest(string message, IReadOnlyDictionary? errors = null) + public class BadRequest(string message, IReadOnlyDictionary>? errors = null) : ApiError(message, statusCode: HttpStatusCode.BadRequest) { - public BadRequest(string message, string field) : this(message, - new Dictionary { { field, message } }) + public BadRequest(string message, string field, object actualValue) : this("Error validating input", + new Dictionary> + { { field, [ValidationError.GenericValidationError(message, actualValue)] } }) { } @@ -55,7 +57,7 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo var errorObj = new JObject { { "key", error.Key }, - { "errors", new JArray(new JObject { { "message", error.Value } }) } + { "errors", JArray.FromObject(error.Value) } }; a.Add(errorObj); } @@ -116,4 +118,55 @@ public enum ErrorCode GenericApiError, UserNotFound, MemberNotFound, +} + +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) + { + return new ValidationError + { + Message = message, + MinLength = minLength, + MaxLength = maxLength, + ActualLength = actualLength + }; + } + + public static ValidationError DisallowedValueError(string message, IEnumerable allowedValues, + object actualValue) + { + return new ValidationError + { + Message = message, + AllowedValues = allowedValues, + ActualValue = actualValue + }; + } + + public static ValidationError GenericValidationError(string message, object? actualValue) + { + return new ValidationError + { + Message = message, + ActualValue = actualValue + }; + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index 5365480..c75f926 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -47,7 +47,7 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s AssertValidAuthType(authType, instance); if (await db.Users.AnyAsync(u => u.Username == username)) - throw new ApiError.BadRequest("Username is already taken", "username"); + throw new ApiError.BadRequest("Username is already taken", "username", username); var user = new User { @@ -124,7 +124,7 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s 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"); + throw new ApiError.BadRequest("Invalid scopes requested for this token", "scopes", scopes); var (token, hash) = GenerateToken(); return (token, new Token diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 182f1bc..a00d4d1 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -23,54 +23,65 @@ public static class ValidationUtils "pronouns.cc", "pronounscc" ]; - - /// - /// Validates whether a username is valid. If it is not valid, throws . - /// This does not check if the username is already taken. - /// - public static void ValidateUsername(string username) + + public static ValidationError? ValidateUsername(string username) { if (!UsernameRegex.IsMatch(username)) - throw username.Length switch + return username.Length switch { - < 2 => new ApiError.BadRequest("Username is too short", "username"), - > 40 => new ApiError.BadRequest("Username is too long", "username"), - _ => new ApiError.BadRequest( - "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", - "username") + < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), + > 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length), + _ => 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))) - throw new ApiError.BadRequest("Username is not allowed", "username"); + return ValidationError.GenericValidationError("Username is not allowed", username); + return null; } - public static void ValidateDisplayName(string? displayName) + public static void Validate(IEnumerable<(string, ValidationError?)> errors) { - if (displayName == null) return; - switch (displayName.Length) + errors = errors.Where(e => e.Item2 != null).ToList(); + if (!errors.Any()) return; + + var errorDict = new Dictionary>(); + foreach (var error in errors) { - case 0: - throw new ApiError.BadRequest("Display name is too short", "display_name"); - case > 100: - throw new ApiError.BadRequest("Display name is too long", "display_name"); + if (errorDict.TryGetValue(error.Item1, out var value)) errorDict[error.Item1] = value.Append(error.Item2!); + errorDict.Add(error.Item1, [error.Item2!]); } + + throw new ApiError.BadRequest("Error validating input", errorDict); } - public static void ValidateBio(string? bio) + public static ValidationError? ValidateDisplayName(string? displayName) { - if (bio == null) return; - switch (bio.Length) + return displayName?.Length switch { - case 0: - throw new ApiError.BadRequest("Bio is too short", "bio"); - case > 1024: - throw new ApiError.BadRequest("Bio is too long", "bio"); - } + 0 => ValidationError.LengthError("Display name is too short", 1, 100, displayName.Length), + > 100 => ValidationError.LengthError("Display name is too long", 1, 100, displayName.Length), + _ => null + }; } - public static void ValidateAvatar(string? avatar) + public static ValidationError? ValidateBio(string? bio) { - if (avatar == null) return; - if (avatar.Length > 1_500_000) throw new ApiError.BadRequest("Avatar is too big", "avatar"); + return bio?.Length switch + { + 0 => ValidationError.LengthError("Bio is too short", 1, 1024, bio.Length), + > 1024 => ValidationError.LengthError("Bio is too long", 1, 1024, bio.Length), + _ => null + }; + } + + public static ValidationError? ValidateAvatar(string? avatar) + { + return avatar?.Length switch + { + 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), + > 1_500_00 => ValidationError.GenericValidationError("Avatar is too large", null), + _ => null + }; } } \ No newline at end of file From a069d0ff1553b93e565204c3842ef600da8822e1 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 14 Jul 2024 21:25:23 +0200 Subject: [PATCH 017/261] feat(backend): add more params to POST /users/@me/members --- .../Controllers/MembersController.cs | 40 ++++++++++++------- Foxnouns.Backend/Database/DatabaseContext.cs | 8 +++- .../Extensions/WebApplicationExtensions.cs | 2 +- Foxnouns.Backend/Foxnouns.Backend.csproj | 1 + Foxnouns.Backend/Jobs/AvatarUpdateJob.cs | 10 +++-- Foxnouns.Backend/Utils/ValidationUtils.cs | 36 ++++++++++++++++- 6 files changed, 74 insertions(+), 23 deletions(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index c189937..3aa06a9 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -1,9 +1,11 @@ +using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; -using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; @@ -37,32 +39,40 @@ public class MembersController( [Authorize("member.create")] public async Task CreateMemberAsync([FromBody] CreateMemberRequest req) { - await using var tx = await db.Database.BeginTransactionAsync(); - - // "Translation of the 'string.Equals' overload with a 'StringComparison' parameter is not supported." - // Member names are case-insensitive, so we need to compare the lowercase forms of both. -#pragma warning disable CA1862 - if (await db.Members.AnyAsync(m => m.UserId == CurrentUser!.Id && m.Name.ToLower() == req.Name.ToLower())) -#pragma warning restore CA1862 - { - throw new ApiError.BadRequest("A member with that name already exists", "name", req.Name); - } + ValidationUtils.Validate([ + ("name", ValidationUtils.ValidateMemberName(req.Name)), + ("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)), + ("bio", ValidationUtils.ValidateBio(req.Bio)), + ("avatar", ValidationUtils.ValidateAvatar(req.Avatar)) + ]); var member = new Member { Id = snowflakeGenerator.GenerateSnowflake(), + User = CurrentUser!, Name = req.Name, - User = CurrentUser! + DisplayName = req.DisplayName, + Bio = req.Bio, + Unlisted = req.Unlisted ?? false }; db.Add(member); _logger.Debug("Creating member {MemberName} ({Id}) for {UserId}", member.Name, member.Id, CurrentUser!.Id); - await db.SaveChangesAsync(); - await tx.CommitAsync(); + try + { + await db.SaveChangesAsync(); + } + 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) AvatarUpdateJob.QueueUpdateMemberAvatar(member.Id, req.Avatar); return Ok(memberRendererService.RenderMember(member, CurrentToken)); } - public record CreateMemberRequest(string Name); + public record CreateMemberRequest(string Name, string? DisplayName, string? Bio, string? Avatar, bool? Unlisted); } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 5c54ab5..cd4ae8b 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -1,3 +1,4 @@ +using EntityFramework.Exceptions.PostgreSQL; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Microsoft.EntityFrameworkCore; @@ -36,10 +37,13 @@ public class DatabaseContext : DbContext protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder - .ConfigureWarnings(c => c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning)) + .ConfigureWarnings(c => + c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning) + .Ignore(CoreEventId.SaveChangesFailed)) .UseNpgsql(_dataSource, o => o.UseNodaTime()) .UseSnakeCaseNamingConvention() - .UseLoggerFactory(_loggerFactory); + .UseLoggerFactory(_loggerFactory) + .UseExceptionProcessor(); protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index bcd29f4..d1ee8fc 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -29,7 +29,7 @@ public static class WebApplicationExtensions // 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.Warning) + config.Logging.LogQueries ? LogEventLevel.Information : LogEventLevel.Fatal) .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index b43e2df..359d65c 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -10,6 +10,7 @@ + diff --git a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs index 6404591..139a384 100644 --- a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs +++ b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs @@ -25,9 +25,13 @@ public class AvatarUpdateJob(DatabaseContext db, IMinioClient minio, Config conf BackgroundJob.Enqueue(job => job.ClearUserAvatar(id)); } - public static void QueueUpdateMemberAvatar(Snowflake id, string? newAvatar) => - BackgroundJob.Enqueue(job => - newAvatar != null ? job.UpdateMemberAvatar(id, newAvatar) : job.ClearMemberAvatar(id)); + public static void QueueUpdateMemberAvatar(Snowflake id, string? newAvatar) + { + if (newAvatar != null) + BackgroundJob.Enqueue(job => job.UpdateMemberAvatar(id, newAvatar)); + else + BackgroundJob.Enqueue(job => job.ClearMemberAvatar(id)); + } public async Task UpdateUserAvatar(Snowflake id, string newAvatar) { diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index a00d4d1..64c0c53 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -9,6 +9,9 @@ public static class ValidationUtils { private static readonly Regex UsernameRegex = new("^[\\w-.]{2,40}$", RegexOptions.IgnoreCase); + private static readonly Regex MemberRegex = + new("^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,\\*]{1,100}$", RegexOptions.IgnoreCase); + private static readonly string[] InvalidUsernames = [ "..", @@ -23,7 +26,16 @@ public static class ValidationUtils "pronouns.cc", "pronounscc" ]; - + + private static readonly string[] InvalidMemberNames = + [ + // these break routing outright + ".", + "..", + // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible + "edit" + ]; + public static ValidationError? ValidateUsername(string username) { if (!UsernameRegex.IsMatch(username)) @@ -32,7 +44,8 @@ public static class ValidationUtils < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), > 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length), _ => ValidationError.GenericValidationError( - "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", username) + "Username is invalid, can only contain alphanumeric characters, dashes, underscores, and periods", + username) }; if (InvalidUsernames.Any(u => string.Equals(u, username, StringComparison.InvariantCultureIgnoreCase))) @@ -40,6 +53,25 @@ public static class ValidationUtils return null; } + public static ValidationError? ValidateMemberName(string memberName) + { + if (!UsernameRegex.IsMatch(memberName)) + return memberName.Length switch + { + < 2 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), + > 40 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), + _ => 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 static void Validate(IEnumerable<(string, ValidationError?)> errors) { errors = errors.Where(e => e.Item2 != null).ToList(); From 2b91723696e29533e1e8e7bad62da9619de6e5ad Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 14 Jul 2024 21:41:16 +0200 Subject: [PATCH 018/261] feat(backend): add member delete endpoint --- .../Controllers/MembersController.cs | 23 +++++++++++++++++- Foxnouns.Backend/Jobs/AvatarUpdateJob.cs | 24 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 3aa06a9..7aea73c 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -6,6 +6,7 @@ using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; @@ -14,7 +15,8 @@ public class MembersController( ILogger logger, DatabaseContext db, MemberRendererService memberRendererService, - ISnowflakeGenerator snowflakeGenerator) : ApiControllerBase + ISnowflakeGenerator snowflakeGenerator, + AvatarUpdateJob avatarUpdate) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); @@ -74,5 +76,24 @@ public class MembersController( return Ok(memberRendererService.RenderMember(member, CurrentToken)); } + [HttpDelete("/api/v2/users/@me/members/{memberRef}")] + [Authorize("member.update")] + public async Task DeleteMemberAsync(string memberRef) + { + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + var 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(); + } + + await db.SaveChangesAsync(); + + if (member.Avatar != null) await avatarUpdate.DeleteMemberAvatar(member.Id, member.Avatar); + return NoContent(); + } + public record CreateMemberRequest(string Name, string? DisplayName, string? Bio, string? Avatar, bool? Unlisted); } \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs index 139a384..bf3d35d 100644 --- a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs +++ b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs @@ -5,6 +5,7 @@ using Foxnouns.Backend.Utils; using Hangfire; using Minio; using Minio.DataModel.Args; +using Minio.Exceptions; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Processing; @@ -163,6 +164,29 @@ public class AvatarUpdateJob(DatabaseContext db, IMinioClient minio, Config conf await db.SaveChangesAsync(); } + /// + /// Deletes a member's avatar. This should only be used when a member is in the process of being deleted, otherwise, + /// with a null avatar should be used instead. + /// + public async Task DeleteMemberAvatar(Snowflake id, string hash) => await DeleteAvatar(MemberAvatarPath(id, hash)); + /// + /// Deletes a user's avatar. This should only be used when a user is in the process of being deleted, otherwise, + /// with a null avatar should be used instead. + /// + public async Task DeleteUserAvatar(Snowflake id, string hash) => await DeleteAvatar(UserAvatarPath(id, hash)); + + private async Task DeleteAvatar(string path) + { + logger.Debug("Deleting avatar at path {Path}", path); + try + { + await minio.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path)); + } + catch (InvalidObjectNameException) + { + } + } + private async Task ConvertAvatar(string uri) { if (!uri.StartsWith("data:image/")) From c4e39d4d59c903372637e87e8ab41869b0615134 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 25 Jul 2024 22:52:15 +0200 Subject: [PATCH 019/261] chore: update dependencies --- Foxnouns.Backend/Foxnouns.Backend.csproj | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 359d65c..45f5cd0 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -14,26 +14,26 @@ - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - - - + + + - - - - + + + + From ef221b2c456208bf4b0ffe451e2bb767def44731 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 22 Aug 2024 15:13:46 +0200 Subject: [PATCH 020/261] feat: update custom preferences endpoint --- ENDPOINTS.md | 9 +- .../Controllers/MembersController.cs | 17 +- .../Controllers/UsersController.cs | 74 ++- Foxnouns.Backend/Database/DatabaseContext.cs | 18 +- ...821210355_AddCustomPreferences.Designer.cs | 535 ++++++++++++++++++ .../20240821210355_AddCustomPreferences.cs | 32 ++ .../DatabaseContextModelSnapshot.cs | 9 +- Foxnouns.Backend/Database/Models/User.cs | 22 + Foxnouns.Backend/Database/Snowflake.cs | 25 +- Foxnouns.Backend/Foxnouns.Backend.csproj | 1 + .../Middleware/AuthorizationMiddleware.cs | 5 + .../Services/UserRendererService.cs | 4 +- Foxnouns.Backend/Utils/ValidationUtils.cs | 89 ++- 13 files changed, 820 insertions(+), 20 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs diff --git a/ENDPOINTS.md b/ENDPOINTS.md index 41ca62a..ddadf1e 100644 --- a/ENDPOINTS.md +++ b/ENDPOINTS.md @@ -23,7 +23,8 @@ `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`: 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 @@ -36,12 +37,12 @@ ## Members - [x] GET `/users/{userRef}/members`: gets list of a user's members. - if the user's member list is hidden, + 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. -- [ ] POST `/users/@me/members`: creates a new member. `member.create` required +- [x] POST `/users/@me/members`: creates a new member. `member.create` required - [ ] PATCH `/users/@me/members/{memberRef}`: edits a member. `member.update` required -- [ ] DELETE `/users/@me/members/{memberRef}`: deletes 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/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 7aea73c..ec28ee5 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -45,7 +45,9 @@ public class MembersController( ("name", ValidationUtils.ValidateMemberName(req.Name)), ("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)), ("bio", ValidationUtils.ValidateBio(req.Bio)), - ("avatar", ValidationUtils.ValidateAvatar(req.Avatar)) + ("avatar", ValidationUtils.ValidateAvatar(req.Avatar)), + ..ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), + ..ValidationUtils.ValidateFieldEntries(req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names") ]); var member = new Member @@ -55,6 +57,9 @@ public class MembersController( Name = req.Name, DisplayName = req.DisplayName, Bio = req.Bio, + Fields = req.Fields ?? [], + Names = req.Names ?? [], + Pronouns = req.Pronouns ?? [], Unlisted = req.Unlisted ?? false }; db.Add(member); @@ -95,5 +100,13 @@ public class MembersController( return NoContent(); } - public record CreateMemberRequest(string Name, string? DisplayName, string? Bio, string? Avatar, bool? Unlisted); + public record CreateMemberRequest( + string Name, + string? DisplayName, + string? Bio, + string? Avatar, + bool? Unlisted, + List? Names, + List? Pronouns, + List? Fields); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index ab9ad0e..19bb88a 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Jobs; @@ -10,7 +11,10 @@ using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users")] -public class UsersController(DatabaseContext db, UserRendererService userRendererService) : ApiControllerBase +public class UsersController( + DatabaseContext db, + UserRendererService userRendererService, + ISnowflakeGenerator snowflakeGenerator) : ApiControllerBase { [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] @@ -74,6 +78,74 @@ public class UsersController(DatabaseContext db, UserRendererService userRendere renderAuthMethods: false)); } + [HttpPatch("@me/custom-preferences")] + [Authorize("user.update")] + [ProducesResponseType>(StatusCodes.Status200OK)] + public async Task UpdateCustomPreferencesAsync([FromBody] List req) + { + ValidationUtils.Validate(ValidateCustomPreferences(req)); + + var user = await db.ResolveUserAsync(CurrentUser!.Id); + var preferences = user.CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)).ToDictionary(); + + foreach (var 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 + }; + } + else + { + preferences[snowflakeGenerator.GenerateSnowflake()] = new User.CustomPreference + { + Favourite = r.Favourite, + Icon = r.Icon, + Muted = r.Muted, + Size = r.Size, + Tooltip = r.Tooltip + }; + } + } + + user.CustomPreferences = preferences; + await db.SaveChangesAsync(); + + return Ok(user.CustomPreferences); + } + + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] + public class CustomPreferencesUpdateRequest + { + 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; } + } + + private static List<(string, ValidationError?)> ValidateCustomPreferences( + List preferences) + { + var errors = new List<(string, ValidationError?)>(); + + if (preferences.Count > 25) + errors.Add(("custom_preferences", + ValidationError.LengthError("Too many custom preferences", 0, 25, preferences.Count))); + if (preferences.Count > 50) return errors; + + // TODO: validate individual preferences + + return errors; + } + public class UpdateUserRequest : PatchRequest { public string? Username { get; init; } diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index cd4ae8b..725afb6 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using EntityFramework.Exceptions.PostgreSQL; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; @@ -31,6 +32,7 @@ public class DatabaseContext : DbContext var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); dataSourceBuilder.UseNodaTime(); + dataSourceBuilder.UseJsonNet(); _dataSource = dataSourceBuilder.Build(); _loggerFactory = loggerFactory; } @@ -57,18 +59,18 @@ public class DatabaseContext : DbContext modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); modelBuilder.Entity().HasIndex(k => k.Key).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().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() - .OwnsOne(m => m.Fields, f => f.ToJson()) - .OwnsOne(m => m.Names, n => n.ToJson()) - .OwnsOne(m => m.Pronouns, p => p.ToJson()); + modelBuilder.Entity().Property(m => m.Fields).HasColumnType("jsonb"); + modelBuilder.Entity().Property(m => m.Names).HasColumnType("jsonb"); + modelBuilder.Entity().Property(m => m.Pronouns).HasColumnType("jsonb"); } } +[SuppressMessage("ReSharper", "UnusedType.Global", Justification = "Used by EF Core's migration generator")] public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory { public DatabaseContext CreateDbContext(string[] args) diff --git a/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs new file mode 100644 index 0000000..c08b9fb --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs @@ -0,0 +1,535 @@ +// +using System; +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("20240821210355_AddCustomPreferences")] + partial class AddCustomPreferences + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (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>("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("LastActive") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_active"); + + 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/20240821210355_AddCustomPreferences.cs b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs new file mode 100644 index 0000000..7d68ad7 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + 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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 4440499..fc99285 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -1,6 +1,8 @@ // using System; +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; @@ -18,7 +20,7 @@ namespace Foxnouns.Backend.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.5") + .HasAnnotation("ProductVersion", "8.0.7") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -267,6 +269,11 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("bio"); + b.Property>("CustomPreferences") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("custom_preferences"); + b.Property("Deleted") .HasColumnType("boolean") .HasColumnName("deleted"); diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index b0e060c..305bd46 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations.Schema; +using Foxnouns.Backend.Utils; +using Newtonsoft.Json; using NodaTime; namespace Foxnouns.Backend.Database.Models; @@ -16,6 +18,7 @@ public class User : BaseModel public List Names { get; set; } = []; public List Pronouns { get; set; } = []; public List Fields { get; set; } = []; + public Dictionary CustomPreferences { 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 @@ -29,6 +32,18 @@ public class User : BaseModel public Instant? DeletedAt { get; set; } public Snowflake? DeletedBy { get; set; } [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 enum UserRole @@ -36,4 +51,11 @@ public enum UserRole User, Moderator, Admin, +} + +public enum PreferenceSize +{ + Large, + Normal, + Small, } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index 04937da..78efee6 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -1,4 +1,6 @@ +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; using NodaTime; @@ -6,7 +8,8 @@ using NodaTime; namespace Foxnouns.Backend.Database; [JsonConverter(typeof(JsonConverter))] -public readonly struct Snowflake(ulong value) +[TypeConverter(typeof(TypeConverter))] +public readonly struct Snowflake(ulong value) : IEquatable { public const long Epoch = 1_640_995_200_000; // 2022-01-01 at 00:00:00 UTC public readonly ulong Value = value; @@ -55,6 +58,12 @@ public readonly struct Snowflake(ulong value) } public override bool Equals(object? obj) => obj is Snowflake other && Value == other.Value; + + public bool Equals(Snowflake other) + { + return Value == other.Value; + } + public override int GetHashCode() => Value.GetHashCode(); public override string ToString() => Value.ToString(); @@ -81,4 +90,18 @@ public readonly struct Snowflake(ulong value) 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) + { + return TryParse((string)value, out var snowflake) ? snowflake : null; + } + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 45f5cd0..29c91d7 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -26,6 +26,7 @@ + diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 570d917..dd0d97f 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -1,3 +1,4 @@ +using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; namespace Foxnouns.Backend.Middleware; @@ -21,6 +22,10 @@ public class AuthorizationMiddleware : IMiddleware 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())); + 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 != UserRole.Admin && token.User.Role != UserRole.Moderator) + throw new ApiError.Forbidden("This endpoint can only be used by moderators."); await next(ctx); } diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index c423e59..9bb198e 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -37,8 +37,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe return new UserResponse( user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, - user.Names, - user.Pronouns, user.Fields, + user.Names, user.Pronouns, user.Fields, user.CustomPreferences, renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null, renderAuthMethods ? authMethods.Select(a => new AuthenticationMethodResponse( @@ -68,6 +67,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe IEnumerable Names, IEnumerable Pronouns, IEnumerable Fields, + Dictionary CustomPreferences, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 64c0c53..276395f 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -1,4 +1,6 @@ using System.Text.RegularExpressions; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; namespace Foxnouns.Backend.Utils; @@ -112,8 +114,93 @@ public static class ValidationUtils return avatar?.Length switch { 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), - > 1_500_00 => ValidationError.GenericValidationError("Avatar is too large", null), + > 1_500_000 => ValidationError.GenericValidationError("Avatar is too large", null), _ => null }; } + + private const int FieldLimit = 25; + private const int FieldNameLimit = 100; + private const int FieldEntryTextLimit = 100; + private const int FieldEntriesLimit = 100; + + private static readonly string[] DefaultStatusOptions = + [ + "favourite", + "okay", + "jokingly", + "friends_only", + "avoid" + ]; + + public static IEnumerable<(string, ValidationError?)> ValidateFields(List? fields, + IReadOnlyDictionary customPreferences) + { + if (fields == null) return []; + + var errors = new List<(string, ValidationError?)>(); + if (fields.Count > 25) + errors.Add(("fields", ValidationError.LengthError("Too many fields", 0, FieldLimit, fields.Count))); + // No overwhelming this function, thank you + if (fields.Count > 100) return errors; + + foreach (var (field, index) in fields.Select((field, index) => (field, index))) + { + switch (field.Name.Length) + { + case > FieldNameLimit: + errors.Add(($"fields.{index}.name", + ValidationError.LengthError("Field name is too long", 1, FieldNameLimit, field.Name.Length))); + break; + case < 1: + errors.Add(($"fields.{index}.name", + ValidationError.LengthError("Field name is too short", 1, FieldNameLimit, field.Name.Length))); + break; + } + + errors = errors.Concat(ValidateFieldEntries(field.Entries, customPreferences, $"fields.{index}")).ToList(); + } + + return errors; + } + + public static 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 > FieldEntriesLimit) + errors.Add(($"{errorPrefix}.entries", + ValidationError.LengthError("Field has too many entries", 0, FieldEntriesLimit, + entries.Length))); + + // Same as above, no overwhelming this function with a ridiculous amount of entries + if (entries.Length > FieldEntriesLimit + 50) return errors; + + foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) + { + switch (entry.Value.Length) + { + case > FieldEntryTextLimit: + errors.Add(($"{errorPrefix}.entries.{entryIdx}.value", + ValidationError.LengthError("Field value is too long", 1, FieldEntryTextLimit, + entry.Value.Length))); + break; + case < 1: + errors.Add(($"{errorPrefix}.entries.{entryIdx}.value", + ValidationError.LengthError("Field value is too short", 1, FieldEntryTextLimit, + entry.Value.Length))); + break; + } + + var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; + + if (!DefaultStatusOptions.Contains(entry.Status) && !customPreferenceIds.Contains(entry.Status)) + errors.Add(($"{errorPrefix}.entries.{entryIdx}.status", + ValidationError.GenericValidationError("Invalid status", entry.Status))); + } + + return errors; + } } \ No newline at end of file From 2915893049bfcc4a11a90037019269179c03edab Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 22 Aug 2024 17:27:04 +0200 Subject: [PATCH 021/261] start user pages --- Foxnouns.Backend/Controllers/UsersController.cs | 5 ----- Foxnouns.Frontend/src/routes/+error.svelte | 2 +- Foxnouns.Frontend/src/routes/+layout.server.ts | 2 +- Foxnouns.Frontend/src/routes/+layout.svelte | 2 +- Foxnouns.Frontend/src/routes/+page.svelte | 8 ++++---- .../src/routes/@[username]/+page.server.ts | 12 ++++++++++++ .../src/routes/@[username]/+page.svelte | 10 ++++++++++ 7 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 Foxnouns.Frontend/src/routes/@[username]/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/@[username]/+page.svelte diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 19bb88a..d9d8ac0 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -21,11 +21,6 @@ public class UsersController( public async Task GetUserAsync(string userRef) { var user = await db.ResolveUserAsync(userRef, CurrentToken); - return await GetUserInnerAsync(user); - } - - private async Task GetUserInnerAsync(User user) - { return Ok(await userRendererService.RenderUserAsync( user, selfUser: CurrentUser, diff --git a/Foxnouns.Frontend/src/routes/+error.svelte b/Foxnouns.Frontend/src/routes/+error.svelte index 9aec269..dd827ae 100644 --- a/Foxnouns.Frontend/src/routes/+error.svelte +++ b/Foxnouns.Frontend/src/routes/+error.svelte @@ -1,4 +1,4 @@ - diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts index 78f6517..a3b3a8a 100644 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -9,5 +9,5 @@ export async function load({ fetch, locals }) { user = await request(fetch, "GET", "/users/@me"); } catch {} - return { meta, user, token: locals.token }; + return { meta, currentUser: user, token: locals.token }; } diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte index 608c9f3..57271d1 100644 --- a/Foxnouns.Frontend/src/routes/+layout.svelte +++ b/Foxnouns.Frontend/src/routes/+layout.svelte @@ -6,5 +6,5 @@ export let data: LayoutData; - + diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte index 2971086..fa8e254 100644 --- a/Foxnouns.Frontend/src/routes/+page.svelte +++ b/Foxnouns.Frontend/src/routes/+page.svelte @@ -11,10 +11,10 @@

Visit kit.svelte.dev to read the documentation

- are you logged in? {data.user !== undefined} - {#if data.user} -
hello, {data.user.username}! -
your ID: {data.user.id} + are you logged in? {data.currentUser !== undefined} + {#if data.currentUser} +
hello, {data.currentUser.username}! +
your ID: {data.currentUser.id} {/if}

diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts new file mode 100644 index 0000000..a803a26 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts @@ -0,0 +1,12 @@ +import { error } from "@sveltejs/kit"; +import type { User } from "$lib/api/user"; +import request from "$lib/request"; + +export const load = async ({ params, fetch }) => { + try { + const user = await request(fetch, "GET", `/users/${params.username}`); + return { user }; + } catch { + error(404, { message: "User not found" }); + } +}; diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte new file mode 100644 index 0000000..639bb52 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte @@ -0,0 +1,10 @@ + + + + @{data.user.username} • pronouns.cc + + +

this is the user page for @{data.user.username}

From 0aadc5fb47f06f3cf2a80b45e0b70efc201e5b8e Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 3 Sep 2024 16:29:51 +0200 Subject: [PATCH 022/261] feat: replace Hangfire with Coravel --- .gitignore | 1 + Foxnouns.Backend/Config.cs | 7 - .../Authentication/AuthController.cs | 2 +- .../Controllers/MembersController.cs | 11 +- .../Controllers/UsersController.cs | 7 +- .../Extensions/AvatarObjectExtensions.cs | 51 ++++ .../Extensions/WebApplicationExtensions.cs | 11 +- Foxnouns.Backend/Foxnouns.Backend.csproj | 5 +- Foxnouns.Backend/Jobs/AvatarUpdateJob.cs | 220 ------------------ .../Jobs/MemberAvatarUpdateInvocable.cs | 79 +++++++ Foxnouns.Backend/Jobs/Payloads.cs | 5 + .../Jobs/UserAvatarUpdateInvocable.cs | 79 +++++++ .../Middleware/AuthenticationMiddleware.cs | 32 +-- Foxnouns.Backend/Program.cs | 18 +- .../Services/ObjectStorageService.cs | 33 +++ Foxnouns.Backend/Utils/Limits.cs | 9 + Foxnouns.Backend/Utils/ValidationUtils.cs | 31 ++- Foxnouns.Backend/appSettings.json | 7 + Foxnouns.Backend/config.example.ini | 6 - 19 files changed, 305 insertions(+), 309 deletions(-) create mode 100644 Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs delete mode 100644 Foxnouns.Backend/Jobs/AvatarUpdateJob.cs create mode 100644 Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs create mode 100644 Foxnouns.Backend/Jobs/Payloads.cs create mode 100644 Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs create mode 100644 Foxnouns.Backend/Services/ObjectStorageService.cs create mode 100644 Foxnouns.Backend/Utils/Limits.cs create mode 100644 Foxnouns.Backend/appSettings.json diff --git a/.gitignore b/.gitignore index 56d5d08..8e84a9d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ bin/ obj/ .version config.ini +*.DotSettings.user diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 62a107f..140214d 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -14,7 +14,6 @@ public class Config public LoggingConfig Logging { get; init; } = new(); public DatabaseConfig Database { get; init; } = new(); - public JobsConfig Jobs { get; init; } = new(); public StorageConfig Storage { get; init; } = new(); public DiscordAuthConfig DiscordAuth { get; init; } = new(); public GoogleAuthConfig GoogleAuth { get; init; } = new(); @@ -38,12 +37,6 @@ public class Config public int? MaxPoolSize { get; init; } } - public class JobsConfig - { - public string Redis { get; init; } = string.Empty; - public int Workers { get; init; } = 5; - } - public class StorageConfig { public string Endpoint { get; init; } = string.Empty; diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index d944dd8..6565fba 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -19,7 +19,7 @@ public class AuthController(Config config, KeyCacheService keyCacheSvc, ILogger config.TumblrAuth.Enabled); var state = HttpUtility.UrlEncode(await keyCacheSvc.GenerateAuthStateAsync()); string? discord = null; - if (config.DiscordAuth.ClientId != null && config.DiscordAuth.ClientSecret != 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" + diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index ec28ee5..19a9569 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -1,6 +1,8 @@ +using Coravel.Queuing.Interfaces; using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; @@ -16,7 +18,8 @@ public class MembersController( DatabaseContext db, MemberRendererService memberRendererService, ISnowflakeGenerator snowflakeGenerator, - AvatarUpdateJob avatarUpdate) : ApiControllerBase + ObjectStorageService objectStorage, + IQueue queue) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); @@ -76,7 +79,9 @@ public class MembersController( throw new ApiError.BadRequest("A member with that name already exists", "name", req.Name); } - if (req.Avatar != null) AvatarUpdateJob.QueueUpdateMemberAvatar(member.Id, req.Avatar); + if (req.Avatar != null) + queue.QueueInvocableWithPayload( + new AvatarUpdatePayload(member.Id, req.Avatar)); return Ok(memberRendererService.RenderMember(member, CurrentToken)); } @@ -96,7 +101,7 @@ public class MembersController( await db.SaveChangesAsync(); - if (member.Avatar != null) await avatarUpdate.DeleteMemberAvatar(member.Id, member.Avatar); + if (member.Avatar != null) await objectStorage.DeleteMemberAvatarAsync(member.Id, member.Avatar); return NoContent(); } diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 19bb88a..0b8f2f2 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Jobs; @@ -14,7 +15,8 @@ namespace Foxnouns.Backend.Controllers; public class UsersController( DatabaseContext db, UserRendererService userRendererService, - ISnowflakeGenerator snowflakeGenerator) : ApiControllerBase + ISnowflakeGenerator snowflakeGenerator, + IQueue queue) : ApiControllerBase { [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] @@ -70,7 +72,8 @@ public class UsersController( // (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))) - AvatarUpdateJob.QueueUpdateUserAvatar(CurrentUser!.Id, req.Avatar); + queue.QueueInvocableWithPayload( + new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); await db.SaveChangesAsync(); await tx.CommitAsync(); diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs new file mode 100644 index 0000000..f7c2b6f --- /dev/null +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -0,0 +1,51 @@ +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 AvatarObjectExtensions +{ + private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"]; + + public static async Task + DeleteMemberAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash) => + await objectStorage.RemoveObjectAsync(MemberAvatarUpdateInvocable.Path(id, hash)); + + public static async Task + DeleteUserAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash) => + await objectStorage.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash)); + + public static async Task ConvertBase64UriToAvatar(this string uri) + { + if (!uri.StartsWith("data:image/")) + throw new ArgumentException("Not a data URI", nameof(uri)); + + var split = uri.Remove(0, "data:".Length).Split(";base64,"); + var contentType = split[0]; + var encoded = split[1]; + if (!ValidContentTypes.Contains(contentType)) + throw new ArgumentException("Invalid content type for image", nameof(uri)); + + if (!AuthUtils.TryFromBase64String(encoded, out var rawImage)) + throw new ArgumentException("Invalid base64 string", nameof(uri)); + + var image = Image.Load(rawImage); + + var processor = new ResizeProcessor( + new ResizeOptions { Size = new Size(512), Mode = ResizeMode.Crop, 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 }); + return stream; + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index d1ee8fc..1915eae 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -1,6 +1,8 @@ using App.Metrics; using App.Metrics.AspNetCore; using App.Metrics.Formatters.Prometheus; +using Coravel; +using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; @@ -93,6 +95,7 @@ public static class WebApplicationExtensions return builder .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appSettings.json", true) .AddIniFile(file, optional: false, reloadOnChange: true) .AddEnvironmentVariables(); } @@ -105,8 +108,10 @@ public static class WebApplicationExtensions .AddScoped() .AddScoped() .AddScoped() - // Background job classes - .AddTransient(); + .AddScoped() + // Transient jobs + .AddTransient() + .AddTransient(); public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services .AddScoped() @@ -122,6 +127,8 @@ public static class WebApplicationExtensions { await BuildInfo.ReadBuildInfo(); + app.Services.ConfigureQueue().LogQueuedTaskProgress(app.Services.GetRequiredService>()); + await using var scope = app.Services.CreateAsyncScope(); var logger = scope.ServiceProvider.GetRequiredService().ForContext(); var db = scope.ServiceProvider.GetRequiredService(); diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 29c91d7..711e620 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -9,11 +9,9 @@ + - - - @@ -28,7 +26,6 @@ - diff --git a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs b/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs deleted file mode 100644 index bf3d35d..0000000 --- a/Foxnouns.Backend/Jobs/AvatarUpdateJob.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Utils; -using Hangfire; -using Minio; -using Minio.DataModel.Args; -using Minio.Exceptions; -using SixLabors.ImageSharp; -using SixLabors.ImageSharp.Formats.Webp; -using SixLabors.ImageSharp.Processing; -using SixLabors.ImageSharp.Processing.Processors.Transforms; - -namespace Foxnouns.Backend.Jobs; - -[SuppressMessage("ReSharper", "MemberCanBePrivate.Global", Justification = "Hangfire jobs need to be public")] -public class AvatarUpdateJob(DatabaseContext db, IMinioClient minio, Config config, ILogger logger) -{ - private readonly string[] _validContentTypes = ["image/png", "image/webp", "image/jpeg"]; - - public static void QueueUpdateUserAvatar(Snowflake id, string? newAvatar) - { - if (newAvatar != null) - BackgroundJob.Enqueue(job => job.UpdateUserAvatar(id, newAvatar)); - else - BackgroundJob.Enqueue(job => job.ClearUserAvatar(id)); - } - - public static void QueueUpdateMemberAvatar(Snowflake id, string? newAvatar) - { - if (newAvatar != null) - BackgroundJob.Enqueue(job => job.UpdateMemberAvatar(id, newAvatar)); - else - BackgroundJob.Enqueue(job => job.ClearMemberAvatar(id)); - } - - public async Task UpdateUserAvatar(Snowflake id, string newAvatar) - { - var 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 - { - var image = await ConvertAvatar(newAvatar); - var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); - image.Seek(0, SeekOrigin.Begin); - var prevHash = user.Avatar; - - await minio.PutObjectAsync(new PutObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(UserAvatarPath(id, hash)) - .WithObjectSize(image.Length) - .WithStreamData(image) - .WithContentType("image/webp") - ); - - user.Avatar = hash; - await db.SaveChangesAsync(); - - if (prevHash != null && prevHash != hash) - await minio.RemoveObjectAsync(new RemoveObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(UserAvatarPath(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); - } - } - - public async Task ClearUserAvatar(Snowflake id) - { - var 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 minio.RemoveObjectAsync(new RemoveObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(UserAvatarPath(user.Id, user.Avatar)) - ); - - user.Avatar = null; - await db.SaveChangesAsync(); - } - - public async Task UpdateMemberAvatar(Snowflake id, string newAvatar) - { - var 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 - { - var image = await ConvertAvatar(newAvatar); - var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); - image.Seek(0, SeekOrigin.Begin); - var prevHash = member.Avatar; - - await minio.PutObjectAsync(new PutObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(MemberAvatarPath(id, hash)) - .WithObjectSize(image.Length) - .WithStreamData(image) - .WithContentType("image/webp") - ); - - member.Avatar = hash; - await db.SaveChangesAsync(); - - if (prevHash != null && prevHash != hash) - await minio.RemoveObjectAsync(new RemoveObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(MemberAvatarPath(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); - } - } - - public async Task ClearMemberAvatar(Snowflake id) - { - var 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 minio.RemoveObjectAsync(new RemoveObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(MemberAvatarPath(member.Id, member.Avatar)) - ); - - member.Avatar = null; - await db.SaveChangesAsync(); - } - - /// - /// Deletes a member's avatar. This should only be used when a member is in the process of being deleted, otherwise, - /// with a null avatar should be used instead. - /// - public async Task DeleteMemberAvatar(Snowflake id, string hash) => await DeleteAvatar(MemberAvatarPath(id, hash)); - /// - /// Deletes a user's avatar. This should only be used when a user is in the process of being deleted, otherwise, - /// with a null avatar should be used instead. - /// - public async Task DeleteUserAvatar(Snowflake id, string hash) => await DeleteAvatar(UserAvatarPath(id, hash)); - - private async Task DeleteAvatar(string path) - { - logger.Debug("Deleting avatar at path {Path}", path); - try - { - await minio.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path)); - } - catch (InvalidObjectNameException) - { - } - } - - private async Task ConvertAvatar(string uri) - { - if (!uri.StartsWith("data:image/")) - throw new ArgumentException("Not a data URI", nameof(uri)); - - var split = uri.Remove(0, "data:".Length).Split(";base64,"); - var contentType = split[0]; - var encoded = split[1]; - if (!_validContentTypes.Contains(contentType)) - throw new ArgumentException("Invalid content type for image", nameof(uri)); - - if (!AuthUtils.TryFromBase64String(encoded, out var rawImage)) - throw new ArgumentException("Invalid base64 string", nameof(uri)); - - var image = Image.Load(rawImage); - - var processor = new ResizeProcessor( - new ResizeOptions { Size = new Size(512), Mode = ResizeMode.Crop, 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 }); - return stream; - } - - private static string UserAvatarPath(Snowflake id, string hash) => $"users/{id}/avatars/{hash}.webp"; - private static string MemberAvatarPath(Snowflake id, string hash) => $"members/{id}/avatars/{hash}.webp"; -} \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs new file mode 100644 index 0000000..56d5077 --- /dev/null +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -0,0 +1,79 @@ +using System.Security.Cryptography; +using Coravel.Invocable; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Extensions; +using Foxnouns.Backend.Services; + +namespace Foxnouns.Backend.Jobs; + +public class MemberAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService objectStorage, ILogger logger) + : IInvocable, IInvocableWithPayload +{ + private readonly ILogger _logger = logger.ForContext(); + public required AvatarUpdatePayload Payload { get; set; } + + public async Task Invoke() + { + 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); + + var 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 + { + var image = await newAvatar.ConvertBase64UriToAvatar(); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); + image.Seek(0, SeekOrigin.Begin); + var prevHash = member.Avatar; + + await objectStorage.PutObjectAsync(Path(id, hash), image, "image/webp"); + + member.Avatar = hash; + await db.SaveChangesAsync(); + + if (prevHash != null && prevHash != hash) + await objectStorage.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); + + var 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 objectStorage.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"; +} \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/Payloads.cs b/Foxnouns.Backend/Jobs/Payloads.cs new file mode 100644 index 0000000..f28254a --- /dev/null +++ b/Foxnouns.Backend/Jobs/Payloads.cs @@ -0,0 +1,5 @@ +using Foxnouns.Backend.Database; + +namespace Foxnouns.Backend.Jobs; + +public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs new file mode 100644 index 0000000..cbec277 --- /dev/null +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -0,0 +1,79 @@ +using System.Security.Cryptography; +using Coravel.Invocable; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Extensions; +using Foxnouns.Backend.Services; + +namespace Foxnouns.Backend.Jobs; + +public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService objectStorage, ILogger logger) + : IInvocable, IInvocableWithPayload +{ + private readonly ILogger _logger = logger.ForContext(); + public required AvatarUpdatePayload Payload { get; set; } + + public async Task Invoke() + { + 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); + + var 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 + { + var image = await newAvatar.ConvertBase64UriToAvatar(); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); + image.Seek(0, SeekOrigin.Begin); + var prevHash = user.Avatar; + + await objectStorage.PutObjectAsync(Path(id, hash), image, "image/webp"); + + user.Avatar = hash; + await db.SaveChangesAsync(); + + if (prevHash != null && prevHash != hash) + await objectStorage.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); + + var 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 objectStorage.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"; +} \ No newline at end of file diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index 8ad5df7..516813b 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -2,7 +2,6 @@ using System.Security.Cryptography; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; -using Hangfire.Dashboard; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -64,33 +63,4 @@ public static class HttpContextExtensions } [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class AuthenticateAttribute : Attribute; - -/// -/// Authentication filter for the Hangfire dashboard. Uses the cookie created by the frontend -/// (and otherwise only read by the frontend) to only allow admins to use it. -/// -public class HangfireDashboardAuthorizationFilter(IServiceProvider services) : IDashboardAsyncAuthorizationFilter -{ - public async Task AuthorizeAsync(DashboardContext context) - { - await using var scope = services.CreateAsyncScope(); - - await using var db = scope.ServiceProvider.GetRequiredService(); - var clock = scope.ServiceProvider.GetRequiredService(); - - var httpContext = context.GetHttpContext(); - - if (!httpContext.Request.Cookies.TryGetValue("pronounscc-token", out var cookie)) return false; - - if (!AuthUtils.TryFromBase64String(cookie!, out var rawToken)) return false; - - 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); - - return oauthToken?.User.Role == UserRole.Admin; - } -} \ No newline at end of file +public class AuthenticateAttribute : Attribute; \ No newline at end of file diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 7230598..e1f201e 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -1,18 +1,15 @@ +using Coravel; using Foxnouns.Backend; using Foxnouns.Backend.Database; using Serilog; using Foxnouns.Backend.Extensions; -using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; -using Hangfire; -using Hangfire.Redis.StackExchange; using Microsoft.AspNetCore.Mvc; using Minio; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Sentry.Extensibility; -using Sentry.Hangfire; // Read version information from .version in the repository root await BuildInfo.ReadBuildInfo(); @@ -64,6 +61,7 @@ JsonConvert.DefaultSettings = () => new JsonSerializerSettings }; builder.Services + .AddQueue() .AddDbContext() .AddCustomServices() .AddCustomMiddleware() @@ -74,12 +72,6 @@ builder.Services .WithCredentials(config.Storage.AccessKey, config.Storage.SecretKey) .Build()); -builder.Services.AddHangfire(c => c.UseSentry().UseRedisStorage(config.Jobs.Redis, new RedisStorageOptions - { - Prefix = "foxnouns_" - })) - .AddHangfireServer(options => { options.WorkerCount = config.Jobs.Workers; }); - var app = builder.Build(); await app.Initialize(args); @@ -95,12 +87,6 @@ app.UseCors(); app.UseCustomMiddleware(); app.MapControllers(); -app.UseHangfireDashboard("/hangfire", new DashboardOptions -{ - AppPath = null, - AsyncAuthorization = [new HangfireDashboardAuthorizationFilter(app.Services)] -}); - app.Urls.Clear(); app.Urls.Add(config.Address); if (config.MetricsAddress != null) app.Urls.Add(config.MetricsAddress); diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs new file mode 100644 index 0000000..2180b90 --- /dev/null +++ b/Foxnouns.Backend/Services/ObjectStorageService.cs @@ -0,0 +1,33 @@ +using Minio; +using Minio.DataModel.Args; +using Minio.Exceptions; + +namespace Foxnouns.Backend.Services; + +public class ObjectStorageService(ILogger logger, Config config, IMinioClient minio) +{ + private readonly ILogger _logger = logger.ForContext(); + + public async Task RemoveObjectAsync(string path) + { + logger.Debug("Deleting object at path {Path}", path); + try + { + await minio.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path)); + } + catch (InvalidObjectNameException) + { + } + } + + public async Task PutObjectAsync(string path, Stream data, string contentType) + { + await minio.PutObjectAsync(new PutObjectArgs() + .WithBucket(config.Storage.Bucket) + .WithObject(path) + .WithObjectSize(data.Length) + .WithStreamData(data) + .WithContentType(contentType) + ); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/Limits.cs b/Foxnouns.Backend/Utils/Limits.cs new file mode 100644 index 0000000..c86df0b --- /dev/null +++ b/Foxnouns.Backend/Utils/Limits.cs @@ -0,0 +1,9 @@ +namespace Foxnouns.Backend.Utils; + +public static class Limits +{ + public const int FieldLimit = 25; + public const int FieldNameLimit = 100; + public const int FieldEntryTextLimit = 100; + public const int FieldEntriesLimit = 100; +} \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 276395f..5c3c591 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -57,11 +57,11 @@ public static class ValidationUtils public static ValidationError? ValidateMemberName(string memberName) { - if (!UsernameRegex.IsMatch(memberName)) + if (!MemberRegex.IsMatch(memberName)) return memberName.Length switch { - < 2 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), - > 40 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), + < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), + > 100 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), _ => ValidationError.GenericValidationError( "Member name cannot contain any of the following: " + " @, ?, !, #, /, \\, [, ], \", ', $, %, &, (, ), {, }, +, <, =, >, ^, |, ~, `, , " + @@ -119,10 +119,7 @@ public static class ValidationUtils }; } - private const int FieldLimit = 25; - private const int FieldNameLimit = 100; - private const int FieldEntryTextLimit = 100; - private const int FieldEntriesLimit = 100; + private static readonly string[] DefaultStatusOptions = [ @@ -140,7 +137,7 @@ public static class ValidationUtils var errors = new List<(string, ValidationError?)>(); if (fields.Count > 25) - errors.Add(("fields", ValidationError.LengthError("Too many fields", 0, FieldLimit, fields.Count))); + errors.Add(("fields", ValidationError.LengthError("Too many fields", 0, Limits.FieldLimit, fields.Count))); // No overwhelming this function, thank you if (fields.Count > 100) return errors; @@ -148,13 +145,13 @@ public static class ValidationUtils { switch (field.Name.Length) { - case > FieldNameLimit: + case > Limits.FieldNameLimit: errors.Add(($"fields.{index}.name", - ValidationError.LengthError("Field name is too long", 1, FieldNameLimit, field.Name.Length))); + ValidationError.LengthError("Field name is too long", 1, Limits.FieldNameLimit, field.Name.Length))); break; case < 1: errors.Add(($"fields.{index}.name", - ValidationError.LengthError("Field name is too short", 1, FieldNameLimit, field.Name.Length))); + ValidationError.LengthError("Field name is too short", 1, Limits.FieldNameLimit, field.Name.Length))); break; } @@ -170,26 +167,26 @@ public static class ValidationUtils if (entries == null || entries.Length == 0) return []; var errors = new List<(string, ValidationError?)>(); - if (entries.Length > FieldEntriesLimit) + if (entries.Length > Limits.FieldEntriesLimit) errors.Add(($"{errorPrefix}.entries", - ValidationError.LengthError("Field has too many entries", 0, FieldEntriesLimit, + ValidationError.LengthError("Field has too many entries", 0, Limits.FieldEntriesLimit, entries.Length))); // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > FieldEntriesLimit + 50) return errors; + if (entries.Length > Limits.FieldEntriesLimit + 50) return errors; foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) { switch (entry.Value.Length) { - case > FieldEntryTextLimit: + case > Limits.FieldEntryTextLimit: errors.Add(($"{errorPrefix}.entries.{entryIdx}.value", - ValidationError.LengthError("Field value is too long", 1, FieldEntryTextLimit, + ValidationError.LengthError("Field value is too long", 1, Limits.FieldEntryTextLimit, entry.Value.Length))); break; case < 1: errors.Add(($"{errorPrefix}.entries.{entryIdx}.value", - ValidationError.LengthError("Field value is too short", 1, FieldEntryTextLimit, + ValidationError.LengthError("Field value is too short", 1, Limits.FieldEntryTextLimit, entry.Value.Length))); break; } diff --git a/Foxnouns.Backend/appSettings.json b/Foxnouns.Backend/appSettings.json new file mode 100644 index 0000000..ae6b417 --- /dev/null +++ b/Foxnouns.Backend/appSettings.json @@ -0,0 +1,7 @@ +{ + "Coravel": { + "Queue": { + "ConsummationDelay": 1 + } + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index e316c8e..0b80a7a 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -31,12 +31,6 @@ Timeout = 5 ; The maximum number of open connections. Defaults to 50. MaxPoolSize = 50 -[Jobs] -; The connection string for the Redis server. -Redis = localhost:6379 -; The number of workers to use for background jobs. Defaults to 5. -Workers = 5 - [Storage] Endpoint = AccessKey = From 54ec469cd92648b7bca277766ba1e2e53f5164f1 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 3 Sep 2024 17:00:14 +0200 Subject: [PATCH 023/261] feat: add actual metrics using prometheus-net --- Foxnouns.Backend/Config.cs | 5 +- .../Controllers/MetaController.cs | 21 ++---- .../Extensions/WebApplicationExtensions.cs | 34 ++-------- Foxnouns.Backend/Foxnouns.Backend.csproj | 5 +- Foxnouns.Backend/FoxnounsMetrics.cs | 37 +++++++++++ Foxnouns.Backend/Metrics.cs | 6 -- Foxnouns.Backend/Program.cs | 10 ++- .../Services/MetricsCollectionService.cs | 66 +++++++++++++++++++ Foxnouns.Backend/config.example.ini | 6 +- 9 files changed, 134 insertions(+), 56 deletions(-) create mode 100644 Foxnouns.Backend/FoxnounsMetrics.cs delete mode 100644 Foxnouns.Backend/Metrics.cs create mode 100644 Foxnouns.Backend/Services/MetricsCollectionService.cs diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 140214d..132a28c 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -10,7 +10,7 @@ public class Config public string MediaBaseUrl { get; set; } = null!; public string Address => $"http://{Host}:{Port}"; - public string? MetricsAddress => Logging.MetricsPort != null ? $"http://{Host}:{Logging.MetricsPort}" : null; + public string MetricsAddress => $"http://{Host}:{Logging.MetricsPort}"; public LoggingConfig Logging { get; init; } = new(); public DatabaseConfig Database { get; init; } = new(); @@ -27,7 +27,8 @@ public class Config public bool SentryTracing { get; init; } = false; public double SentryTracesSampleRate { get; init; } = 0.0; public bool LogQueries { get; init; } = false; - public int? MetricsPort { get; init; } + public bool EnableMetrics { get; init; } = false; + public ushort MetricsPort { get; init; } = 5001; } public class DatabaseConfig diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 6f6b4e1..31c505e 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -1,30 +1,23 @@ -using Foxnouns.Backend.Database; -using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Mvc; -using NodaTime; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/meta")] -public class MetaController(DatabaseContext db, IClock clock) : ApiControllerBase +public class MetaController : ApiControllerBase { private const string Repository = "https://codeberg.org/pronounscc/pronouns.cc"; [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task GetMeta() + public IActionResult GetMeta() { - var now = clock.GetCurrentInstant(); - var users = await db.Users.Select(u => u.LastActive).ToListAsync(); - var memberCount = await db.Members.CountAsync(); - return Ok(new MetaResponse( - Repository, BuildInfo.Version, BuildInfo.Hash, memberCount, + Repository, BuildInfo.Version, BuildInfo.Hash, (int)FoxnounsMetrics.MemberCount.Value, new UserInfo( - users.Count, - users.Count(i => i > now - Duration.FromDays(30)), - users.Count(i => i > now - Duration.FromDays(7)), - users.Count(i => i > now - Duration.FromDays(1)) + (int)FoxnounsMetrics.UsersCount.Value, + (int)FoxnounsMetrics.UsersActiveMonthCount.Value, + (int)FoxnounsMetrics.UsersActiveWeekCount.Value, + (int)FoxnounsMetrics.UsersActiveDayCount.Value )) ); } diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 1915eae..a76928c 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -1,6 +1,3 @@ -using App.Metrics; -using App.Metrics.AspNetCore; -using App.Metrics.Formatters.Prometheus; using Coravel; using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; @@ -9,6 +6,7 @@ using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Microsoft.EntityFrameworkCore; using NodaTime; +using Prometheus; using Serilog; using Serilog.Events; using IClock = NodaTime.IClock; @@ -59,32 +57,12 @@ public static class WebApplicationExtensions return config; } - public static WebApplicationBuilder AddMetrics(this WebApplicationBuilder builder) + public static WebApplicationBuilder AddMetrics(this WebApplicationBuilder builder, Config config) { - var config = builder.Configuration.Get() ?? new(); - var metrics = AppMetrics.CreateDefaultBuilder() - .OutputMetrics.AsPrometheusPlainText() - .Build(); - - builder.Services.AddSingleton(metrics); - builder.Services.AddSingleton(metrics); - - builder.WebHost - .ConfigureMetrics(metrics) - .UseMetrics(opts => - { - opts.EndpointOptions = options => - { - // Metrics must listen on a separate port for security reasons. If no metrics port is set, disable the endpoint entirely. - options.MetricsEndpointEnabled = config.Logging.MetricsPort != null; - options.EnvironmentInfoEndpointEnabled = config.Logging.MetricsPort != null; - options.MetricsTextEndpointEnabled = false; - options.MetricsEndpointOutputFormatter = metrics.OutputMetricsFormatters - .OfType().First(); - }; - }) - .UseMetricsWebTracking() - .ConfigureAppMetricsHostingConfiguration(opts => { opts.AllEndpointsPort = config.Logging.MetricsPort; }); + builder.Services.AddMetricServer(o => o.Port = config.Logging.MetricsPort) + .AddSingleton(); + if (!config.Logging.EnableMetrics) + builder.Services.AddHostedService(); return builder; } diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 711e620..82ccf80 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -6,9 +6,6 @@ - - - @@ -25,6 +22,8 @@ + + diff --git a/Foxnouns.Backend/FoxnounsMetrics.cs b/Foxnouns.Backend/FoxnounsMetrics.cs new file mode 100644 index 0000000..b5cc1ac --- /dev/null +++ b/Foxnouns.Backend/FoxnounsMetrics.cs @@ -0,0 +1,37 @@ +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"); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Metrics.cs b/Foxnouns.Backend/Metrics.cs deleted file mode 100644 index a830ab8..0000000 --- a/Foxnouns.Backend/Metrics.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Foxnouns.Backend; - -public static class Metrics -{ - -} \ No newline at end of file diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index e1f201e..e849bd1 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc; using Minio; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using Prometheus; using Sentry.Extensibility; // Read version information from .version in the repository root @@ -18,7 +19,9 @@ var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration(); -builder.AddSerilog().AddMetrics(); +builder + .AddSerilog() + .AddMetrics(config); builder.WebHost .UseSentry(opts => @@ -89,7 +92,10 @@ app.MapControllers(); app.Urls.Clear(); app.Urls.Add(config.Address); -if (config.MetricsAddress != null) app.Urls.Add(config.MetricsAddress); + +// Make sure metrics are updated whenever Prometheus scrapes them +Metrics.DefaultRegistry.AddBeforeCollectCallback(async ct => + await app.Services.GetRequiredService().CollectMetricsAsync(ct)); // Fire off the periodic tasks loop in the background _ = new Timer(_ => diff --git a/Foxnouns.Backend/Services/MetricsCollectionService.cs b/Foxnouns.Backend/Services/MetricsCollectionService.cs new file mode 100644 index 0000000..f860650 --- /dev/null +++ b/Foxnouns.Backend/Services/MetricsCollectionService.cs @@ -0,0 +1,66 @@ +using System.Diagnostics; +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using Prometheus; + +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) + { + var timer = FoxnounsMetrics.MetricsCollectionTime.NewTimer(); + var now = clock.GetCurrentInstant(); + + await using var scope = services.CreateAsyncScope(); + await using var db = scope.ServiceProvider.GetRequiredService(); + + var 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)); + + var 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 innerService) + : 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"); + await innerService.CollectMetricsAsync(ct); + } + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index 0b80a7a..27e5cbf 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -20,7 +20,11 @@ SentryTracing = true SentryTracesSampleRate = 1.0 ; Whether to log SQL queries. Note that this is very verbose. Defaults to false. LogQueries = false -; The port the /metrics endpoint will listen on. If not set, metrics will be disabled. +; 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] From fb324e7576a72ca573984a6a21f12eab84bd8148 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Sep 2024 01:46:39 +0200 Subject: [PATCH 024/261] refactor: replace periodic tasks loop with background service --- .../Extensions/WebApplicationExtensions.cs | 57 ++++++++++++------- Foxnouns.Backend/Program.cs | 37 ++---------- Foxnouns.Backend/Services/KeyCacheService.cs | 4 +- .../Services/PeriodicTasksService.cs | 26 +++++++++ 4 files changed, 67 insertions(+), 57 deletions(-) create mode 100644 Foxnouns.Backend/Services/PeriodicTasksService.cs diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index a76928c..bf6eda2 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -5,6 +5,7 @@ using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Microsoft.EntityFrameworkCore; +using Minio; using NodaTime; using Prometheus; using Serilog; @@ -57,16 +58,6 @@ public static class WebApplicationExtensions return config; } - public static WebApplicationBuilder AddMetrics(this WebApplicationBuilder builder, Config config) - { - builder.Services.AddMetricServer(o => o.Port = config.Logging.MetricsPort) - .AddSingleton(); - if (!config.Logging.EnableMetrics) - builder.Services.AddHostedService(); - - return builder; - } - public static IConfigurationBuilder AddConfiguration(this IConfigurationBuilder builder) { var file = Environment.GetEnvironmentVariable("FOXNOUNS_CONFIG_FILE") ?? "config.ini"; @@ -78,18 +69,40 @@ public static class WebApplicationExtensions .AddEnvironmentVariables(); } - public static IServiceCollection AddCustomServices(this IServiceCollection services) => services - .AddSingleton(SystemClock.Instance) - .AddSnowflakeGenerator() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - // Transient jobs - .AddTransient() - .AddTransient(); + /// + /// Adds required services to the IServiceCollection. + /// This should only add services that are not ASP.NET-related (i.e. no middleware). + /// + public static IServiceCollection AddServices(this IServiceCollection services, Config config) + { + services + .AddQueue() + .AddDbContext() + .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() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + // Background services + .AddHostedService() + // Transient jobs + .AddTransient() + .AddTransient(); + + if (!config.Logging.EnableMetrics) + services.AddHostedService(); + + return services; + } public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services .AddScoped() diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index e849bd1..911c895 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -1,12 +1,9 @@ -using Coravel; using Foxnouns.Backend; -using Foxnouns.Backend.Database; using Serilog; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; -using Minio; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Prometheus; @@ -19,9 +16,7 @@ var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration(); -builder - .AddSerilog() - .AddMetrics(config); +builder.AddSerilog(); builder.WebHost .UseSentry(opts => @@ -64,16 +59,10 @@ JsonConvert.DefaultSettings = () => new JsonSerializerSettings }; builder.Services - .AddQueue() - .AddDbContext() - .AddCustomServices() + .AddServices(config) .AddCustomMiddleware() .AddEndpointsApiExplorer() - .AddSwaggerGen() - .AddMinio(c => - c.WithEndpoint(config.Storage.Endpoint) - .WithCredentials(config.Storage.AccessKey, config.Storage.SecretKey) - .Build()); + .AddSwaggerGen(); var app = builder.Build(); @@ -97,23 +86,5 @@ app.Urls.Add(config.Address); Metrics.DefaultRegistry.AddBeforeCollectCallback(async ct => await app.Services.GetRequiredService().CollectMetricsAsync(ct)); -// Fire off the periodic tasks loop in the background -_ = new Timer(_ => -{ - var __ = RunPeriodicTasksAsync(); -}, null, TimeSpan.FromSeconds(30), TimeSpan.FromMinutes(1)); - app.Run(); -Log.CloseAndFlush(); - -return; - -async Task RunPeriodicTasksAsync() -{ - await using var scope = app.Services.CreateAsyncScope(); - var logger = scope.ServiceProvider.GetRequiredService(); - logger.Debug("Running periodic tasks"); - - var keyCacheSvc = scope.ServiceProvider.GetRequiredService(); - await keyCacheSvc.DeleteExpiredKeysAsync(); -} \ No newline at end of file +Log.CloseAndFlush(); \ No newline at end of file diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 4523d16..108199c 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -37,9 +37,9 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) return value.Value; } - public async Task DeleteExpiredKeysAsync() + public async Task DeleteExpiredKeysAsync(CancellationToken ct) { - var count = await db.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()).ExecuteDeleteAsync(); + var count = await db.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()).ExecuteDeleteAsync(ct); if (count != 0) logger.Information("Removed {Count} expired keys from the database", count); } diff --git a/Foxnouns.Backend/Services/PeriodicTasksService.cs b/Foxnouns.Backend/Services/PeriodicTasksService.cs new file mode 100644 index 0000000..d043317 --- /dev/null +++ b/Foxnouns.Backend/Services/PeriodicTasksService.cs @@ -0,0 +1,26 @@ +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)) + { + _logger.Debug("Collecting metrics"); + await RunPeriodicTasksAsync(ct); + } + } + + private async Task RunPeriodicTasksAsync(CancellationToken ct) + { + _logger.Debug("Running periodic tasks"); + + await using var scope = services.CreateAsyncScope(); + + var keyCacheSvc = scope.ServiceProvider.GetRequiredService(); + await keyCacheSvc.DeleteExpiredKeysAsync(ct); + } +} \ No newline at end of file From 6c9d1c328b5b856f9450b3fe405be1af5398d659 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 4 Sep 2024 14:25:44 +0200 Subject: [PATCH 025/261] fix: add class context to all loggers, format --- .../Authentication/AuthController.cs | 4 ++- .../Authentication/DiscordAuthController.cs | 10 ++++--- .../Authentication/EmailAuthController.cs | 6 ++-- .../Controllers/DebugController.cs | 4 ++- .../Database/DatabaseQueryExtensions.cs | 3 +- Foxnouns.Backend/Database/Models/User.cs | 2 +- .../Extensions/WebApplicationExtensions.cs | 2 +- Foxnouns.Backend/Foxnouns.Backend.csproj | 30 +++++++++---------- Foxnouns.Backend/FoxnounsMetrics.cs | 2 +- Foxnouns.Backend/GlobalUsing.cs | 2 +- .../Jobs/UserAvatarUpdateInvocable.cs | 4 +-- .../Middleware/ErrorHandlerMiddleware.cs | 1 + Foxnouns.Backend/Services/KeyCacheService.cs | 4 ++- .../Services/ObjectStorageService.cs | 8 +++-- .../Services/PeriodicTasksService.cs | 2 +- Foxnouns.Backend/Utils/ValidationUtils.cs | 7 +++-- 16 files changed, 54 insertions(+), 37 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 6565fba..db2e21f 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -9,11 +9,13 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth")] public class AuthController(Config config, KeyCacheService keyCacheSvc, ILogger logger) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpPost("urls")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task UrlsAsync() { - logger.Debug("Generating auth URLs for Discord: {Discord}, Google: {Google}, Tumblr: {Tumblr}", + _logger.Debug("Generating auth URLs for Discord: {Discord}, Google: {Google}, Tumblr: {Tumblr}", config.DiscordAuth.Enabled, config.GoogleAuth.Enabled, config.TumblrAuth.Enabled); diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 577231c..d717aba 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -20,6 +20,8 @@ public class DiscordAuthController( RemoteAuthService remoteAuthSvc, UserRendererService userRendererSvc) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpPost("callback")] // TODO: duplicating attribute doesn't work, find another way to mark both as possible response // leaving it here for documentation purposes @@ -34,7 +36,7 @@ public class DiscordAuthController( var user = await authSvc.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); if (user != null) return Ok(await GenerateUserTokenAsync(user)); - logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, + _logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, remoteUser.Id); var ticket = AuthUtils.RandomToken(); @@ -51,7 +53,7 @@ public class DiscordAuthController( 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", + _logger.Error("Discord user {Id} has valid ticket but is already linked to an existing account", remoteUser.Id); throw new FoxnounsError("Discord ticket was issued for user with existing link"); } @@ -65,13 +67,13 @@ public class DiscordAuthController( private async Task GenerateUserTokenAsync(User user) { var frontendApp = await db.GetFrontendApplicationAsync(); - logger.Debug("Logging user {Id} in with Discord", user.Id); + _logger.Debug("Logging user {Id} in with Discord", user.Id); var (tokenStr, token) = authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); - logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); + _logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); await db.SaveChangesAsync(); diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index e1146c5..19dfc2f 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -13,6 +13,8 @@ public class EmailAuthController( IClock clock, ILogger logger) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task LoginAsync([FromBody] LoginRequest req) @@ -23,13 +25,13 @@ public class EmailAuthController( var frontendApp = await db.GetFrontendApplicationAsync(); - logger.Debug("Logging user {Id} in with email and password", user.Id); + _logger.Debug("Logging user {Id} in with email and password", user.Id); var (tokenStr, token) = authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); - logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); + _logger.Debug("Generated token {TokenId} for {UserId}", token.Id, user.Id); await db.SaveChangesAsync(); diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs index 94a0ff2..a8d3ab2 100644 --- a/Foxnouns.Backend/Controllers/DebugController.cs +++ b/Foxnouns.Backend/Controllers/DebugController.cs @@ -14,11 +14,13 @@ public class DebugController( IClock clock, ILogger logger) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpPost("users")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task CreateUserAsync([FromBody] CreateUserRequest req) { - logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email); + _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(); diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index b8f10e9..1d4e851 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -49,7 +49,8 @@ public static class DatabaseQueryExtensions 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) + public static async Task ResolveMemberAsync(this DatabaseContext context, string userRef, string memberRef, + Token? token) { var user = await context.ResolveUserAsync(userRef, token); return await context.ResolveMemberAsync(user.Id, memberRef); diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index 305bd46..c152e65 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -39,7 +39,7 @@ public class User : BaseModel 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; } diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index bf6eda2..1f1ee31 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -97,7 +97,7 @@ public static class WebApplicationExtensions // Transient jobs .AddTransient() .AddTransient(); - + if (!config.Logging.EnableMetrics) services.AddHostedService(); diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 82ccf80..c22fdf4 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -6,31 +6,31 @@ - + - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - - - - - + + + + + - - - - + + + + diff --git a/Foxnouns.Backend/FoxnounsMetrics.cs b/Foxnouns.Backend/FoxnounsMetrics.cs index b5cc1ac..648b97a 100644 --- a/Foxnouns.Backend/FoxnounsMetrics.cs +++ b/Foxnouns.Backend/FoxnounsMetrics.cs @@ -15,7 +15,7 @@ public static class FoxnounsMetrics 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"); diff --git a/Foxnouns.Backend/GlobalUsing.cs b/Foxnouns.Backend/GlobalUsing.cs index 5275c8d..8c73595 100644 --- a/Foxnouns.Backend/GlobalUsing.cs +++ b/Foxnouns.Backend/GlobalUsing.cs @@ -1,2 +1,2 @@ global using ILogger = Serilog.ILogger; -global using Log = Serilog.Log; +global using Log = Serilog.Log; \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index cbec277..f0b04b2 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -21,7 +21,7 @@ public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService private async Task UpdateUserAvatarAsync(Snowflake id, string newAvatar) { _logger.Debug("Updating avatar for user {MemberId}", id); - + var user = await db.Users.FindAsync(id); if (user == null) { @@ -55,7 +55,7 @@ public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService private async Task ClearUserAvatarAsync(Snowflake id) { _logger.Debug("Clearing avatar for user {MemberId}", id); - + var user = await db.Users.FindAsync(id); if (user == null) { diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index 39dfd85..c52c3f0 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -110,6 +110,7 @@ public record HttpApiError [JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] public required ErrorCode Code { get; init; } + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string? ErrorId { get; init; } diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 108199c..bd8a862 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -9,6 +9,8 @@ namespace Foxnouns.Backend.Services; public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) { + private readonly ILogger _logger = logger.ForContext(); + public Task SetKeyAsync(string key, string value, Duration expireAfter) => SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter); @@ -40,7 +42,7 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) public async Task DeleteExpiredKeysAsync(CancellationToken ct) { var count = await db.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()).ExecuteDeleteAsync(ct); - if (count != 0) logger.Information("Removed {Count} expired keys from the database", count); + if (count != 0) _logger.Information("Removed {Count} expired keys from the database", count); } public Task SetKeyAsync(string key, T obj, Duration expiresAt) where T : class => diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs index 2180b90..de60074 100644 --- a/Foxnouns.Backend/Services/ObjectStorageService.cs +++ b/Foxnouns.Backend/Services/ObjectStorageService.cs @@ -7,21 +7,25 @@ namespace Foxnouns.Backend.Services; public class ObjectStorageService(ILogger logger, Config config, IMinioClient minio) { private readonly ILogger _logger = logger.ForContext(); - + public async Task RemoveObjectAsync(string path) { - logger.Debug("Deleting object at path {Path}", path); + _logger.Debug("Deleting object at path {Path}", path); try { await minio.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path)); } catch (InvalidObjectNameException) { + // ignore non-existent objects } } public async Task PutObjectAsync(string path, Stream data, string contentType) { + _logger.Debug("Putting object at path {Path} with length {Length} and content type {ContentType}", path, + data.Length, contentType); + await minio.PutObjectAsync(new PutObjectArgs() .WithBucket(config.Storage.Bucket) .WithObject(path) diff --git a/Foxnouns.Backend/Services/PeriodicTasksService.cs b/Foxnouns.Backend/Services/PeriodicTasksService.cs index d043317..b799abe 100644 --- a/Foxnouns.Backend/Services/PeriodicTasksService.cs +++ b/Foxnouns.Backend/Services/PeriodicTasksService.cs @@ -17,7 +17,7 @@ public class PeriodicTasksService(ILogger logger, IServiceProvider services) : B private async Task RunPeriodicTasksAsync(CancellationToken ct) { _logger.Debug("Running periodic tasks"); - + await using var scope = services.CreateAsyncScope(); var keyCacheSvc = scope.ServiceProvider.GetRequiredService(); diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 5c3c591..8100f4f 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -120,7 +120,6 @@ public static class ValidationUtils } - private static readonly string[] DefaultStatusOptions = [ "favourite", @@ -147,11 +146,13 @@ public static class ValidationUtils { case > Limits.FieldNameLimit: errors.Add(($"fields.{index}.name", - ValidationError.LengthError("Field name is too long", 1, Limits.FieldNameLimit, field.Name.Length))); + ValidationError.LengthError("Field name is too long", 1, Limits.FieldNameLimit, + field.Name.Length))); break; case < 1: errors.Add(($"fields.{index}.name", - ValidationError.LengthError("Field name is too short", 1, Limits.FieldNameLimit, field.Name.Length))); + ValidationError.LengthError("Field name is too short", 1, Limits.FieldNameLimit, + field.Name.Length))); break; } From 22d09ad7a6e2613a48305c4086f5e6c21926bea7 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Sep 2024 21:10:45 +0200 Subject: [PATCH 026/261] fix: return correct error in GET /users/@me --- Foxnouns.Backend/Controllers/UsersController.cs | 13 ++++++++++--- .../Database/DatabaseQueryExtensions.cs | 16 +++++++++++----- Foxnouns.Backend/ExpectedError.cs | 14 ++++++++++---- .../Middleware/AuthorizationMiddleware.cs | 4 ++-- Foxnouns.Backend/Services/UserRendererService.cs | 7 ++++--- 5 files changed, 37 insertions(+), 17 deletions(-) diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 78967f3..8fdf235 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -20,15 +20,16 @@ public class UsersController( { [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetUserAsync(string userRef) + public async Task GetUserAsync(string userRef, CancellationToken ct = default) { - var user = await db.ResolveUserAsync(userRef, CurrentToken); + var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); return Ok(await userRendererService.RenderUserAsync( user, selfUser: CurrentUser, token: CurrentToken, renderMembers: true, - renderAuthMethods: true + renderAuthMethods: true, + ct: ct )); } @@ -59,6 +60,11 @@ public class UsersController( user.Bio = req.Bio; } + if (req.HasProperty(nameof(req.Links))) + { + user.Links = req.Links ?? []; + } + if (req.HasProperty(nameof(req.Avatar))) errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); @@ -150,5 +156,6 @@ public class UsersController( public string? DisplayName { get; init; } public string? Bio { get; init; } public string? Avatar { get; init; } + public string[]? Links { get; init; } } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 1d4e851..b6ccaaa 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -9,23 +9,29 @@ namespace Foxnouns.Backend.Database; public static class DatabaseQueryExtensions { - public static async Task ResolveUserAsync(this DatabaseContext context, string userRef, Token? token) + public static async Task ResolveUserAsync(this DatabaseContext context, string userRef, Token? token, + CancellationToken ct = default) { - if (userRef == "@me" && token != null) - return await context.Users.FirstAsync(u => u.Id == token.UserId); + if (userRef == "@me") + { + 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 var snowflake)) { user = await context.Users .Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Id == snowflake); + .FirstOrDefaultAsync(u => u.Id == snowflake, ct); if (user != null) return user; } user = await context.Users .Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Username == userRef); + .FirstOrDefaultAsync(u => u.Username == userRef, ct); if (user != null) return user; throw new ApiError.NotFound("No user with that ID or username found.", code: ErrorCode.UserNotFound); } diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 6e39af7..f277265 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -23,11 +23,15 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo public readonly HttpStatusCode StatusCode = statusCode ?? HttpStatusCode.InternalServerError; public readonly ErrorCode ErrorCode = errorCode ?? ErrorCode.InternalServerError; - public class Unauthorized(string message) : ApiError(message, statusCode: HttpStatusCode.Unauthorized, - errorCode: ErrorCode.AuthenticationError); + public class Unauthorized(string message, ErrorCode errorCode = ErrorCode.AuthenticationError) : ApiError(message, + statusCode: HttpStatusCode.Unauthorized, + errorCode: errorCode); - public class Forbidden(string message, IEnumerable? scopes = null) - : ApiError(message, statusCode: HttpStatusCode.Forbidden) + public class Forbidden( + string message, + IEnumerable? scopes = null, + ErrorCode errorCode = ErrorCode.Forbidden) + : ApiError(message, statusCode: HttpStatusCode.Forbidden, errorCode: errorCode) { public readonly string[] Scopes = scopes?.ToArray() ?? []; } @@ -115,6 +119,8 @@ public enum ErrorCode Forbidden, BadRequest, AuthenticationError, + AuthenticationRequired, + MissingScopes, GenericApiError, UserNotFound, MemberNotFound, diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index dd0d97f..6d4bc49 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -18,10 +18,10 @@ public class AuthorizationMiddleware : IMiddleware var token = ctx.GetToken(); if (token == null) - throw new ApiError.Unauthorized("This endpoint requires an authenticated user."); + throw new ApiError.Unauthorized("This endpoint requires an authenticated user.", ErrorCode.AuthenticationRequired); 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())); + attribute.Scopes.Except(token.Scopes.ExpandScopes()), ErrorCode.MissingScopes); 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 != UserRole.Admin && token.User.Role != UserRole.Moderator) diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 9bb198e..4449488 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -12,7 +12,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe public async Task RenderUserAsync(User user, User? selfUser = null, Token? token = null, bool renderMembers = true, - bool renderAuthMethods = false) + bool renderAuthMethods = false, + CancellationToken ct = default) { var isSelfUser = selfUser?.Id == user.Id; var tokenCanReadHiddenMembers = token.HasScope("member.read") && isSelfUser; @@ -24,7 +25,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe renderAuthMethods = renderAuthMethods && tokenPrivileged; IEnumerable members = - renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync() : []; + renderMembers ? await db.Members.Where(m => m.UserId == user.Id).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); @@ -32,7 +33,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe ? await db.AuthMethods .Where(a => a.UserId == user.Id) .Include(a => a.FediverseApplication) - .ToListAsync() + .ToListAsync(ct) : []; return new UserResponse( From fa3c1ccaa7a924b28eb230b590247848f926bd83 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Sep 2024 22:17:10 +0200 Subject: [PATCH 027/261] feat: add user settings endpoint --- .../Controllers/UsersController.cs | 31 ++ Foxnouns.Backend/Database/DatabaseContext.cs | 1 + ...20240905191709_AddUserSettings.Designer.cs | 432 ++++++++++++++++++ .../20240905191709_AddUserSettings.cs | 30 ++ .../DatabaseContextModelSnapshot.cs | 175 ++----- Foxnouns.Backend/Database/Models/User.cs | 6 + 6 files changed, 536 insertions(+), 139 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 8fdf235..a2c6219 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -158,4 +158,35 @@ public class UsersController( public string? Avatar { get; init; } public string[]? Links { get; init; } } + + + [HttpGet("@me/settings")] + [Authorize("user.read_hidden")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task GetUserSettingsAsync(CancellationToken ct = default) + { + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); + return Ok(user.Settings); + } + + [HttpPatch("@me/settings")] + [Authorize("user.read_hidden", "user.update")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task UpdateUserSettingsAsync([FromBody] UpdateUserSettingsRequest req, CancellationToken ct = default) + { + var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); + + if (req.HasProperty(nameof(req.DarkMode))) + user.Settings.DarkMode = req.DarkMode; + + db.Update(user); + await db.SaveChangesAsync(ct); + + return Ok(user.Settings); + } + + public class UpdateUserSettingsRequest : PatchRequest + { + public bool? DarkMode { get; init; } + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 725afb6..26087b8 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -63,6 +63,7 @@ public class DatabaseContext : DbContext 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.Fields).HasColumnType("jsonb"); modelBuilder.Entity().Property(m => m.Names).HasColumnType("jsonb"); diff --git a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs new file mode 100644 index 0000000..45b3a53 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs @@ -0,0 +1,432 @@ +// +using System; +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("20240905191709_AddUserSettings")] + partial class AddUserSettings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .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>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + + b.Property("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("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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (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>("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("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("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.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.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs new file mode 100644 index 0000000..49c5bd1 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs @@ -0,0 +1,30 @@ +using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + 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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index fc99285..36ca955 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -150,6 +150,11 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("display_name"); + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + b.Property("Links") .IsRequired() .HasColumnType("text[]") @@ -160,6 +165,16 @@ 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("Unlisted") .HasColumnType("boolean") .HasColumnName("unlisted"); @@ -269,7 +284,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("bio"); - b.Property>("CustomPreferences") + b.Property>("CustomPreferences") .IsRequired() .HasColumnType("jsonb") .HasColumnName("custom_preferences"); @@ -290,6 +305,11 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("display_name"); + b.Property>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + b.Property("LastActive") .HasColumnType("timestamp with time zone") .HasColumnName("last_active"); @@ -307,14 +327,29 @@ 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("Username") .IsRequired() .HasColumnType("text") @@ -358,72 +393,6 @@ 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"); }); @@ -448,78 +417,6 @@ namespace Foxnouns.Backend.Database.Migrations 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"); diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index c152e65..de48944 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -25,6 +25,7 @@ public class User : BaseModel public List Members { get; } = []; public List AuthMethods { get; } = []; + public UserSettings Settings { get; set; } = new(); public required Instant LastActive { get; set; } @@ -58,4 +59,9 @@ public enum PreferenceSize Large, Normal, Small, +} + +public class UserSettings +{ + public bool? DarkMode { get; set; } = null; } \ No newline at end of file From c4adf6918c23b4b0c87792239d8585ccba77f7ca Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Sep 2024 22:29:12 +0200 Subject: [PATCH 028/261] switch to another frontend framework wheeeeeeeeeeee --- Foxnouns.Frontend/.env.example | 4 - Foxnouns.Frontend/.eslintrc.cjs | 84 + Foxnouns.Frontend/.gitignore | 9 +- Foxnouns.Frontend/.npmrc | 1 - Foxnouns.Frontend/.prettierignore | 4 - Foxnouns.Frontend/.prettierrc | 5 +- Foxnouns.Frontend/README.md | 52 +- Foxnouns.Frontend/app/app.scss | 21 + Foxnouns.Frontend/app/components/nav/Logo.tsx | 41 + .../app/components/nav/Navbar.tsx | 87 + Foxnouns.Frontend/app/entry.client.tsx | 18 + Foxnouns.Frontend/app/entry.server.tsx | 140 + Foxnouns.Frontend/app/env.server.ts | 3 + Foxnouns.Frontend/app/lib/api/error.ts | 28 + .../{src => app}/lib/api/meta.ts | 0 .../{src => app}/lib/api/user.ts | 4 + Foxnouns.Frontend/app/lib/request.server.ts | 66 + Foxnouns.Frontend/app/lib/settings.server.ts | 23 + Foxnouns.Frontend/app/root.tsx | 85 + .../app/routes/$username/route.tsx | 30 + Foxnouns.Frontend/app/routes/_index.tsx | 45 + .../app/routes/dark-mode/route.tsx | 31 + Foxnouns.Frontend/eslint.config.js | 33 - Foxnouns.Frontend/package.json | 82 +- Foxnouns.Frontend/public/favicon.ico | Bin 0 -> 16958 bytes Foxnouns.Frontend/server.js | 52 + Foxnouns.Frontend/src/app.d.ts | 16 - Foxnouns.Frontend/src/app.html | 19 - Foxnouns.Frontend/src/app.scss | 8 - Foxnouns.Frontend/src/error.html | 73 - Foxnouns.Frontend/src/hooks.server.ts | 15 - Foxnouns.Frontend/src/lib/api/auth.ts | 18 - Foxnouns.Frontend/src/lib/index.ts | 1 - Foxnouns.Frontend/src/lib/nav/Dropdown.svelte | 11 - .../src/lib/nav/DropdownItem.svelte | 10 - Foxnouns.Frontend/src/lib/nav/Logo.svelte | 34 - Foxnouns.Frontend/src/lib/nav/Navbar.svelte | 66 - Foxnouns.Frontend/src/lib/request.ts | 72 - Foxnouns.Frontend/src/lib/store.ts | 9 - Foxnouns.Frontend/src/routes/+error.svelte | 14 - .../src/routes/+layout.server.ts | 13 - Foxnouns.Frontend/src/routes/+layout.svelte | 10 - Foxnouns.Frontend/src/routes/+page.svelte | 24 - .../src/routes/@[username]/+page.server.ts | 12 - .../src/routes/@[username]/+page.svelte | 10 - .../src/routes/auth/login/+page.server.ts | 12 - .../src/routes/auth/login/+page.svelte | 53 - .../routes/auth/login/discord/+page.server.ts | 51 - .../routes/auth/login/discord/+page.svelte | 79 - .../src/routes/neofox_confused_2048.png | Bin 146626 -> 0 bytes Foxnouns.Frontend/static/default/512.webp | Bin 43736 -> 0 bytes Foxnouns.Frontend/static/favicon.svg | 2 - Foxnouns.Frontend/static/logo.svg | 34 - Foxnouns.Frontend/static/robots.txt | 5 - Foxnouns.Frontend/svelte.config.js | 24 - Foxnouns.Frontend/tsconfig.json | 37 +- Foxnouns.Frontend/vite.config.ts | 24 +- Foxnouns.Frontend/yarn.lock | 6245 ++++++++++++++--- 58 files changed, 6246 insertions(+), 1703 deletions(-) delete mode 100644 Foxnouns.Frontend/.env.example create mode 100644 Foxnouns.Frontend/.eslintrc.cjs delete mode 100644 Foxnouns.Frontend/.npmrc delete mode 100644 Foxnouns.Frontend/.prettierignore create mode 100644 Foxnouns.Frontend/app/app.scss create mode 100644 Foxnouns.Frontend/app/components/nav/Logo.tsx create mode 100644 Foxnouns.Frontend/app/components/nav/Navbar.tsx create mode 100644 Foxnouns.Frontend/app/entry.client.tsx create mode 100644 Foxnouns.Frontend/app/entry.server.tsx create mode 100644 Foxnouns.Frontend/app/env.server.ts create mode 100644 Foxnouns.Frontend/app/lib/api/error.ts rename Foxnouns.Frontend/{src => app}/lib/api/meta.ts (100%) rename Foxnouns.Frontend/{src => app}/lib/api/user.ts (75%) create mode 100644 Foxnouns.Frontend/app/lib/request.server.ts create mode 100644 Foxnouns.Frontend/app/lib/settings.server.ts create mode 100644 Foxnouns.Frontend/app/root.tsx create mode 100644 Foxnouns.Frontend/app/routes/$username/route.tsx create mode 100644 Foxnouns.Frontend/app/routes/_index.tsx create mode 100644 Foxnouns.Frontend/app/routes/dark-mode/route.tsx delete mode 100644 Foxnouns.Frontend/eslint.config.js create mode 100644 Foxnouns.Frontend/public/favicon.ico create mode 100644 Foxnouns.Frontend/server.js delete mode 100644 Foxnouns.Frontend/src/app.d.ts delete mode 100644 Foxnouns.Frontend/src/app.html delete mode 100644 Foxnouns.Frontend/src/app.scss delete mode 100644 Foxnouns.Frontend/src/error.html delete mode 100644 Foxnouns.Frontend/src/hooks.server.ts delete mode 100644 Foxnouns.Frontend/src/lib/api/auth.ts delete mode 100644 Foxnouns.Frontend/src/lib/index.ts delete mode 100644 Foxnouns.Frontend/src/lib/nav/Dropdown.svelte delete mode 100644 Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte delete mode 100644 Foxnouns.Frontend/src/lib/nav/Logo.svelte delete mode 100644 Foxnouns.Frontend/src/lib/nav/Navbar.svelte delete mode 100644 Foxnouns.Frontend/src/lib/request.ts delete mode 100644 Foxnouns.Frontend/src/lib/store.ts delete mode 100644 Foxnouns.Frontend/src/routes/+error.svelte delete mode 100644 Foxnouns.Frontend/src/routes/+layout.server.ts delete mode 100644 Foxnouns.Frontend/src/routes/+layout.svelte delete mode 100644 Foxnouns.Frontend/src/routes/+page.svelte delete mode 100644 Foxnouns.Frontend/src/routes/@[username]/+page.server.ts delete mode 100644 Foxnouns.Frontend/src/routes/@[username]/+page.svelte delete mode 100644 Foxnouns.Frontend/src/routes/auth/login/+page.server.ts delete mode 100644 Foxnouns.Frontend/src/routes/auth/login/+page.svelte delete mode 100644 Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts delete mode 100644 Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte delete mode 100755 Foxnouns.Frontend/src/routes/neofox_confused_2048.png delete mode 100644 Foxnouns.Frontend/static/default/512.webp delete mode 100644 Foxnouns.Frontend/static/favicon.svg delete mode 100644 Foxnouns.Frontend/static/logo.svg delete mode 100644 Foxnouns.Frontend/static/robots.txt delete mode 100644 Foxnouns.Frontend/svelte.config.js diff --git a/Foxnouns.Frontend/.env.example b/Foxnouns.Frontend/.env.example deleted file mode 100644 index 91687be..0000000 --- a/Foxnouns.Frontend/.env.example +++ /dev/null @@ -1,4 +0,0 @@ -# The API base that the server itself should call, this should not be behind a reverse proxy. -PRIVATE_API_BASE=http://localhost:5000/api -# The API base that clients should call, behind a reverse proxy. -PUBLIC_API_BASE=https://pronouns.cc/api diff --git a/Foxnouns.Frontend/.eslintrc.cjs b/Foxnouns.Frontend/.eslintrc.cjs new file mode 100644 index 0000000..a50c150 --- /dev/null +++ b/Foxnouns.Frontend/.eslintrc.cjs @@ -0,0 +1,84 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + ignorePatterns: ["!**/.server", "!**/.client"], + + // Base config + extends: ["eslint:recommended"], + + overrides: [ + // React + { + files: ["**/*.{js,jsx,ts,tsx}"], + plugins: ["react", "jsx-a11y"], + extends: [ + "plugin:react/recommended", + "plugin:react/jsx-runtime", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended", + ], + settings: { + react: { + version: "detect", + }, + formComponents: ["Form"], + linkComponents: [ + { name: "Link", linkAttribute: "to" }, + { name: "NavLink", linkAttribute: "to" }, + ], + "import/resolver": { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ["**/*.{ts,tsx}"], + plugins: ["@typescript-eslint", "import"], + parser: "@typescript-eslint/parser", + settings: { + "import/internal-regex": "^~/", + "import/resolver": { + node: { + extensions: [".ts", ".tsx"], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: [ + "plugin:@typescript-eslint/recommended", + "plugin:import/recommended", + "plugin:import/typescript", + ], + }, + + // Node + { + files: [".eslintrc.cjs"], + env: { + node: true, + }, + }, + ], +}; diff --git a/Foxnouns.Frontend/.gitignore b/Foxnouns.Frontend/.gitignore index 6635cf5..80ec311 100644 --- a/Foxnouns.Frontend/.gitignore +++ b/Foxnouns.Frontend/.gitignore @@ -1,10 +1,5 @@ -.DS_Store node_modules + +/.cache /build -/.svelte-kit -/package .env -.env.* -!.env.example -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 cc41cea..0000000 --- a/Foxnouns.Frontend/.prettierignore +++ /dev/null @@ -1,4 +0,0 @@ -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock diff --git a/Foxnouns.Frontend/.prettierrc b/Foxnouns.Frontend/.prettierrc index 9b685c1..c959087 100644 --- a/Foxnouns.Frontend/.prettierrc +++ b/Foxnouns.Frontend/.prettierrc @@ -1,6 +1,3 @@ { - "useTabs": true, - "printWidth": 100, - "plugins": ["prettier-plugin-svelte"], - "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] + "useTabs": true } diff --git a/Foxnouns.Frontend/README.md b/Foxnouns.Frontend/README.md index 5ce6766..6c4d216 100644 --- a/Foxnouns.Frontend/README.md +++ b/Foxnouns.Frontend/README.md @@ -1,38 +1,40 @@ -# create-svelte +# Welcome to Remix! -Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). +- 📖 [Remix docs](https://remix.run/docs) -## Creating a project +## Development -If you're seeing this, you've probably already done this step. Congrats! +Run the dev server: -```bash -# create a new project in the current directory -npm create svelte@latest - -# create a new project in my-app -npm create svelte@latest 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 +```shellscript npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open ``` -## Building +## Deployment -To create a production version of your app: +First, build your app for production: -```bash +```sh npm run build ``` -You can preview the production build with `npm run preview`. +Then run the app in production mode: -> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. +```sh +npm start +``` + +Now you'll need to pick a host to deploy it to. + +### DIY + +If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. + +Make sure to deploy the output of `npm run build` + +- `build/server` +- `build/client` + +## Styling + +This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. diff --git a/Foxnouns.Frontend/app/app.scss b/Foxnouns.Frontend/app/app.scss new file mode 100644 index 0000000..19155ce --- /dev/null +++ b/Foxnouns.Frontend/app/app.scss @@ -0,0 +1,21 @@ +$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" !default; + +@import "bootstrap/scss/bootstrap"; + +@import "@fontsource/firago/400.css"; +@import "@fontsource/firago/400-italic.css"; +@import "@fontsource/firago/700.css"; \ No newline at end of file diff --git a/Foxnouns.Frontend/app/components/nav/Logo.tsx b/Foxnouns.Frontend/app/components/nav/Logo.tsx new file mode 100644 index 0000000..216eb54 --- /dev/null +++ b/Foxnouns.Frontend/app/components/nav/Logo.tsx @@ -0,0 +1,41 @@ +export default function Logo() { + return ( + + + + + + + + + + + + + + + + ); +} diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx new file mode 100644 index 0000000..2dccea3 --- /dev/null +++ b/Foxnouns.Frontend/app/components/nav/Navbar.tsx @@ -0,0 +1,87 @@ +import { Link, useFetcher } from "@remix-run/react"; +import Meta from "~/lib/api/meta"; +import { User, UserSettings } from "~/lib/api/user"; +import Logo from "./Logo"; + +import Nav from "react-bootstrap/Nav"; +import Navbar from "react-bootstrap/Navbar"; +import NavDropdown from "react-bootstrap/NavDropdown"; +import { + BrightnessHigh, + BrightnessHighFill, + MoonFill, +} from "react-bootstrap-icons"; + +export default function MainNavbar({ + user, + settings, +}: { + meta: Meta; + user?: User; + settings: UserSettings; +}) { + const fetcher = useFetcher(); + + const userMenu = user ? ( + @{user.username}} align="end"> + + View profile + + + Settings + + + + Log out + + + ) : ( + + Log in or sign up + + ); + + const ThemeIcon = + settings.dark_mode === null + ? BrightnessHigh + : settings.dark_mode + ? MoonFill + : BrightnessHighFill; + + return ( + + + + + + + + + + + Theme + + } + align="end" + > + + Automatic + + + Dark mode + + + Light mode + + + + + ); +} diff --git a/Foxnouns.Frontend/app/entry.client.tsx b/Foxnouns.Frontend/app/entry.client.tsx new file mode 100644 index 0000000..305d71b --- /dev/null +++ b/Foxnouns.Frontend/app/entry.client.tsx @@ -0,0 +1,18 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +import { startTransition, StrictMode } from "react"; +import { hydrateRoot } from "react-dom/client"; + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/Foxnouns.Frontend/app/entry.server.tsx b/Foxnouns.Frontend/app/entry.server.tsx new file mode 100644 index 0000000..5e213e2 --- /dev/null +++ b/Foxnouns.Frontend/app/entry.server.tsx @@ -0,0 +1,140 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from "node:stream"; + +import type { AppLoadContext, EntryContext } from "@remix-run/node"; +import { createReadableStreamFromReadable } from "@remix-run/node"; +import { RemixServer } from "@remix-run/react"; +import { isbot } from "isbot"; +import { renderToPipeableStream } from "react-dom/server"; + +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // This is ignored so we can keep it in the template for visibility. Feel + // free to delete this parameter in your app if you're not using it! + // eslint-disable-next-line @typescript-eslint/no-unused-vars + loadContext: AppLoadContext, +) { + return isbot(request.headers.get("user-agent") || "") + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set("Content-Type", "text/html"); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts new file mode 100644 index 0000000..4a81ebe --- /dev/null +++ b/Foxnouns.Frontend/app/env.server.ts @@ -0,0 +1,3 @@ +import { env } from "node:process"; + +export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; diff --git a/Foxnouns.Frontend/app/lib/api/error.ts b/Foxnouns.Frontend/app/lib/api/error.ts new file mode 100644 index 0000000..02e871c --- /dev/null +++ b/Foxnouns.Frontend/app/lib/api/error.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +export type ApiError = { + status: number; + message: string; + code: ErrorCode; + 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", +} + +export type ValidationError = { + message: string; + min_length?: number; + max_length?: number; + actual_length?: number; + allowed_values?: any[]; + actual_value?: any; +}; diff --git a/Foxnouns.Frontend/src/lib/api/meta.ts b/Foxnouns.Frontend/app/lib/api/meta.ts similarity index 100% rename from Foxnouns.Frontend/src/lib/api/meta.ts rename to Foxnouns.Frontend/app/lib/api/meta.ts diff --git a/Foxnouns.Frontend/src/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts similarity index 75% rename from Foxnouns.Frontend/src/lib/api/user.ts rename to Foxnouns.Frontend/app/lib/api/user.ts index 3832872..babae31 100644 --- a/Foxnouns.Frontend/src/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -7,3 +7,7 @@ export type User = { avatar_url: string | null; links: string[]; }; + +export type UserSettings = { + dark_mode: boolean | null; +}; diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts new file mode 100644 index 0000000..144edbe --- /dev/null +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -0,0 +1,66 @@ +import { parse as parseCookie, serialize as serializeCookie } from "cookie"; +import { API_BASE } from "~/env.server"; +import { ApiError, ErrorCode } from "./api/error"; + +export type RequestParams = { + token?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + body?: any; + headers?: Record; +}; + +export default async function serverRequest( + method: string, + path: string, + params: RequestParams = {}, +) { + const url = `${API_BASE}/v2${path}`; + const resp = await fetch(url, { + method, + body: params.body ? JSON.stringify(params.body) : undefined, + headers: { + ...params.headers, + ...(params.token ? { Authorization: params.token } : {}), + "Content-Type": "application/json", + }, + }); + + if (resp.headers.get("Content-Type")?.indexOf("application/json") === -1) { + // If we don't get a JSON response, the server almost certainly encountered an internal error it couldn't recover from + // (that, or the reverse proxy, which should also be treated as a 500 error) + throw { + status: 500, + code: ErrorCode.InternalServerError, + message: "Internal server error", + } as ApiError; + } + + if (resp.status < 200 || resp.status >= 400) + throw (await resp.json()) as ApiError; + return (await resp.json()) as T; +} + +export function getCookie( + req: Request, + cookieName: string, +): string | undefined { + const header = req.headers.get("Cookie"); + if (!header) return undefined; + + const cookie = parseCookie(header); + return cookieName in cookie ? cookie[cookieName] : undefined; +} + +const YEAR = 365 * 86400; + +export const writeCookie = ( + cookieName: string, + value: string, + maxAge: number | undefined = YEAR, +) => + serializeCookie(cookieName, value, { + maxAge, + path: "/", + sameSite: "lax", + httpOnly: true, + }); diff --git a/Foxnouns.Frontend/app/lib/settings.server.ts b/Foxnouns.Frontend/app/lib/settings.server.ts new file mode 100644 index 0000000..c10096f --- /dev/null +++ b/Foxnouns.Frontend/app/lib/settings.server.ts @@ -0,0 +1,23 @@ +import { UserSettings } from "./api/user"; +import { getCookie } from "./request.server"; + +export default function getLocalSettings(req: Request): UserSettings { + const settings = { dark_mode: null } as UserSettings; + const theme = getCookie(req, "pronounscc-theme"); + + switch (theme) { + case "auto": + settings.dark_mode = null; + break; + case "light": + settings.dark_mode = false; + break; + case "dark": + settings.dark_mode = true; + break; + default: + break; + } + + return settings; +} diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx new file mode 100644 index 0000000..d1c62f3 --- /dev/null +++ b/Foxnouns.Frontend/app/root.tsx @@ -0,0 +1,85 @@ +import { + json, + Links, + Meta as MetaComponent, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, +} from "@remix-run/react"; +import { LoaderFunction } from "@remix-run/node"; +import SSRProvider from "react-bootstrap/SSRProvider"; + +import serverRequest, { getCookie, writeCookie } from "./lib/request.server"; +import Meta from "./lib/api/meta"; +import Navbar from "./components/nav/Navbar"; +import { User, UserSettings } from "./lib/api/user"; +import { ApiError, ErrorCode } from "./lib/api/error"; + +import "./app.scss"; +import getLocalSettings from "./lib/settings.server"; + +export const loader: LoaderFunction = async ({ request }) => { + const meta = await serverRequest("GET", "/meta"); + + const token = getCookie(request, "pronounscc-token"); + let setCookie = ""; + + let meUser: User | undefined; + let settings = getLocalSettings(request); + if (token) { + try { + const user = await serverRequest("GET", "/users/@me", { token }); + meUser = user; + + settings = await serverRequest( + "GET", + "/users/@me/settings", + { token }, + ); + } catch (e) { + // If we get an unauthorized error, clear the token, as it's not valid anymore. + if ((e as ApiError).code === ErrorCode.AuthenticationRequired) { + setCookie = writeCookie("pronounscc-token", token, 0); + } + } + } + + return json( + { meta, meUser, settings }, + { + headers: { "Set-Cookie": setCookie }, + }, + ); +}; + +export function Layout({ children }: { children: React.ReactNode }) { + const { settings } = useLoaderData(); + + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + const { meta, meUser, settings } = useLoaderData(); + + return ( + <> + + + + ); +} diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx new file mode 100644 index 0000000..a26fb49 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -0,0 +1,30 @@ +import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; +import { redirect, useLoaderData } from "@remix-run/react"; +import { User } from "~/lib/api/user"; +import serverRequest from "~/lib/request.server"; + +export const meta: MetaFunction = ({ data }) => { + const { user } = data!; + + return [{ title: `@${user.username} - pronouns.cc` }]; +}; + +export const loader: LoaderFunction = async ({ params }) => { + let username = params.username!; + if (!username.startsWith("@")) throw redirect(`/@${username}`); + username = username.substring("@".length); + + const user = await serverRequest("GET", `/users/${username}`); + + return json({ user }); +}; + +export default function UserPage() { + const { user } = useLoaderData(); + + return ( + <> + hello! this is the user page for @{user.username}. their ID is {user.id} + + ); +} diff --git a/Foxnouns.Frontend/app/routes/_index.tsx b/Foxnouns.Frontend/app/routes/_index.tsx new file mode 100644 index 0000000..5f6fc14 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/_index.tsx @@ -0,0 +1,45 @@ +import type { MetaFunction } from "@remix-run/node"; + +export const meta: MetaFunction = () => { + return [{ title: "pronouns.cc" }]; +}; + +export default function Index() { + return ( +
+

Welcome to Remix

+ +
+ ); +} diff --git a/Foxnouns.Frontend/app/routes/dark-mode/route.tsx b/Foxnouns.Frontend/app/routes/dark-mode/route.tsx new file mode 100644 index 0000000..1a20d86 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/dark-mode/route.tsx @@ -0,0 +1,31 @@ +import { ActionFunction } from "@remix-run/node"; +import { UserSettings } from "~/lib/api/user"; +import serverRequest, { getCookie, writeCookie } from "~/lib/request.server"; + +// Handles theme switching +// Remix itself handles redirecting back to the original page after the setting is set +export const action: ActionFunction = async ({ request }) => { + const body = await request.formData(); + const theme = (body.get("theme") as string | null) || "auto"; + + const token = getCookie(request, "pronounscc-token"); + if (token) { + await serverRequest("PATCH", "/users/@me/settings", { + token, + body: { + dark_mode: theme === "auto" ? null : theme === "dark" ? true : false, + }, + }); + + return new Response(null, { + status: 204, + }); + } + + return new Response(null, { + headers: { + "Set-Cookie": writeCookie("pronounscc-theme", theme), + }, + status: 204, + }); +}; diff --git a/Foxnouns.Frontend/eslint.config.js b/Foxnouns.Frontend/eslint.config.js deleted file mode 100644 index a351fa9..0000000 --- a/Foxnouns.Frontend/eslint.config.js +++ /dev/null @@ -1,33 +0,0 @@ -import js from '@eslint/js'; -import ts from 'typescript-eslint'; -import svelte from 'eslint-plugin-svelte'; -import prettier from 'eslint-config-prettier'; -import globals from 'globals'; - -/** @type {import('eslint').Linter.FlatConfig[]} */ -export default [ - 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/'] - } -]; diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 34152a0..673ec92 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -1,40 +1,58 @@ { - "name": "foxnouns.frontend", - "version": "0.0.1", + "name": "foxnouns-fe", "private": true, + "sideEffects": false, + "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", - "lint": "prettier --check . && eslint .", - "format": "prettier --write ." + "build": "remix vite:build", + "dev": "node ./server.js", + "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", + "start": "cross-env NODE_ENV=production node ./server.js", + "typecheck": "tsc", + "format": "prettier -w ." + }, + "dependencies": { + "@remix-run/express": "^2.11.2", + "@remix-run/node": "^2.11.2", + "@remix-run/react": "^2.11.2", + "@remix-run/serve": "^2.11.2", + "bootstrap": "^5.3.3", + "classnames": "^2.5.1", + "compression": "^1.7.4", + "cookie": "^0.6.0", + "cross-env": "^7.0.3", + "express": "^4.19.2", + "isbot": "^4.1.0", + "morgan": "^1.10.0", + "react": "^18.2.0", + "react-bootstrap": "^2.10.4", + "react-bootstrap-icons": "^1.11.4", + "react-dom": "^18.2.0" }, "devDependencies": { "@fontsource/firago": "^5.0.11", - "@sveltejs/adapter-node": "^5.0.1", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "@sveltestrap/sveltestrap": "^6.2.7", - "@tabler/icons-svelte": "^3.5.0", - "@types/eslint": "^8.56.7", - "bootstrap-icons": "^1.11.3", - "bulma": "^1.0.1", - "eslint": "^9.0.0", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.36.0", - "globals": "^15.0.0", - "prettier": "^3.1.1", - "prettier-plugin-svelte": "^3.1.2", - "sass": "^1.77.4", - "svelte": "^4.2.7", - "svelte-check": "^3.6.0", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "typescript-eslint": "^8.0.0-alpha.20", - "vite": "^5.0.3" + "@remix-run/dev": "^2.11.2", + "@types/compression": "^1.7.5", + "@types/cookie": "^0.6.0", + "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "eslint": "^8.38.0", + "eslint-import-resolver-typescript": "^3.6.1", + "eslint-plugin-import": "^2.28.1", + "eslint-plugin-jsx-a11y": "^6.7.1", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0", + "prettier": "^3.3.3", + "sass": "^1.78.0", + "typescript": "^5.1.6", + "vite": "^5.1.0", + "vite-tsconfig-paths": "^4.2.1" }, - "type": "module", - "dependencies": {} + "engines": { + "node": ">=20.0.0" + } } diff --git a/Foxnouns.Frontend/public/favicon.ico b/Foxnouns.Frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8830cf6821b354114848e6354889b8ecf6d2bc61 GIT binary patch literal 16958 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 EKbET!0{{R3 literal 0 HcmV?d00001 diff --git a/Foxnouns.Frontend/server.js b/Foxnouns.Frontend/server.js new file mode 100644 index 0000000..ed053cf --- /dev/null +++ b/Foxnouns.Frontend/server.js @@ -0,0 +1,52 @@ +import { env } from "node:process"; +import { createRequestHandler } from "@remix-run/express"; +import compression from "compression"; +import express from "express"; +import morgan from "morgan"; + +const viteDevServer = + env.NODE_ENV === "production" + ? undefined + : await import("vite").then((vite) => + vite.createServer({ + server: { middlewareMode: true }, + }), + ); + +const remixHandler = createRequestHandler({ + build: viteDevServer + ? () => viteDevServer.ssrLoadModule("virtual:remix/server-build") + : await import("./build/server/index.js"), +}); + +const app = express(); + +app.use(compression()); + +// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header +app.disable("x-powered-by"); + +// handle asset requests +if (viteDevServer) { + app.use(viteDevServer.middlewares); +} else { + // Vite fingerprints its assets so we can cache forever. + app.use( + "/assets", + express.static("build/client/assets", { immutable: true, maxAge: "1y" }), + ); +} + +// Everything else (like favicon.ico) is cached for an hour. You may want to be +// more aggressive with this caching. +app.use(express.static("build/client", { maxAge: "1h" })); + +app.use(morgan("tiny")); + +// handle SSR requests +app.all("*", remixHandler); + +const port = env.PORT || 3000; +app.listen(port, () => + console.log(`Express server listening at http://localhost:${port}`), +); diff --git a/Foxnouns.Frontend/src/app.d.ts b/Foxnouns.Frontend/src/app.d.ts deleted file mode 100644 index f7864c3..0000000 --- a/Foxnouns.Frontend/src/app.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// See https://kit.svelte.dev/docs/types#app -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - interface Locals { - token?: string; - } - // 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 53007b2..0000000 --- a/Foxnouns.Frontend/src/app.html +++ /dev/null @@ -1,19 +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 ca0a41a..0000000 --- a/Foxnouns.Frontend/src/app.scss +++ /dev/null @@ -1,8 +0,0 @@ -@use "bulma/sass" with ( - $family-primary: "FiraGO" -); - -@import "@fontsource/firago/400.css"; -@import "@fontsource/firago/400-italic.css"; -@import "@fontsource/firago/700.css"; -@import "bootstrap-icons/font/bootstrap-icons.css"; diff --git a/Foxnouns.Frontend/src/error.html b/Foxnouns.Frontend/src/error.html deleted file mode 100644 index 40fc538..0000000 --- a/Foxnouns.Frontend/src/error.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - Internal error occurred - - - -
- -

Internal error occurred

-

An internal error has occurred. Don't worry, it's (probably) not your fault.

-

- If this is the first time this is happening, try reloading the page. Otherwise, check the - status page for updates. -

-
-

- Status: %sveltekit.status%
- Error message: %sveltekit.error.message% -

- - diff --git a/Foxnouns.Frontend/src/hooks.server.ts b/Foxnouns.Frontend/src/hooks.server.ts deleted file mode 100644 index 59efc4b..0000000 --- a/Foxnouns.Frontend/src/hooks.server.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { PRIVATE_API_BASE } from "$env/static/private"; -import { PUBLIC_API_BASE } from "$env/static/public"; - -export async function handle({ event, resolve }) { - event.locals.token = event.cookies.get("pronounscc-token"); - return await resolve(event); -} - -export function handleFetch({ event, request, fetch }) { - if (request.url.startsWith(PUBLIC_API_BASE)) - request = new Request(request.url.replace(PUBLIC_API_BASE, PRIVATE_API_BASE), request); - if (event.locals.token) request.headers.set("Authorization", event.locals.token); - - return fetch(request); -} diff --git a/Foxnouns.Frontend/src/lib/api/auth.ts b/Foxnouns.Frontend/src/lib/api/auth.ts deleted file mode 100644 index 9e5b2d1..0000000 --- a/Foxnouns.Frontend/src/lib/api/auth.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { User } from "./user"; - -export type CallbackRequest = { - code: string; - state: string; -}; - -export type CallbackResponse = { - has_account: boolean; - ticket: string; - remote_username: string | null; -}; - -export type AuthResponse = { - user: User; - token: string; - expires_at: string; -}; diff --git a/Foxnouns.Frontend/src/lib/index.ts b/Foxnouns.Frontend/src/lib/index.ts deleted file mode 100644 index 856f2b6..0000000 --- a/Foxnouns.Frontend/src/lib/index.ts +++ /dev/null @@ -1 +0,0 @@ -// place files you want to import through the `$lib` alias in this folder. diff --git a/Foxnouns.Frontend/src/lib/nav/Dropdown.svelte b/Foxnouns.Frontend/src/lib/nav/Dropdown.svelte deleted file mode 100644 index 88ac724..0000000 --- a/Foxnouns.Frontend/src/lib/nav/Dropdown.svelte +++ /dev/null @@ -1,11 +0,0 @@ - - -
- - -
diff --git a/Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte b/Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte deleted file mode 100644 index be22533..0000000 --- a/Foxnouns.Frontend/src/lib/nav/DropdownItem.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - -{#if divider} - -{:else} - -{/if} diff --git a/Foxnouns.Frontend/src/lib/nav/Logo.svelte b/Foxnouns.Frontend/src/lib/nav/Logo.svelte deleted file mode 100644 index 9da9d99..0000000 --- a/Foxnouns.Frontend/src/lib/nav/Logo.svelte +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/Foxnouns.Frontend/src/lib/nav/Navbar.svelte b/Foxnouns.Frontend/src/lib/nav/Navbar.svelte deleted file mode 100644 index b467d02..0000000 --- a/Foxnouns.Frontend/src/lib/nav/Navbar.svelte +++ /dev/null @@ -1,66 +0,0 @@ - - - diff --git a/Foxnouns.Frontend/src/lib/request.ts b/Foxnouns.Frontend/src/lib/request.ts deleted file mode 100644 index 9536ca9..0000000 --- a/Foxnouns.Frontend/src/lib/request.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PUBLIC_API_BASE } from "$env/static/public"; - -export type RequestParams = { - token?: string; - body?: any; - headers?: Record; -}; - -/** - * Fetch a path from the API and parse the response. - * To make sure the request is authenticated in load functions, - * pass `fetch` from the request object into opts. - * - * @param fetchFn A function like `fetch`, such as from the `load` function - * @param method The HTTP method, i.e. GET, POST, PATCH - * @param path The path to request, minus the leading `/api/v2` - * @param params Extra options for this request - * @returns T - * @throws APIError - */ -export default async function request( - fetchFn: typeof fetch, - method: string, - path: string, - params: RequestParams = {}, -) { - const url = `${PUBLIC_API_BASE}/v2${path}`; - const resp = await fetchFn(url, { - method, - body: params.body ? JSON.stringify(params.body) : undefined, - headers: { - ...params.headers, - ...(params.token ? { Authorization: params.token } : {}), - "Content-Type": "application/json", - }, - }); - - if (resp.status < 200 || resp.status >= 400) throw await resp.json(); - return (await resp.json()) as T; -} - -/** - * Fetch a path from the API and discard the response. - * To make sure the request is authenticated in load functions, - * pass `fetch` from the request object into opts. - * - * @param fetchFn A function like `fetch`, such as from the `load` function - * @param method The HTTP method, i.e. GET, POST, PATCH - * @param path The path to request, minus the leading `/api/v2` - * @param params Extra options for this request - * @returns T - * @throws APIError - */ -export async function fastRequest( - fetchFn: typeof fetch, - method: string, - path: string, - params: RequestParams = {}, -): Promise { - const url = `${PUBLIC_API_BASE}/v2${path}`; - const resp = await fetchFn(url, { - method, - body: params.body ? JSON.stringify(params.body) : undefined, - headers: { - ...params.headers, - ...(params.token ? { Authorization: params.token } : {}), - "Content-Type": "application/json", - }, - }); - - if (resp.status < 200 || resp.status >= 400) throw await resp.json(); -} diff --git a/Foxnouns.Frontend/src/lib/store.ts b/Foxnouns.Frontend/src/lib/store.ts deleted file mode 100644 index d4449ad..0000000 --- a/Foxnouns.Frontend/src/lib/store.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { writable } from "svelte/store"; -import { browser } from "$app/environment"; - -const defaultThemeValue = "light"; -const initialThemeValue = browser - ? window.localStorage.getItem("pronouns-theme") ?? defaultThemeValue - : defaultThemeValue; - -export const themeStore = writable(initialThemeValue); diff --git a/Foxnouns.Frontend/src/routes/+error.svelte b/Foxnouns.Frontend/src/routes/+error.svelte deleted file mode 100644 index dd827ae..0000000 --- a/Foxnouns.Frontend/src/routes/+error.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -{#if $page.status === 404} -
- A very confused-looking fox -

Not found

-

Our foxes can't find the page you're looking for, sorry!

-
-{:else} - div.has-text-centered -{/if} diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts deleted file mode 100644 index a3b3a8a..0000000 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type Meta from "$lib/api/meta"; -import type { User } from "$lib/api/user"; -import request from "$lib/request"; - -export async function load({ fetch, locals }) { - const meta = await request(fetch, "GET", "/meta"); - let user: User | undefined; - try { - user = await request(fetch, "GET", "/users/@me"); - } catch {} - - return { meta, currentUser: user, token: locals.token }; -} diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte deleted file mode 100644 index 57271d1..0000000 --- a/Foxnouns.Frontend/src/routes/+layout.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte deleted file mode 100644 index fa8e254..0000000 --- a/Foxnouns.Frontend/src/routes/+page.svelte +++ /dev/null @@ -1,24 +0,0 @@ - - - - pronouns.cc - - -

Welcome to SvelteKit

-

Visit kit.svelte.dev to read the documentation

- -

- are you logged in? {data.currentUser !== undefined} - {#if data.currentUser} -
hello, {data.currentUser.username}! -
your ID: {data.currentUser.id} - {/if} -

- -

- stats: - {data.meta.users.total} users, {data.meta.members} members -

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 a803a26..0000000 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { error } from "@sveltejs/kit"; -import type { User } from "$lib/api/user"; -import request from "$lib/request"; - -export const load = async ({ params, fetch }) => { - try { - const user = await request(fetch, "GET", `/users/${params.username}`); - return { user }; - } catch { - error(404, { message: "User not found" }); - } -}; diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte deleted file mode 100644 index 639bb52..0000000 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - @{data.user.username} • pronouns.cc - - -

this is the user page for @{data.user.username}

diff --git a/Foxnouns.Frontend/src/routes/auth/login/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/login/+page.server.ts deleted file mode 100644 index d62e5d7..0000000 --- a/Foxnouns.Frontend/src/routes/auth/login/+page.server.ts +++ /dev/null @@ -1,12 +0,0 @@ -import request from "$lib/request.js"; - -type UrlsResponse = { - discord: string | null; - google: string | null; - tumblr: string | null; -}; - -export const load = async ({ fetch }) => { - const urls = await request(fetch, "POST", "/auth/urls"); - return { urls }; -}; diff --git a/Foxnouns.Frontend/src/routes/auth/login/+page.svelte b/Foxnouns.Frontend/src/routes/auth/login/+page.svelte deleted file mode 100644 index bead6b4..0000000 --- a/Foxnouns.Frontend/src/routes/auth/login/+page.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - -
-
-
-
-

Log in with email address

-
-
- -
- -
-
-
- -
- -
-
-
-
- -
-
- Sign up -
-
-
-
- {#if hasUrls} -
-

Log in with third-party provider

- -
- {/if} -
-
-
diff --git a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts deleted file mode 100644 index 051a0c0..0000000 --- a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.server.ts +++ /dev/null @@ -1,51 +0,0 @@ -import request from "$lib/request"; -import type { AuthResponse, CallbackResponse } from "$lib/api/auth"; - -export const load = async ({ fetch, url, cookies, parent }) => { - const data = await parent(); - if (data.user) { - return { loggedIn: true, token: data.token, user: data.user }; - } - - const resp = await request( - fetch, - "POST", - "/auth/discord/callback", - { - body: { - code: url.searchParams.get("code"), - state: url.searchParams.get("state"), - }, - }, - ); - - if ("token" in resp) { - const authResp = resp as AuthResponse; - cookies.set("pronounscc-token", authResp.token, { path: "/" }); - return { loggedIn: true, token: authResp.token, user: authResp.user }; - } - - const callbackResp = resp as CallbackResponse; - return { - loggedIn: false, - hasAccount: callbackResp.has_account, - ticket: resp.ticket, - remoteUsername: resp.remote_username, - }; -}; - -export const actions = { - register: async ({ cookies, request: req, fetch, locals }) => { - const data = await req.formData(); - const username = data.get("username"); - const ticket = data.get("ticket"); - - const resp = await request(fetch, "POST", "/auth/discord/register", { - body: { username, ticket }, - }); - cookies.set("pronounscc-token", resp.token, { path: "/" }); - locals.token = resp.token; - - return { token: resp.token, user: resp.user }; - }, -}; diff --git a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte b/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte deleted file mode 100644 index c7968b1..0000000 --- a/Foxnouns.Frontend/src/routes/auth/login/discord/+page.svelte +++ /dev/null @@ -1,79 +0,0 @@ - - -
- {#if form?.user} -

Successfully created account!

-

Welcome, @{form.user.username}!

-

- You should automatically be redirected to your profile in a few seconds. If you're not - redirected, please press the link above. -

- {:else if data.loggedIn} -

Successfully logged in!

-

You are now logged in as @{data.user?.username}.

-

- You should automatically be redirected to your profile in a few seconds. If you're not - redirected, please press the link above. -

- {:else} -

Finish signing up with a Discord account

-
-
- -
- -
-
-
- -
- -
-
- -
-
- -
-
-
- {/if} -
diff --git a/Foxnouns.Frontend/src/routes/neofox_confused_2048.png b/Foxnouns.Frontend/src/routes/neofox_confused_2048.png deleted file mode 100755 index 2813f170168d89d8d931beaa4be21b7ff74636a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 146626 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 z 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 b{oGPpTgqo1zdya``qj^BpGiM$ef$3bw1^eH diff --git a/Foxnouns.Frontend/static/default/512.webp b/Foxnouns.Frontend/static/default/512.webp deleted file mode 100644 index 3701f7d02a08806325fea477614df265fb65839d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43736 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 Y@gbNeZwVDKs+N>u!N1=V2x71R0K5_gS^xk5 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/logo.svg b/Foxnouns.Frontend/static/logo.svg deleted file mode 100644 index 9371e6c..0000000 --- a/Foxnouns.Frontend/static/logo.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/Foxnouns.Frontend/static/robots.txt b/Foxnouns.Frontend/static/robots.txt deleted file mode 100644 index 496b815..0000000 --- a/Foxnouns.Frontend/static/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -User-agent: * -Disallow: /@* -Disallow: /auth -Disallow: /settings -Disallow: /edit diff --git a/Foxnouns.Frontend/svelte.config.js b/Foxnouns.Frontend/svelte.config.js deleted file mode 100644 index 446cf25..0000000 --- a/Foxnouns.Frontend/svelte.config.js +++ /dev/null @@ -1,24 +0,0 @@ -import adapter from "@sveltejs/adapter-node"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), - - kit: { - // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. - // If your environment is not supported, or you settled on a specific environment, switch out the adapter. - // See https://kit.svelte.dev/docs/adapters for more information about adapters. - adapter: adapter(), - env: { - privatePrefix: "PRIVATE_", - }, - csrf: { - checkOrigin: false, - }, - }, -}; - -export default config; diff --git a/Foxnouns.Frontend/tsconfig.json b/Foxnouns.Frontend/tsconfig.json index fc93cbd..6f3de27 100644 --- a/Foxnouns.Frontend/tsconfig.json +++ b/Foxnouns.Frontend/tsconfig.json @@ -1,19 +1,32 @@ { - "extends": "./.svelte-kit/tsconfig.json", + "include": [ + "**/*.ts", + "**/*.tsx", + "**/.server/**/*.ts", + "**/.server/**/*.tsx", + "**/.client/**/*.ts", + "**/.client/**/*.tsx" + ], "compilerOptions": { - "allowJs": true, - "checkJs": true, + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["@remix-run/node", "vite/client"], + "isolatedModules": true, "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "Bundler", "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, + "target": "ES2022", "strict": true, - "moduleResolution": "bundler" + "allowJs": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Vite takes care of building everything, not tsc. + "noEmit": true } - // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias - // except $lib which is handled by https://kit.svelte.dev/docs/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 index bbf8c7d..e609580 100644 --- a/Foxnouns.Frontend/vite.config.ts +++ b/Foxnouns.Frontend/vite.config.ts @@ -1,6 +1,24 @@ -import { sveltekit } from '@sveltejs/kit/vite'; -import { defineConfig } from 'vite'; +import { vitePlugin as remix } from "@remix-run/dev"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; export default defineConfig({ - plugins: [sveltekit()] + plugins: [ + remix({ + future: { + v3_fetcherPersist: true, + v3_relativeSplatPath: true, + v3_throwAbortReason: true, + }, + }), + tsconfigPaths(), + ], + server: { + proxy: { + "/api": { + target: "http://localhost:5000", + changeOrigin: true, + }, + }, + }, }); diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index dcc8b33..f786a58 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.1": +"@ampproject/remapping@^2.2.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== @@ -10,120 +10,618 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@esbuild/aix-ppc64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" - integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== +"@babel/code-frame@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.7.tgz#882fd9e09e8ee324e496bd040401c6f046ef4465" + integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== + dependencies: + "@babel/highlight" "^7.24.7" + picocolors "^1.0.0" -"@esbuild/android-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" - integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== +"@babel/compat-data@^7.25.2": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.25.4.tgz#7d2a80ce229890edcf4cc259d4d696cb4dae2fcb" + integrity sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ== -"@esbuild/android-arm@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" - integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== +"@babel/core@^7.20.7", "@babel/core@^7.21.8", "@babel/core@^7.23.9": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.25.2.tgz#ed8eec275118d7613e77a352894cd12ded8eba77" + integrity sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.0" + "@babel/helper-compilation-targets" "^7.25.2" + "@babel/helper-module-transforms" "^7.25.2" + "@babel/helpers" "^7.25.0" + "@babel/parser" "^7.25.0" + "@babel/template" "^7.25.0" + "@babel/traverse" "^7.25.2" + "@babel/types" "^7.25.2" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" -"@esbuild/android-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" - integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== +"@babel/generator@^7.21.5", "@babel/generator@^7.25.0", "@babel/generator@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.25.6.tgz#0df1ad8cb32fe4d2b01d8bf437f153d19342a87c" + integrity sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw== + dependencies: + "@babel/types" "^7.25.6" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" -"@esbuild/darwin-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" - integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== +"@babel/helper-annotate-as-pure@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz#5373c7bc8366b12a033b4be1ac13a206c6656aab" + integrity sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg== + dependencies: + "@babel/types" "^7.24.7" -"@esbuild/darwin-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" - integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== +"@babel/helper-compilation-targets@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz#e1d9410a90974a3a5a66e84ff55ef62e3c02d06c" + integrity sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw== + dependencies: + "@babel/compat-data" "^7.25.2" + "@babel/helper-validator-option" "^7.24.8" + browserslist "^4.23.1" + lru-cache "^5.1.1" + semver "^6.3.1" -"@esbuild/freebsd-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" - integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== +"@babel/helper-create-class-features-plugin@^7.25.0": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.4.tgz#57eaf1af38be4224a9d9dd01ddde05b741f50e14" + integrity sha512-ro/bFs3/84MDgDmMwbcHgDa8/E6J3QKNTk4xJJnVeFtGE+tL0K26E3pNxhYz2b67fJpt7Aphw5XcploKXuCvCQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/helper-replace-supers" "^7.25.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/traverse" "^7.25.4" + semver "^6.3.1" -"@esbuild/freebsd-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" - integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== +"@babel/helper-member-expression-to-functions@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz#6155e079c913357d24a4c20480db7c712a5c3fb6" + integrity sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA== + dependencies: + "@babel/traverse" "^7.24.8" + "@babel/types" "^7.24.8" -"@esbuild/linux-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" - integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== +"@babel/helper-module-imports@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz#f2f980392de5b84c3328fc71d38bd81bbb83042b" + integrity sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@esbuild/linux-arm@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" - integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== +"@babel/helper-module-transforms@^7.24.8", "@babel/helper-module-transforms@^7.25.2": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz#ee713c29768100f2776edf04d4eb23b8d27a66e6" + integrity sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ== + dependencies: + "@babel/helper-module-imports" "^7.24.7" + "@babel/helper-simple-access" "^7.24.7" + "@babel/helper-validator-identifier" "^7.24.7" + "@babel/traverse" "^7.25.2" -"@esbuild/linux-ia32@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" - integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== +"@babel/helper-optimise-call-expression@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz#8b0a0456c92f6b323d27cfd00d1d664e76692a0f" + integrity sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A== + dependencies: + "@babel/types" "^7.24.7" -"@esbuild/linux-loong64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" - integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== +"@babel/helper-plugin-utils@^7.24.7", "@babel/helper-plugin-utils@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz#94ee67e8ec0e5d44ea7baeb51e571bd26af07878" + integrity sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg== -"@esbuild/linux-mips64el@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" - integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== +"@babel/helper-replace-supers@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz#ff44deac1c9f619523fe2ca1fd650773792000a9" + integrity sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.24.8" + "@babel/helper-optimise-call-expression" "^7.24.7" + "@babel/traverse" "^7.25.0" -"@esbuild/linux-ppc64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" - integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== +"@babel/helper-simple-access@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz#bcade8da3aec8ed16b9c4953b74e506b51b5edb3" + integrity sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@esbuild/linux-riscv64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" - integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== +"@babel/helper-skip-transparent-expression-wrappers@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz#5f8fa83b69ed5c27adc56044f8be2b3ea96669d9" + integrity sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ== + dependencies: + "@babel/traverse" "^7.24.7" + "@babel/types" "^7.24.7" -"@esbuild/linux-s390x@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" - integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== +"@babel/helper-string-parser@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz#5b3329c9a58803d5df425e5785865881a81ca48d" + integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== -"@esbuild/linux-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" - integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== +"@babel/helper-validator-identifier@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz#75b889cfaf9e35c2aaf42cf0d72c8e91719251db" + integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== -"@esbuild/netbsd-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" - integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== +"@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz#3725cdeea8b480e86d34df15304806a06975e33d" + integrity sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q== -"@esbuild/openbsd-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" - integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== +"@babel/helpers@^7.25.0": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.25.6.tgz#57ee60141829ba2e102f30711ffe3afab357cc60" + integrity sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q== + dependencies: + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.6" -"@esbuild/sunos-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" - integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== +"@babel/highlight@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.7.tgz#a05ab1df134b286558aae0ed41e6c5f731bf409d" + integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + dependencies: + "@babel/helper-validator-identifier" "^7.24.7" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" -"@esbuild/win32-arm64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" - integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== +"@babel/parser@^7.21.8", "@babel/parser@^7.25.0", "@babel/parser@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.25.6.tgz#85660c5ef388cbbf6e3d2a694ee97a38f18afe2f" + integrity sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q== + dependencies: + "@babel/types" "^7.25.6" -"@esbuild/win32-ia32@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" - integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== +"@babel/plugin-syntax-decorators@^7.22.10": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz#e4f8a0a8778ccec669611cd5aed1ed8e6e3a6fcf" + integrity sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" -"@esbuild/win32-x64@0.20.2": - version "0.20.2" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" - integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== +"@babel/plugin-syntax-jsx@^7.21.4", "@babel/plugin-syntax-jsx@^7.24.7": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz#39a1fa4a7e3d3d7f34e2acc6be585b718d30e02d" + integrity sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + +"@babel/plugin-syntax-typescript@^7.20.0", "@babel/plugin-syntax-typescript@^7.24.7": + version "7.25.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.4.tgz#04db9ce5a9043d9c635e75ae7969a2cd50ca97ff" + integrity sha512-uMOCoHVU52BsSWxPOMVv5qKRdeSlPuImUCB2dlPuBSU+W2/ROE7/Zg8F2Kepbk+8yBa68LlRKxO+xgEVWorsDg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.8" + +"@babel/plugin-transform-modules-commonjs@^7.24.7": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.8.tgz#ab6421e564b717cb475d6fff70ae7f103536ea3c" + integrity sha512-WHsk9H8XxRs3JXKWFiqtQebdh9b/pTk4EgueygFzYlTKAg0Ud985mSevdNjdXdFBATSKVJGQXP1tv6aGbssLKA== + dependencies: + "@babel/helper-module-transforms" "^7.24.8" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-simple-access" "^7.24.7" + +"@babel/plugin-transform-typescript@^7.24.7": + version "7.25.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz#237c5d10de6d493be31637c6b9fa30b6c5461add" + integrity sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.24.7" + "@babel/helper-create-class-features-plugin" "^7.25.0" + "@babel/helper-plugin-utils" "^7.24.8" + "@babel/helper-skip-transparent-expression-wrappers" "^7.24.7" + "@babel/plugin-syntax-typescript" "^7.24.7" + +"@babel/preset-typescript@^7.21.5": + version "7.24.7" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.24.7.tgz#66cd86ea8f8c014855671d5ea9a737139cbbfef1" + integrity sha512-SyXRe3OdWwIwalxDg5UtJnJQO+YPcTfwiIY2B0Xlddh9o7jpWLvv8X1RthIeDOxQ+O1ML5BLPCONToObyVQVuQ== + dependencies: + "@babel/helper-plugin-utils" "^7.24.7" + "@babel/helper-validator-option" "^7.24.7" + "@babel/plugin-syntax-jsx" "^7.24.7" + "@babel/plugin-transform-modules-commonjs" "^7.24.7" + "@babel/plugin-transform-typescript" "^7.24.7" + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.21.0", "@babel/runtime@^7.24.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" + integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.25.0": + version "7.25.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.0.tgz#e733dc3134b4fede528c15bc95e89cb98c52592a" + integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/parser" "^7.25.0" + "@babel/types" "^7.25.0" + +"@babel/traverse@^7.23.2", "@babel/traverse@^7.24.7", "@babel/traverse@^7.24.8", "@babel/traverse@^7.25.0", "@babel/traverse@^7.25.2", "@babel/traverse@^7.25.4": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.25.6.tgz#04fad980e444f182ecf1520504941940a90fea41" + integrity sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ== + dependencies: + "@babel/code-frame" "^7.24.7" + "@babel/generator" "^7.25.6" + "@babel/parser" "^7.25.6" + "@babel/template" "^7.25.0" + "@babel/types" "^7.25.6" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.22.5", "@babel/types@^7.24.7", "@babel/types@^7.24.8", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.25.6": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.25.6.tgz#893942ddb858f32ae7a004ec9d3a76b3463ef8e6" + integrity sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw== + dependencies: + "@babel/helper-string-parser" "^7.24.8" + "@babel/helper-validator-identifier" "^7.24.7" + to-fast-properties "^2.0.0" + +"@emotion/hash@^0.9.0": + version "0.9.2" + resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.2.tgz#ff9221b9f58b4dfe61e619a7788734bd63f6898b" + integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g== + +"@esbuild/aix-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz#d1bc06aedb6936b3b6d313bf809a5a40387d2b7f" + integrity sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA== + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz#b11bd4e4d031bb320c93c83c137797b2be5b403b" + integrity sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg== + +"@esbuild/android-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz#7ad65a36cfdb7e0d429c353e00f680d737c2aed4" + integrity sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.6.tgz#ac6b5674da2149997f6306b3314dae59bbe0ac26" + integrity sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g== + +"@esbuild/android-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.12.tgz#b0c26536f37776162ca8bde25e42040c203f2824" + integrity sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.6.tgz#18c48bf949046638fc209409ff684c6bb35a5462" + integrity sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ== + +"@esbuild/android-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.12.tgz#cb13e2211282012194d89bf3bfe7721273473b3d" + integrity sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz#b3fe19af1e4afc849a07c06318124e9c041e0646" + integrity sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA== + +"@esbuild/darwin-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz#cbee41e988020d4b516e9d9e44dd29200996275e" + integrity sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz#f4dacd1ab21e17b355635c2bba6a31eba26ba569" + integrity sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg== + +"@esbuild/darwin-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz#e37d9633246d52aecf491ee916ece709f9d5f4cd" + integrity sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz#ea4531aeda70b17cbe0e77b0c5c36298053855b4" + integrity sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg== + +"@esbuild/freebsd-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz#1ee4d8b682ed363b08af74d1ea2b2b4dbba76487" + integrity sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz#1896170b3c9f63c5e08efdc1f8abc8b1ed7af29f" + integrity sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q== + +"@esbuild/freebsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz#37a693553d42ff77cd7126764b535fb6cc28a11c" + integrity sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz#967dfb951c6b2de6f2af82e96e25d63747f75079" + integrity sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w== + +"@esbuild/linux-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz#be9b145985ec6c57470e0e051d887b09dddb2d4b" + integrity sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz#097a0ee2be39fed3f37ea0e587052961e3bcc110" + integrity sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw== + +"@esbuild/linux-arm@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz#207ecd982a8db95f7b5279207d0ff2331acf5eef" + integrity sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz#a38a789d0ed157495a6b5b4469ec7868b59e5278" + integrity sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ== + +"@esbuild/linux-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz#d0d86b5ca1562523dc284a6723293a52d5860601" + integrity sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz#ae3983d0fb4057883c8246f57d2518c2af7cf2ad" + integrity sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ== + +"@esbuild/linux-loong64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz#9a37f87fec4b8408e682b528391fa22afd952299" + integrity sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz#15fbbe04648d944ec660ee5797febdf09a9bd6af" + integrity sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA== + +"@esbuild/linux-mips64el@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz#4ddebd4e6eeba20b509d8e74c8e30d8ace0b89ec" + integrity sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz#38210094e8e1a971f2d1fd8e48462cc65f15ef19" + integrity sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg== + +"@esbuild/linux-ppc64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz#adb67dadb73656849f63cd522f5ecb351dd8dee8" + integrity sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz#bc3c66d5578c3b9951a6ed68763f2a6856827e4a" + integrity sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ== + +"@esbuild/linux-riscv64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz#11bc0698bf0a2abf8727f1c7ace2112612c15adf" + integrity sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz#d7ba7af59285f63cfce6e5b7f82a946f3e6d67fc" + integrity sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q== + +"@esbuild/linux-s390x@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz#e86fb8ffba7c5c92ba91fc3b27ed5a70196c3cc8" + integrity sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz#ba51f8760a9b9370a2530f98964be5f09d90fed0" + integrity sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw== + +"@esbuild/linux-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz#5f37cfdc705aea687dfe5dfbec086a05acfe9c78" + integrity sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz#e84d6b6fdde0261602c1e56edbb9e2cb07c211b9" + integrity sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A== + +"@esbuild/netbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz#29da566a75324e0d0dd7e47519ba2f7ef168657b" + integrity sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz#cf4b9fb80ce6d280a673d54a731d9c661f88b083" + integrity sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw== + +"@esbuild/openbsd-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz#306c0acbdb5a99c95be98bdd1d47c916e7dc3ff0" + integrity sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz#a6838e246079b24d962b9dcb8d208a3785210a73" + integrity sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw== + +"@esbuild/sunos-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz#0933eaab9af8b9b2c930236f62aae3fc593faf30" + integrity sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz#ace0186e904d109ea4123317a3ba35befe83ac21" + integrity sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg== + +"@esbuild/win32-arm64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz#773bdbaa1971b36db2f6560088639ccd1e6773ae" + integrity sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz#7fb3f6d4143e283a7f7dffc98a6baf31bb365c7e" + integrity sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg== + +"@esbuild/win32-ia32@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz#000516cad06354cc84a73f0943a4aa690ef6fd67" + integrity sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.17.6": + version "0.17.6" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz#563ff4277f1230a006472664fa9278a83dd124da" + integrity sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA== + +"@esbuild/win32-x64@0.19.12": + version "0.19.12" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz#c57c8afbb4054a3ab8317591a0b7320360b444ae" + integrity sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" @@ -132,59 +630,66 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": - version "4.10.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.1.tgz#361461e5cb3845d874e61731c11cfedd664d83a0" - integrity sha512-Zm2NGpWELsQAD1xsJzGQpYfvICSsFkEpU0jxBjfdC6uNEWXcHnfs9hScFWtXVDVl+rBQJGrl4g1vcKIejpH9dA== +"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": + version "4.11.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.11.0.tgz#b0ffd0312b4a3fd2d6f77237e7248a5ad3a680ae" + integrity sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A== -"@eslint/config-array@^0.15.1": - version "0.15.1" - resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.15.1.tgz#1fa78b422d98f4e7979f2211a1fde137e26c7d61" - integrity sha512-K4gzNq+yymn/EVsXYmf+SBcBro8MTf+aXJZUphM96CdzUEr+ClGDvAbpmaEK+cGVigVXIgs9gNmvHAlrzzY5JQ== - dependencies: - "@eslint/object-schema" "^2.1.3" - debug "^4.3.1" - minimatch "^3.0.5" - -"@eslint/eslintrc@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" - integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^10.0.1" - globals "^14.0.0" + espree "^9.6.0" + globals "^13.19.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" -"@eslint/js@9.4.0": - version "9.4.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.4.0.tgz#96a2edd37ec0551ce5f9540705be23951c008a0c" - integrity sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg== - -"@eslint/object-schema@^2.1.3": - version "2.1.3" - resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.3.tgz#e65ae80ee2927b4fd8c5c26b15ecacc2b2a6cc2a" - integrity sha512-HAbhAYKfsAC2EkTqve00ibWIZlaU74Z1EHwAjYr4PXF0YU2VEA1zSIKSSpKszRLRWwHzzRZXvK632u+uXzvsvw== +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== "@fontsource/firago@^5.0.11": version "5.0.11" resolved "https://registry.yarnpkg.com/@fontsource/firago/-/firago-5.0.11.tgz#8fe3c8b47cc1d8148bc50c80189ed3aac8555cb7" integrity sha512-XfFsLxSFMTbJTN+94yFTJyuFGmoxtykt+6rL0fj9unCeXslllirpH6KetIlbZO73NzTUmKYRvtOJdOgVbBGtaQ== +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/retry@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" - integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" "@jridgewell/gen-mapping@^0.3.5": version "0.3.5" @@ -205,12 +710,12 @@ resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== -"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24": +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": version "0.3.25" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== @@ -218,6 +723,34 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@jspm/core@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@jspm/core/-/core-2.0.1.tgz#3f08c59c60a5f5e994523ed6b0b665ec80adc94e" + integrity sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw== + +"@mdx-js/mdx@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-2.3.0.tgz#d65d8c3c28f3f46bb0e7cb3bf7613b39980671a9" + integrity sha512-jLuwRlz8DQfQNiUCJR50Y09CGPq3fLtmtUQfVrj79E0JWu3dvsVcxVIcfhR5h0iXu+/z++zDrYeiJqifRynJkA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/mdx" "^2.0.0" + estree-util-build-jsx "^2.0.0" + estree-util-is-identifier-name "^2.0.0" + estree-util-to-js "^1.1.0" + estree-walker "^3.0.0" + hast-util-to-estree "^2.0.0" + markdown-extensions "^1.0.0" + periscopic "^3.0.0" + remark-mdx "^2.0.0" + remark-parse "^10.0.0" + remark-rehype "^10.0.0" + unified "^10.0.0" + unist-util-position-from-estree "^1.0.0" + unist-util-stringify-position "^3.0.0" + unist-util-visit "^4.0.0" + vfile "^5.0.0" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -239,326 +772,730 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@polka/url@^1.0.0-next.24": - version "1.0.0-next.25" - resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817" - integrity sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ== +"@nolyfill/is-core-module@1.0.39": + version "1.0.39" + resolved "https://registry.yarnpkg.com/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz#3dc35ba0f1e66b403c00b39344f870298ebb1c8e" + integrity sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== -"@popperjs/core@^2.11.8": +"@npmcli/fs@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.1.tgz#59cdaa5adca95d135fc00f2bb53f5771575ce726" + integrity sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg== + dependencies: + semver "^7.3.5" + +"@npmcli/git@^4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-4.1.0.tgz#ab0ad3fd82bc4d8c1351b6c62f0fa56e8fe6afa6" + integrity sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ== + dependencies: + "@npmcli/promise-spawn" "^6.0.0" + lru-cache "^7.4.4" + npm-pick-manifest "^8.0.0" + proc-log "^3.0.0" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^3.0.0" + +"@npmcli/package-json@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/package-json/-/package-json-4.0.1.tgz#1a07bf0e086b640500791f6bf245ff43cc27fa37" + integrity sha512-lRCEGdHZomFsURroh522YvA/2cVb9oPIJrjHanCJZkiasz1BzcnLr3tBJhlV7S86MBJBuAQ33is2D60YitZL2Q== + dependencies: + "@npmcli/git" "^4.1.0" + glob "^10.2.2" + hosted-git-info "^6.1.1" + json-parse-even-better-errors "^3.0.0" + normalize-package-data "^5.0.0" + proc-log "^3.0.0" + semver "^7.5.3" + +"@npmcli/promise-spawn@^6.0.0": + version "6.0.2" + resolved "https://registry.yarnpkg.com/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz#c8bc4fa2bd0f01cb979d8798ba038f314cfa70f2" + integrity sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg== + dependencies: + which "^3.0.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@popperjs/core@^2.11.6": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== -"@rollup/plugin-commonjs@^25.0.7": - version "25.0.8" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz#c77e608ab112a666b7f2a6bea625c73224f7dd34" - integrity sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A== +"@react-aria/ssr@^3.5.0": + version "3.9.5" + resolved "https://registry.yarnpkg.com/@react-aria/ssr/-/ssr-3.9.5.tgz#775d84f51f90934ff51ae74eeba3728daac1a381" + integrity sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ== dependencies: - "@rollup/pluginutils" "^5.0.1" - commondir "^1.0.1" - estree-walker "^2.0.2" - glob "^8.0.3" - is-reference "1.2.1" - magic-string "^0.30.3" + "@swc/helpers" "^0.5.0" -"@rollup/plugin-json@^6.1.0": - version "6.1.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.1.0.tgz#fbe784e29682e9bb6dee28ea75a1a83702e7b805" - integrity sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA== +"@remix-run/dev@^2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@remix-run/dev/-/dev-2.11.2.tgz#e5d2f7b0f7012b9a83d5bf3ed1c74c54a7ec87fb" + integrity sha512-9DGb2UOIO4jOdws04Z+KmCeEBqbP36XvJZdcd4w16wDGI0I1ZY1c5ro58tB/7zPwN40s9MD9UzCYm6P+EkdeAg== dependencies: - "@rollup/pluginutils" "^5.1.0" - -"@rollup/plugin-node-resolve@^15.2.3": - version "15.2.3" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz#e5e0b059bd85ca57489492f295ce88c2d4b0daf9" - integrity sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ== - dependencies: - "@rollup/pluginutils" "^5.0.1" - "@types/resolve" "1.20.2" - deepmerge "^4.2.2" - is-builtin-module "^3.2.1" - is-module "^1.0.0" - resolve "^1.22.1" - -"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.1.0": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" - integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^2.0.2" + "@babel/core" "^7.21.8" + "@babel/generator" "^7.21.5" + "@babel/parser" "^7.21.8" + "@babel/plugin-syntax-decorators" "^7.22.10" + "@babel/plugin-syntax-jsx" "^7.21.4" + "@babel/preset-typescript" "^7.21.5" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.22.5" + "@mdx-js/mdx" "^2.3.0" + "@npmcli/package-json" "^4.0.1" + "@remix-run/node" "2.11.2" + "@remix-run/router" "1.19.1" + "@remix-run/server-runtime" "2.11.2" + "@types/mdx" "^2.0.5" + "@vanilla-extract/integration" "^6.2.0" + arg "^5.0.1" + cacache "^17.1.3" + chalk "^4.1.2" + chokidar "^3.5.1" + cross-spawn "^7.0.3" + dotenv "^16.0.0" + es-module-lexer "^1.3.1" + esbuild "0.17.6" + esbuild-plugins-node-modules-polyfill "^1.6.0" + execa "5.1.1" + exit-hook "2.2.1" + express "^4.19.2" + fs-extra "^10.0.0" + get-port "^5.1.1" + gunzip-maybe "^1.4.2" + jsesc "3.0.2" + json5 "^2.2.2" + lodash "^4.17.21" + lodash.debounce "^4.0.8" + minimatch "^9.0.0" + ora "^5.4.1" + picocolors "^1.0.0" picomatch "^2.3.1" - -"@rollup/rollup-android-arm-eabi@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" - integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== - -"@rollup/rollup-android-arm64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" - integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== - -"@rollup/rollup-darwin-arm64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" - integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== - -"@rollup/rollup-darwin-x64@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" - integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== - -"@rollup/rollup-linux-arm-gnueabihf@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" - integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== - -"@rollup/rollup-linux-arm-musleabihf@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" - integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== - -"@rollup/rollup-linux-arm64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" - integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== - -"@rollup/rollup-linux-arm64-musl@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" - integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== - -"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" - integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== - -"@rollup/rollup-linux-riscv64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" - integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== - -"@rollup/rollup-linux-s390x-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" - integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== - -"@rollup/rollup-linux-x64-gnu@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" - integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== - -"@rollup/rollup-linux-x64-musl@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" - integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== - -"@rollup/rollup-win32-arm64-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" - integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== - -"@rollup/rollup-win32-ia32-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" - integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== - -"@rollup/rollup-win32-x64-msvc@4.18.0": - version "4.18.0" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" - integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== - -"@sveltejs/adapter-node@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@sveltejs/adapter-node/-/adapter-node-5.0.1.tgz#b5ed2d5897d06589e6266095a531b2ba8fbe4a15" - integrity sha512-eYdmxdUWMW+dad1JfMsWBPY2vjXz9eE+52A2AQnXPScPJlIxIVk5mmbaEEzrZivLfO2wEcLTZ5vdC03W69x+iA== - dependencies: - "@rollup/plugin-commonjs" "^25.0.7" - "@rollup/plugin-json" "^6.1.0" - "@rollup/plugin-node-resolve" "^15.2.3" - rollup "^4.9.5" - -"@sveltejs/kit@^2.0.0": - version "2.5.10" - resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-2.5.10.tgz#c52e54508d96af5db8a92f2a81250bd81d0c409b" - integrity sha512-OqoyTmFG2cYmCFAdBfW+Qxbg8m23H4dv6KqwEt7ofr/ROcfcIl3Z/VT56L22H9f0uNZyr+9Bs1eh2gedOCK9kA== - dependencies: - "@types/cookie" "^0.6.0" - cookie "^0.6.0" - devalue "^5.0.0" - esm-env "^1.0.0" - import-meta-resolve "^4.1.0" - kleur "^4.1.5" - magic-string "^0.30.5" - mrmime "^2.0.0" - sade "^1.8.1" + pidtree "^0.6.0" + postcss "^8.4.19" + postcss-discard-duplicates "^5.1.0" + postcss-load-config "^4.0.1" + postcss-modules "^6.0.0" + prettier "^2.7.1" + pretty-ms "^7.0.1" + react-refresh "^0.14.0" + remark-frontmatter "4.0.1" + remark-mdx-frontmatter "^1.0.1" + semver "^7.3.7" set-cookie-parser "^2.6.0" - sirv "^2.0.4" - tiny-glob "^0.2.9" + tar-fs "^2.1.1" + tsconfig-paths "^4.0.0" + ws "^7.4.5" -"@sveltejs/vite-plugin-svelte-inspector@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz#116ba2b73be43c1d7d93de749f37becc7e45bb8c" - integrity sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg== +"@remix-run/express@2.11.2", "@remix-run/express@^2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@remix-run/express/-/express-2.11.2.tgz#c23eacbc9b962694430f4a1aa8970c9480648a3c" + integrity sha512-ebyvHJKRBDgQGNBMxsILt21IwMTjGxQxlr0VNxRJo5rNd5CcuULpx/PPmsBc1gsc/Jx9aUXpT7a9l0UEOc6+jw== dependencies: - debug "^4.3.4" + "@remix-run/node" "2.11.2" -"@sveltejs/vite-plugin-svelte@^3.0.0": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz#e71bb0631ca40a3a1d272315beaff9bdd5482841" - integrity sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A== +"@remix-run/node@2.11.2", "@remix-run/node@^2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@remix-run/node/-/node-2.11.2.tgz#c27c8baeb9f361e1edd3cb2ba48991743f30d654" + integrity sha512-gRNFM61EOYWNmYgf+pvBt6MrirWlkDz1G6RQsJNowtRqbYoy05AdDe5HiHGF5w8ZMAZVeXnZiwbL0Nt7ykYBCA== dependencies: - "@sveltejs/vite-plugin-svelte-inspector" "^2.1.0" - debug "^4.3.4" - deepmerge "^4.3.1" - kleur "^4.1.5" - magic-string "^0.30.10" - svelte-hmr "^0.16.0" - vitefu "^0.2.5" + "@remix-run/server-runtime" "2.11.2" + "@remix-run/web-fetch" "^4.4.2" + "@web3-storage/multipart-parser" "^1.0.0" + cookie-signature "^1.1.0" + source-map-support "^0.5.21" + stream-slice "^0.1.2" + undici "^6.11.1" -"@sveltestrap/sveltestrap@^6.2.7": - version "6.2.7" - resolved "https://registry.yarnpkg.com/@sveltestrap/sveltestrap/-/sveltestrap-6.2.7.tgz#5b2736cbee2db973f02b09d2e9d5bf819418f1f9" - integrity sha512-WwLLfAFUb42BGuRrf3Vbct30bQMzlEMMipN/MfxhjuLTmLQeW9muVJfPyvjtWS+mY+RjkSCoHvAp/ZobP1NLlQ== +"@remix-run/react@^2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@remix-run/react/-/react-2.11.2.tgz#193442d3f72b3375537d68e8862d4559f5b27fbd" + integrity sha512-SjjuK3aD/9wnIC5r0ZBNCpVSwEwt67YOQM7DCXhHiS8BtCvAxWEC4k4t8CvO9IwBG0gczqxzSqASH7U1RVtWqw== dependencies: - "@popperjs/core" "^2.11.8" + "@remix-run/router" "1.19.1" + "@remix-run/server-runtime" "2.11.2" + react-router "6.26.1" + react-router-dom "6.26.1" + turbo-stream "2.3.0" -"@tabler/icons-svelte@^3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@tabler/icons-svelte/-/icons-svelte-3.5.0.tgz#02efede4ce0ed680e0835878c6c02cd63daf9d9a" - integrity sha512-mc5ardGEM7cnUA4/q6Mz5bmW9B6t28vAAOf4Wl6+KXiTwG00EjImfnIr3pS3Ihi9sFIiXvJPYRl4H5IHlgvJvQ== +"@remix-run/router@1.19.1": + version "1.19.1" + resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.19.1.tgz#984771bfd1de2715f42394c87fb716c1349e014f" + integrity sha512-S45oynt/WH19bHbIXjtli6QmwNYvaz+vtnubvNpNDvUOoA/OWh6j1OikIP3G+v5GHdxyC6EXoChG3HgYGEUfcg== + +"@remix-run/serve@^2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@remix-run/serve/-/serve-2.11.2.tgz#246bf98e8a490cd43bd927114d9bf907b4d9582b" + integrity sha512-f1ETbCAlkSO3kg1zcQyLVHxI2r1TXqV2nfPgX/5+7QmA1dEHJD3OhvSmbvopwSMSfi1jzuyRbJo04yK4aJ8ztg== dependencies: - "@tabler/icons" "3.5.0" + "@remix-run/express" "2.11.2" + "@remix-run/node" "2.11.2" + chokidar "^3.5.3" + compression "^1.7.4" + express "^4.19.2" + get-port "5.1.1" + morgan "^1.10.0" + source-map-support "^0.5.21" -"@tabler/icons@3.5.0": - version "3.5.0" - resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.5.0.tgz#29d0dbf100c8cb392dd64f1fe8efdcfcd1f57e44" - integrity sha512-I53dC3ZSHQ2MZFGvDYJelfXm91L2bTTixS4w5jTAulLhHbCZso5Bih4Rk/NYZxlngLQMKHvEYwZQ+6w/WluKiA== +"@remix-run/server-runtime@2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@remix-run/server-runtime/-/server-runtime-2.11.2.tgz#0eb5ed30f3413049f941b1bc6eab78f53c866ced" + integrity sha512-abG6ENj0X3eHqDxqO2thWM2NSEiPnqyt58z1BbiQCv+t8g0Zuqd5QHbB4wzdNvfS0vKhg+jJiigcJneAc4sZzw== + dependencies: + "@remix-run/router" "1.19.1" + "@types/cookie" "^0.6.0" + "@web3-storage/multipart-parser" "^1.0.0" + cookie "^0.6.0" + set-cookie-parser "^2.4.8" + source-map "^0.7.3" + turbo-stream "2.3.0" + +"@remix-run/web-blob@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-blob/-/web-blob-3.1.0.tgz#e0c669934c1eb6028960047e57a13ed38bbfb434" + integrity sha512-owGzFLbqPH9PlKb8KvpNJ0NO74HWE2euAn61eEiyCXX/oteoVzTVSN8mpLgDjaxBf2btj5/nUllSUgpyd6IH6g== + dependencies: + "@remix-run/web-stream" "^1.1.0" + web-encoding "1.1.5" + +"@remix-run/web-fetch@^4.4.2": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@remix-run/web-fetch/-/web-fetch-4.4.2.tgz#ce7aedef72cc26e15060e8cf84674029f92809b6" + integrity sha512-jgKfzA713/4kAW/oZ4bC3MoLWyjModOVDjFPNseVqcJKSafgIscrYL9G50SurEYLswPuoU3HzSbO0jQCMYWHhA== + dependencies: + "@remix-run/web-blob" "^3.1.0" + "@remix-run/web-file" "^3.1.0" + "@remix-run/web-form-data" "^3.1.0" + "@remix-run/web-stream" "^1.1.0" + "@web3-storage/multipart-parser" "^1.0.0" + abort-controller "^3.0.0" + data-uri-to-buffer "^3.0.1" + mrmime "^1.0.0" + +"@remix-run/web-file@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-file/-/web-file-3.1.0.tgz#07219021a2910e90231bc30ca1ce693d0e9d3825" + integrity sha512-dW2MNGwoiEYhlspOAXFBasmLeYshyAyhIdrlXBi06Duex5tDr3ut2LFKVj7tyHLmn8nnNwFf1BjNbkQpygC2aQ== + dependencies: + "@remix-run/web-blob" "^3.1.0" + +"@remix-run/web-form-data@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-form-data/-/web-form-data-3.1.0.tgz#47f9ad8ce8bf1c39ed83eab31e53967fe8e3df6a" + integrity sha512-NdeohLMdrb+pHxMQ/Geuzdp0eqPbea+Ieo8M8Jx2lGC6TBHsgHzYcBvr0LyPdPVycNRDEpWpiDdCOdCryo3f9A== + dependencies: + web-encoding "1.1.5" + +"@remix-run/web-stream@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@remix-run/web-stream/-/web-stream-1.1.0.tgz#b93a8f806c2c22204930837c44d81fdedfde079f" + integrity sha512-KRJtwrjRV5Bb+pM7zxcTJkhIqWWSy+MYsIxHK+0m5atcznsf15YwUBWHWulZerV2+vvHH1Lp1DD7pw6qKW8SgA== + dependencies: + web-streams-polyfill "^3.1.1" + +"@restart/hooks@^0.4.9": + version "0.4.16" + resolved "https://registry.yarnpkg.com/@restart/hooks/-/hooks-0.4.16.tgz#95ae8ac1cc7e2bd4fed5e39800ff85604c6d59fb" + integrity sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w== + dependencies: + dequal "^2.0.3" + +"@restart/ui@^1.6.9": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@restart/ui/-/ui-1.8.0.tgz#3e8d80822b5fbef0576f94acda51d7da9e79e005" + integrity sha512-xJEOXUOTmT4FngTmhdjKFRrVVF0hwCLNPdatLCHkyS4dkiSK12cEu1Y0fjxktjJrdst9jJIc5J6ihMJCoWEN/g== + dependencies: + "@babel/runtime" "^7.21.0" + "@popperjs/core" "^2.11.6" + "@react-aria/ssr" "^3.5.0" + "@restart/hooks" "^0.4.9" + "@types/warning" "^3.0.0" + dequal "^2.0.3" + dom-helpers "^5.2.0" + uncontrollable "^8.0.1" + warning "^4.0.3" + +"@rollup/rollup-android-arm-eabi@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.2.tgz#0412834dc423d1ff7be4cb1fc13a86a0cd262c11" + integrity sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg== + +"@rollup/rollup-android-arm64@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.2.tgz#baf1a014b13654f3b9e835388df9caf8c35389cb" + integrity sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA== + +"@rollup/rollup-darwin-arm64@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.2.tgz#0a2c364e775acdf1172fe3327662eec7c46e55b1" + integrity sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q== + +"@rollup/rollup-darwin-x64@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.2.tgz#a972db75890dfab8df0da228c28993220a468c42" + integrity sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w== + +"@rollup/rollup-linux-arm-gnueabihf@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.2.tgz#1609d0630ef61109dd19a278353e5176d92e30a1" + integrity sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w== + +"@rollup/rollup-linux-arm-musleabihf@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.2.tgz#3c1dca5f160aa2e79e4b20ff6395eab21804f266" + integrity sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w== + +"@rollup/rollup-linux-arm64-gnu@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.2.tgz#c2fe376e8b04eafb52a286668a8df7c761470ac7" + integrity sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw== + +"@rollup/rollup-linux-arm64-musl@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.2.tgz#e62a4235f01e0f66dbba587c087ca6db8008ec80" + integrity sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w== + +"@rollup/rollup-linux-powerpc64le-gnu@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.2.tgz#24b3457e75ee9ae5b1c198bd39eea53222a74e54" + integrity sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ== + +"@rollup/rollup-linux-riscv64-gnu@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.2.tgz#38edfba9620fe2ca8116c97e02bd9f2d606bde09" + integrity sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg== + +"@rollup/rollup-linux-s390x-gnu@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.2.tgz#a3bfb8bc5f1e802f8c76cff4a4be2e9f9ac36a18" + integrity sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ== + +"@rollup/rollup-linux-x64-gnu@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.2.tgz#0dadf34be9199fcdda44b5985a086326344f30ad" + integrity sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw== + +"@rollup/rollup-linux-x64-musl@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.2.tgz#7b7deddce240400eb87f2406a445061b4fed99a8" + integrity sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg== + +"@rollup/rollup-win32-arm64-msvc@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.2.tgz#a0ca0c5149c2cfb26fab32e6ba3f16996fbdb504" + integrity sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ== + +"@rollup/rollup-win32-ia32-msvc@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.2.tgz#aae2886beec3024203dbb5569db3a137bc385f8e" + integrity sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw== + +"@rollup/rollup-win32-x64-msvc@4.21.2": + version "4.21.2" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.2.tgz#e4291e3c1bc637083f87936c333cdbcad22af63b" + integrity sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA== + +"@rtsao/scc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@rtsao/scc/-/scc-1.1.0.tgz#927dd2fae9bc3361403ac2c7a00c32ddce9ad7e8" + integrity sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== + +"@swc/helpers@^0.5.0": + version "0.5.13" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.13.tgz#33e63ff3cd0cade557672bd7888a39ce7d115a8c" + integrity sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w== + dependencies: + tslib "^2.4.0" + +"@types/acorn@^4.0.0": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" + integrity sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ== + dependencies: + "@types/estree" "*" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/compression@^1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@types/compression/-/compression-1.7.5.tgz#0f80efef6eb031be57b12221c4ba6bc3577808f7" + integrity sha512-AAQvK5pxMpaT+nDvhHrsBhLSYG5yQdtkaJE1WYieSNY2mVFKAgmU4ks65rkZD5oqnGCFLyQpUr1CqI4DmUMyDg== + dependencies: + "@types/express" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" "@types/cookie@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5" integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA== -"@types/eslint@^8.56.7": - version "8.56.10" - resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" - integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ== +"@types/debug@^4.0.0": + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== + dependencies: + "@types/ms" "*" + +"@types/estree-jsx@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree-jsx/-/estree-jsx-1.0.5.tgz#858a88ea20f34fe65111f005a689fa1ebf70dc18" + integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg== dependencies: "@types/estree" "*" - "@types/json-schema" "*" -"@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0", "@types/estree@^1.0.1": +"@types/estree@*", "@types/estree@1.0.5", "@types/estree@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== -"@types/json-schema@*": +"@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/hast@^2.0.0": + version "2.3.10" + resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.10.tgz#5c9d9e0b304bbb8879b857225c5ebab2d81d7643" + integrity sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw== + dependencies: + "@types/unist" "^2" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/json-schema@^7.0.12": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== -"@types/pug@^2.0.6": - version "2.0.10" - resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.10.tgz#52f8dbd6113517aef901db20b4f3fca543b88c1f" - integrity sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA== +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/resolve@1.20.2": - version "1.20.2" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" - integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== - -"@typescript-eslint/eslint-plugin@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.29.tgz#63fde3a80ba7936b07dc5a7d10c3a82d42e4be36" - integrity sha512-lEjQc/jfr3MePgq0mxbAIvAvzInotE48L8bAwfoHkdwBTJxpwN9ywjEvgBIZ8dRssvdm49stItPFazDnZnMWMA== +"@types/mdast@^3.0.0": + version "3.0.15" + resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.15.tgz#49c524a263f30ffa28b71ae282f813ed000ab9f5" + integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ== dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.0.0-alpha.29" - "@typescript-eslint/type-utils" "8.0.0-alpha.29" - "@typescript-eslint/utils" "8.0.0-alpha.29" - "@typescript-eslint/visitor-keys" "8.0.0-alpha.29" + "@types/unist" "^2" + +"@types/mdx@^2.0.0", "@types/mdx@^2.0.5": + version "2.0.13" + resolved "https://registry.yarnpkg.com/@types/mdx/-/mdx-2.0.13.tgz#68f6877043d377092890ff5b298152b0a21671bd" + integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/morgan@^1.9.9": + version "1.9.9" + resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.9.tgz#d60dec3979e16c203a000159daa07d3fb7270d7f" + integrity sha512-iRYSDKVaC6FkGSpEVVIvrRGw0DfJMiQzIn3qr2G5B3C//AWkulhXgaBd7tS9/J79GWSYMTHGs7PfI5b3Y8m+RQ== + dependencies: + "@types/node" "*" + +"@types/ms@*": + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== + +"@types/node@*": + version "22.5.4" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.4.tgz#83f7d1f65bc2ed223bdbf57c7884f1d5a4fa84e8" + integrity sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg== + dependencies: + undici-types "~6.19.2" + +"@types/prop-types@*": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/react-dom@^18.2.7": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.6": + version "4.4.11" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.11.tgz#d963253a611d757de01ebb241143b1017d5d63d5" + integrity sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@>=16.9.11", "@types/react@^18.2.20": + version "18.3.5" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f" + integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/unist@^2", "@types/unist@^2.0.0": + version "2.0.11" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" + integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== + +"@types/warning@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798" + integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== + +"@typescript-eslint/eslint-plugin@^6.7.4": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" + integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== + dependencies: + "@eslint-community/regexpp" "^4.5.1" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/type-utils" "6.21.0" + "@typescript-eslint/utils" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" graphemer "^1.4.0" - ignore "^5.3.1" + ignore "^5.2.4" natural-compare "^1.4.0" - ts-api-utils "^1.3.0" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/parser@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.0.0-alpha.29.tgz#dc9c395b77dfb339bafa613a02174cb928e8df31" - integrity sha512-WB5SMIFoEAco8rzfqFbVncbZobvigOePjpbDbRAvOn4dHGcYLvyNv6hy0vFOv52ngfCGjIEznDhUOKfKTVohJw== +"@typescript-eslint/parser@^6.7.4": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== dependencies: - "@typescript-eslint/scope-manager" "8.0.0-alpha.29" - "@typescript-eslint/types" "8.0.0-alpha.29" - "@typescript-eslint/typescript-estree" "8.0.0-alpha.29" - "@typescript-eslint/visitor-keys" "8.0.0-alpha.29" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.29.tgz#a1c45f586020892591c374d80d65b9ac0e49a16e" - integrity sha512-DqTnaDI3ULPE5xXeWTKzdBqcOScDyFna6oqaQAIKaNjTGCBB75MmvLl3+G1SbpFGQMlhTilkCcjvKkAr0Av1Rw== +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== dependencies: - "@typescript-eslint/types" "8.0.0-alpha.29" - "@typescript-eslint/visitor-keys" "8.0.0-alpha.29" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/type-utils@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.29.tgz#ccb353b3eaf1b84f0d24b0a4f94f5cc8e183044e" - integrity sha512-12PYg3bgUCMsl5jvUC6A2x2gT25jULiCdV/58I1uweUxCYcQC6rh8FN+h5zx6LKnxQr79MJhgfh3vLk6rD+VZQ== +"@typescript-eslint/type-utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" + integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== dependencies: - "@typescript-eslint/typescript-estree" "8.0.0-alpha.29" - "@typescript-eslint/utils" "8.0.0-alpha.29" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/utils" "6.21.0" debug "^4.3.4" - ts-api-utils "^1.3.0" + ts-api-utils "^1.0.1" -"@typescript-eslint/types@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.0.0-alpha.29.tgz#bdfa0eb29b08621fec8035e6fa66b8f3a3510c55" - integrity sha512-RG0/ZUiX3H0Dgjt9/3CYkAgQeUoo4AVZxi5xot/JI4t5Wfx+4gn4J3ywAf+AcNokplPZYdGsc/awqwqBgUQhtA== +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/typescript-estree@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.29.tgz#be30eb723357c535c0d591e9dd5338217a8d73ac" - integrity sha512-O2BkauDJjjprmTSJR+3fcnFtTEu6/t0Aku1v8momFg3FT8t4Bym8DrBz3wHO5/T746aa/TkOH/rXgYD6DLd8Bg== +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== dependencies: - "@typescript-eslint/types" "8.0.0-alpha.29" - "@typescript-eslint/visitor-keys" "8.0.0-alpha.29" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^1.3.0" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" -"@typescript-eslint/utils@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.0.0-alpha.29.tgz#e438b8f2d8d39ea7d664cbdf5c28f32cb526793b" - integrity sha512-zBlyvo4GhuTiJ1At3h9fsnOrUSUgQHG9mYtamxIsTVDVFd0Jbkl/yKgzhi43OpQTIiPkMDnZF/M4/7RbytRKlA== +"@typescript-eslint/utils@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "8.0.0-alpha.29" - "@typescript-eslint/types" "8.0.0-alpha.29" - "@typescript-eslint/typescript-estree" "8.0.0-alpha.29" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" -"@typescript-eslint/visitor-keys@8.0.0-alpha.29": - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.29.tgz#7e9a63005e78be4c62a7325a1079e42ae2ea2416" - integrity sha512-6Ubt9zHVMg2t+vljk50T5vdsk72OHimtlmdQ2IiGoNhYZu9YxtlPSh/Mdw+PDYvNpjvSec1zDg+o8uN2/wQKQQ== +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== dependencies: - "@typescript-eslint/types" "8.0.0-alpha.29" - eslint-visitor-keys "^3.4.3" + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" -acorn-jsx@^5.3.2: +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@vanilla-extract/babel-plugin-debug-ids@^1.0.4": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.6.tgz#e9033b5fb97c1b13066cec701f42e753373c2516" + integrity sha512-C188vUEYmw41yxg3QooTs8r1IdbDQQ2mH7L5RkORBnHx74QlmsNfqVmKwAVTgrlYt8JoRaWMtPfGm/Ql0BNQrA== + dependencies: + "@babel/core" "^7.23.9" + +"@vanilla-extract/css@^1.14.0": + version "1.15.5" + resolved "https://registry.yarnpkg.com/@vanilla-extract/css/-/css-1.15.5.tgz#06782b98b4d1478baec578fb06c223bde589d4b3" + integrity sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng== + dependencies: + "@emotion/hash" "^0.9.0" + "@vanilla-extract/private" "^1.0.6" + css-what "^6.1.0" + cssesc "^3.0.0" + csstype "^3.0.7" + dedent "^1.5.3" + deep-object-diff "^1.1.9" + deepmerge "^4.2.2" + lru-cache "^10.4.3" + media-query-parser "^2.0.2" + modern-ahocorasick "^1.0.0" + picocolors "^1.0.0" + +"@vanilla-extract/integration@^6.2.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@vanilla-extract/integration/-/integration-6.5.0.tgz#613407565b07dc60b123ca9080ea3f47cd2ce7bb" + integrity sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ== + dependencies: + "@babel/core" "^7.20.7" + "@babel/plugin-syntax-typescript" "^7.20.0" + "@vanilla-extract/babel-plugin-debug-ids" "^1.0.4" + "@vanilla-extract/css" "^1.14.0" + esbuild "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0" + eval "0.1.8" + find-up "^5.0.0" + javascript-stringify "^2.0.1" + lodash "^4.17.21" + mlly "^1.4.2" + outdent "^0.8.0" + vite "^5.0.11" + vite-node "^1.2.0" + +"@vanilla-extract/private@^1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@vanilla-extract/private/-/private-1.0.6.tgz#f10bbf3189f7b827d0bd7f804a6219dd03ddbdd4" + integrity sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw== + +"@web3-storage/multipart-parser@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz#6b69dc2a32a5b207ba43e556c25cc136a56659c4" + integrity sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw== + +"@zxing/text-encoding@0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" + integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-jsx@^5.0.0, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.10.0, acorn@^8.11.3, acorn@^8.9.0: - version "8.11.3" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" - integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== +acorn@^8.0.0, acorn@^8.11.3, acorn@^8.9.0: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" ajv@^6.12.4: version "6.12.6" @@ -575,13 +1512,30 @@ ansi-regex@^5.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== -ansi-styles@^4.1.0: +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== dependencies: color-convert "^2.0.1" +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" @@ -590,44 +1544,207 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" - integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== +aria-query@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" + integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== dependencies: - dequal "^2.0.3" + deep-equal "^2.0.5" + +array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -axobject-query@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.0.0.tgz#04a4c90dce33cc5d606c76d6216e3b250ff70dab" - integrity sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw== +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== dependencies: - dequal "^2.0.3" + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.findlastindex@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== + +astring@^1.8.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef" + integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axe-core@^4.10.0: + version "4.10.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59" + integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== + +axobject-query@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" + integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== + +bail@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" + integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== -bootstrap-icons@^1.11.3: - version "1.11.3" - resolved "https://registry.yarnpkg.com/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz#03f9cb754ec005c52f9ee616e2e84a82cab3084b" - integrity sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww== +bl@^4.0.3, bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bootstrap@^5.3.3: + version "5.3.3" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.3.tgz#de35e1a765c897ac940021900fcbb831602bac38" + integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg== brace-expansion@^1.1.7: version "1.1.11" @@ -651,27 +1768,105 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" -buffer-crc32@^0.2.5: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== +browserify-zlib@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" + integrity sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ== + dependencies: + pako "~0.2.0" -builtin-modules@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== +browserslist@^4.23.1: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" -bulma@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/bulma/-/bulma-1.0.1.tgz#e37261d6f8e1a3494c9378803d9958effb2715ce" - integrity sha512-+xv/BIAEQakHkR0QVz+s+RjNqfC53Mx9ZYexyaFNFo9wx5i76HXArNdwW7bccyJxa5mgV/T5DcVGqsAB19nBJQ== +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + +cacache@^17.1.3: + version "17.1.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.4.tgz#b3ff381580b47e85c6e64f801101508e26604b35" + integrity sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A== + dependencies: + "@npmcli/fs" "^3.1.0" + fs-minipass "^3.0.0" + glob "^10.2.2" + lru-cache "^7.7.1" + minipass "^7.0.3" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + p-map "^4.0.0" + ssri "^10.0.0" + tar "^6.1.11" + unique-filename "^3.0.0" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -chalk@^4.0.0: +caniuse-lite@^1.0.30001646: + version "1.0.30001655" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f" + integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== + +ccount@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" + integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -679,7 +1874,27 @@ chalk@^4.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1: +character-entities-html4@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" + integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== + +character-entities-legacy@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" + integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== + +character-entities@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== + +character-reference-invalid@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" + integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== @@ -694,16 +1909,49 @@ chalk@^4.0.0: optionalDependencies: fsevents "~2.3.2" -code-red@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/code-red/-/code-red-1.0.4.tgz#59ba5c9d1d320a4ef795bc10a28bd42bfebe3e35" - integrity sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw== +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +classnames@^2.3.2, classnames@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" - "@types/estree" "^1.0.1" - acorn "^8.10.0" - estree-walker "^3.0.3" - periscopic "^3.1.0" + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" color-convert@^2.0.1: version "2.0.1" @@ -712,27 +1960,96 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== +comma-separated-tokens@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" + integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -cookie@^0.6.0: +confbox@^0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" + integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie-signature@^1.1.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.1.tgz#790dea2cce64638c7ae04d9fabed193bd7ccf3b4" + integrity sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw== + +cookie@0.6.0, cookie@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== -cross-spawn@^7.0.2: +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -741,50 +2058,174 @@ cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -css-tree@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-2.3.1.tgz#10264ce1e5442e8572fc82fbe490644ff54b5c20" - integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== - dependencies: - mdn-data "2.0.30" - source-map-js "^1.0.1" +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== cssesc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== -debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.5" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" - integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== +csstype@^3.0.2, csstype@^3.0.7: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-uri-to-buffer@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" + integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== dependencies: ms "2.1.2" +decode-named-character-reference@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" + integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== + dependencies: + character-entities "^2.0.0" + +dedent@^1.5.3: + version "1.5.3" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" + integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== + +deep-equal@^2.0.5: + version "2.2.3" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" + integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.5" + es-get-iterator "^1.1.3" + get-intrinsic "^1.2.2" + is-arguments "^1.1.1" + is-array-buffer "^3.0.2" + is-date-object "^1.0.5" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + isarray "^2.0.5" + object-is "^1.1.5" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + side-channel "^1.0.4" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.13" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.2.2, deepmerge@^4.3.1: +deep-object-diff@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.9.tgz#6df7ef035ad6a0caa44479c536ed7b02570f4595" + integrity sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA== + +deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== -dequal@^2.0.3: +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +dequal@^2.0.0, dequal@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== -detect-indent@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" - integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== -devalue@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/devalue/-/devalue-5.0.0.tgz#1ca0099a7d715b4d6cac3924e770ccbbc584ad98" - integrity sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA== +diff@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" + integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== dir-glob@^3.0.1: version "3.0.1" @@ -793,74 +2234,448 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -es6-promise@^3.1.2: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" - integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg== +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" -esbuild@^0.20.1: - version "0.20.2" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" - integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dotenv@^16.0.0: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + +duplexify@^3.5.0, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.15.0: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-get-iterator@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" + integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + has-symbols "^1.0.3" + is-arguments "^1.1.1" + is-map "^2.0.2" + is-set "^2.0.2" + is-string "^1.0.7" + isarray "^2.0.5" + stop-iteration-iterator "^1.0.0" + +es-iterator-helpers@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-module-lexer@^1.3.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild-plugins-node-modules-polyfill@^1.6.0: + version "1.6.6" + resolved "https://registry.yarnpkg.com/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.6.6.tgz#acdfbd32443a1667a029b930b15a5ae767a7ed25" + integrity sha512-0wDvliv65SCaaGtmoITnmXqqiUzU+ggFupnOgkEo2B9cQ+CUt58ql2+EY6dYoEsoqiHRu2NuTrFUJGMJEgMmLw== + dependencies: + "@jspm/core" "^2.0.1" + local-pkg "^0.5.0" + resolve.exports "^2.0.2" + +esbuild@0.17.6: + version "0.17.6" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.6.tgz#bbccd4433629deb6e0a83860b3b61da120ba4e01" + integrity sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q== optionalDependencies: - "@esbuild/aix-ppc64" "0.20.2" - "@esbuild/android-arm" "0.20.2" - "@esbuild/android-arm64" "0.20.2" - "@esbuild/android-x64" "0.20.2" - "@esbuild/darwin-arm64" "0.20.2" - "@esbuild/darwin-x64" "0.20.2" - "@esbuild/freebsd-arm64" "0.20.2" - "@esbuild/freebsd-x64" "0.20.2" - "@esbuild/linux-arm" "0.20.2" - "@esbuild/linux-arm64" "0.20.2" - "@esbuild/linux-ia32" "0.20.2" - "@esbuild/linux-loong64" "0.20.2" - "@esbuild/linux-mips64el" "0.20.2" - "@esbuild/linux-ppc64" "0.20.2" - "@esbuild/linux-riscv64" "0.20.2" - "@esbuild/linux-s390x" "0.20.2" - "@esbuild/linux-x64" "0.20.2" - "@esbuild/netbsd-x64" "0.20.2" - "@esbuild/openbsd-x64" "0.20.2" - "@esbuild/sunos-x64" "0.20.2" - "@esbuild/win32-arm64" "0.20.2" - "@esbuild/win32-ia32" "0.20.2" - "@esbuild/win32-x64" "0.20.2" + "@esbuild/android-arm" "0.17.6" + "@esbuild/android-arm64" "0.17.6" + "@esbuild/android-x64" "0.17.6" + "@esbuild/darwin-arm64" "0.17.6" + "@esbuild/darwin-x64" "0.17.6" + "@esbuild/freebsd-arm64" "0.17.6" + "@esbuild/freebsd-x64" "0.17.6" + "@esbuild/linux-arm" "0.17.6" + "@esbuild/linux-arm64" "0.17.6" + "@esbuild/linux-ia32" "0.17.6" + "@esbuild/linux-loong64" "0.17.6" + "@esbuild/linux-mips64el" "0.17.6" + "@esbuild/linux-ppc64" "0.17.6" + "@esbuild/linux-riscv64" "0.17.6" + "@esbuild/linux-s390x" "0.17.6" + "@esbuild/linux-x64" "0.17.6" + "@esbuild/netbsd-x64" "0.17.6" + "@esbuild/openbsd-x64" "0.17.6" + "@esbuild/sunos-x64" "0.17.6" + "@esbuild/win32-arm64" "0.17.6" + "@esbuild/win32-ia32" "0.17.6" + "@esbuild/win32-x64" "0.17.6" + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + optionalDependencies: + "@esbuild/aix-ppc64" "0.21.5" + "@esbuild/android-arm" "0.21.5" + "@esbuild/android-arm64" "0.21.5" + "@esbuild/android-x64" "0.21.5" + "@esbuild/darwin-arm64" "0.21.5" + "@esbuild/darwin-x64" "0.21.5" + "@esbuild/freebsd-arm64" "0.21.5" + "@esbuild/freebsd-x64" "0.21.5" + "@esbuild/linux-arm" "0.21.5" + "@esbuild/linux-arm64" "0.21.5" + "@esbuild/linux-ia32" "0.21.5" + "@esbuild/linux-loong64" "0.21.5" + "@esbuild/linux-mips64el" "0.21.5" + "@esbuild/linux-ppc64" "0.21.5" + "@esbuild/linux-riscv64" "0.21.5" + "@esbuild/linux-s390x" "0.21.5" + "@esbuild/linux-x64" "0.21.5" + "@esbuild/netbsd-x64" "0.21.5" + "@esbuild/openbsd-x64" "0.21.5" + "@esbuild/sunos-x64" "0.21.5" + "@esbuild/win32-arm64" "0.21.5" + "@esbuild/win32-ia32" "0.21.5" + "@esbuild/win32-x64" "0.21.5" + +"esbuild@npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0": + version "0.19.12" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" + integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.19.12" + "@esbuild/android-arm" "0.19.12" + "@esbuild/android-arm64" "0.19.12" + "@esbuild/android-x64" "0.19.12" + "@esbuild/darwin-arm64" "0.19.12" + "@esbuild/darwin-x64" "0.19.12" + "@esbuild/freebsd-arm64" "0.19.12" + "@esbuild/freebsd-x64" "0.19.12" + "@esbuild/linux-arm" "0.19.12" + "@esbuild/linux-arm64" "0.19.12" + "@esbuild/linux-ia32" "0.19.12" + "@esbuild/linux-loong64" "0.19.12" + "@esbuild/linux-mips64el" "0.19.12" + "@esbuild/linux-ppc64" "0.19.12" + "@esbuild/linux-riscv64" "0.19.12" + "@esbuild/linux-s390x" "0.19.12" + "@esbuild/linux-x64" "0.19.12" + "@esbuild/netbsd-x64" "0.19.12" + "@esbuild/openbsd-x64" "0.19.12" + "@esbuild/sunos-x64" "0.19.12" + "@esbuild/win32-arm64" "0.19.12" + "@esbuild/win32-ia32" "0.19.12" + "@esbuild/win32-x64" "0.19.12" + +escalade@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-compat-utils@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz#7fc92b776d185a70c4070d03fd26fde3d59652e4" - integrity sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q== +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== dependencies: - semver "^7.5.4" + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" -eslint-config-prettier@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" - integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== - -eslint-plugin-svelte@^2.36.0: - version "2.39.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-svelte/-/eslint-plugin-svelte-2.39.0.tgz#83a5a9fd2199fdb101322b549b0ab1b426f62d93" - integrity sha512-FXktBLXsrxbA+6ZvJK2z/sQOrUKyzSg3fNWK5h0reSCjr2fjAsc9ai/s/JvSl4Hgvz3nYVtTIMwarZH5RcB7BA== +eslint-import-resolver-typescript@^3.6.1: + version "3.6.3" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e" + integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA== dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@jridgewell/sourcemap-codec" "^1.4.15" - debug "^4.3.4" - eslint-compat-utils "^0.5.0" - esutils "^2.0.3" - known-css-properties "^0.31.0" - postcss "^8.4.38" - postcss-load-config "^3.1.4" - postcss-safe-parser "^6.0.0" - postcss-selector-parser "^6.0.16" - semver "^7.6.0" - svelte-eslint-parser ">=0.36.0 <1.0.0" + "@nolyfill/is-core-module" "1.0.39" + debug "^4.3.5" + enhanced-resolve "^5.15.0" + eslint-module-utils "^2.8.1" + fast-glob "^3.3.2" + get-tsconfig "^4.7.5" + is-bun-module "^1.0.2" + is-glob "^4.0.3" + +eslint-module-utils@^2.8.1, eslint-module-utils@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342" + integrity sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.28.1: + version "2.30.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449" + integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw== + dependencies: + "@rtsao/scc" "^1.1.0" + array-includes "^3.1.8" + array.prototype.findlastindex "^1.2.5" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.9.0" + hasown "^2.0.2" + is-core-module "^2.15.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + object.groupby "^1.0.3" + object.values "^1.2.0" + semver "^6.3.1" + tsconfig-paths "^3.15.0" + +eslint-plugin-jsx-a11y@^6.7.1: + version "6.10.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz#36fb9dead91cafd085ddbe3829602fb10ef28339" + integrity sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== + dependencies: + aria-query "~5.1.3" + array-includes "^3.1.8" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "^4.10.0" + axobject-query "^4.1.0" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + es-iterator-helpers "^1.0.19" + hasown "^2.0.2" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" + minimatch "^3.1.2" + object.fromentries "^2.0.8" + safe-regex-test "^1.0.3" + string.prototype.includes "^2.0.0" + +eslint-plugin-react-hooks@^4.6.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== + +eslint-plugin-react@^7.33.2: + version "7.35.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz#d32500d3ec268656d5071918bfec78cfd8b070ed" + integrity sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.2" + array.prototype.tosorted "^1.1.4" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.19" + estraverse "^5.3.0" + hasown "^2.0.2" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.values "^1.2.0" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.11" + string.prototype.repeat "^1.0.0" eslint-scope@^7.2.2: version "7.2.2" @@ -870,55 +2685,46 @@ eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-scope@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc" - integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint-visitor-keys@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" - integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== - -eslint@^9.0.0: - version "9.4.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.4.0.tgz#79150c3610ae606eb131f1d648d5f43b3d45f3cd" - integrity sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA== +eslint@^8.38.0: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/config-array" "^0.15.1" - "@eslint/eslintrc" "^3.1.0" - "@eslint/js" "9.4.0" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" "@humanwhocodes/module-importer" "^1.0.1" - "@humanwhocodes/retry" "^0.3.0" "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" + doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^8.0.1" - eslint-visitor-keys "^4.0.0" - espree "^10.0.1" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^8.0.0" + file-entry-cache "^6.0.1" find-up "^5.0.0" glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" @@ -928,21 +2734,7 @@ eslint@^9.0.0: strip-ansi "^6.0.1" text-table "^0.2.0" -esm-env@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/esm-env/-/esm-env-1.0.0.tgz#b124b40b180711690a4cb9b00d16573391950413" - integrity sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA== - -espree@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" - integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== - dependencies: - acorn "^8.11.3" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^4.0.0" - -espree@^9.6.1: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -952,9 +2744,9 @@ espree@^9.6.1: eslint-visitor-keys "^3.4.1" esquery@^1.4.2: - version "1.5.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" - integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== dependencies: estraverse "^5.1.0" @@ -965,34 +2757,159 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^5.1.0, estraverse@^5.2.0: +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estree-walker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== +estree-util-attach-comments@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz#ee44f4ff6890ee7dfb3237ac7810154c94c63f84" + integrity sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w== + dependencies: + "@types/estree" "^1.0.0" -estree-walker@^3.0.0, estree-walker@^3.0.3: +estree-util-build-jsx@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz#32f8a239fb40dc3f3dca75bb5dcf77a831e4e47b" + integrity sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg== + dependencies: + "@types/estree-jsx" "^1.0.0" + estree-util-is-identifier-name "^2.0.0" + estree-walker "^3.0.0" + +estree-util-is-identifier-name@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-1.1.0.tgz#2e3488ea06d9ea2face116058864f6370b37456d" + integrity sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ== + +estree-util-is-identifier-name@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz#fb70a432dcb19045e77b05c8e732f1364b4b49b2" + integrity sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ== + +estree-util-to-js@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz#0f80d42443e3b13bd32f7012fffa6f93603f4a36" + integrity sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA== + dependencies: + "@types/estree-jsx" "^1.0.0" + astring "^1.8.0" + source-map "^0.7.0" + +estree-util-value-to-estree@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz#1d3125594b4d6680f666644491e7ac1745a3df49" + integrity sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw== + dependencies: + is-plain-obj "^3.0.0" + +estree-util-visit@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-1.2.1.tgz#8bc2bc09f25b00827294703835aabee1cc9ec69d" + integrity sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/unist" "^2.0.0" + +estree-walker@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== dependencies: "@types/estree" "^1.0.0" -esutils@^2.0.2, esutils@^2.0.3: +esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eval@0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.8.tgz#2b903473b8cc1d1989b83a1e7923f883eb357f85" + integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== + dependencies: + "@types/node" "*" + require-like ">= 0.1.1" + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +execa@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +exit-hook@2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.1.tgz#007b2d92c6428eda2b76e7016a34351586934593" + integrity sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw== + +express@^4.19.2: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "3.1.3" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.7, fast-glob@^3.2.9: +fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -1020,12 +2937,19 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -file-entry-cache@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" - integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== +fault@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" + integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== dependencies: - flat-cache "^4.0.0" + format "^0.2.0" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" fill-range@^7.1.1: version "7.1.1" @@ -1034,6 +2958,19 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -1042,19 +2979,78 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" - integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== dependencies: flatted "^3.2.9" - keyv "^4.5.4" + keyv "^4.5.3" + rimraf "^3.0.2" flatted@^3.2.9: version "3.3.1" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" + integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +format@^0.2.0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" + integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-minipass@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" + integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== + dependencies: + minipass "^7.0.3" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -1070,6 +3066,70 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +generic-names@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" + integrity sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A== + dependencies: + loader-utils "^3.2.0" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-port@5.1.1, get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +get-tsconfig@^4.7.5: + version "4.8.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.0.tgz#125dc13a316f61650a12b20c97c11b8fd996fedd" + integrity sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw== + dependencies: + resolve-pkg-maps "^1.0.0" + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1084,6 +3144,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob@^10.2.2: + version "10.4.5" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" + integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^1.11.1" + glob@^7.1.3: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1096,31 +3168,25 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" + type-fest "^0.20.2" -globals@^14.0.0: - version "14.0.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" - integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== - -globals@^15.0.0: - version "15.4.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-15.4.0.tgz#3e36ea6e4d9ddcf1cb42d92f5c4a145a8a2ddc1c" - integrity sha512-unnwvMZpv0eDUyjNyh9DH/yxUaRYrEjW/qK4QcdrHg3oO11igUQrCSgODHEqxlKg8v2CD2Sd7UkqqEBoz5U7TQ== - -globalyzer@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" - integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" globby@^11.1.0: version "11.1.0" @@ -1139,7 +3205,14 @@ globrex@^0.1.2: resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== -graceful-fs@^4.1.3: +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1149,27 +3222,139 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +gunzip-maybe@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac" + integrity sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw== + dependencies: + browserify-zlib "^0.1.4" + is-deflate "^1.0.0" + is-gzip "^1.0.0" + peek-stream "^1.1.0" + pumpify "^1.3.3" + through2 "^2.0.3" + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -hasown@^2.0.0: +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: function-bind "^1.1.2" -ignore@^5.2.0, ignore@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" - integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== +hast-util-to-estree@^2.0.0: + version "2.3.3" + resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz#da60142ffe19a6296923ec222aba73339c8bf470" + integrity sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ== + dependencies: + "@types/estree" "^1.0.0" + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^2.0.0" + "@types/unist" "^2.0.0" + comma-separated-tokens "^2.0.0" + estree-util-attach-comments "^2.0.0" + estree-util-is-identifier-name "^2.0.0" + hast-util-whitespace "^2.0.0" + mdast-util-mdx-expression "^1.0.0" + mdast-util-mdxjs-esm "^1.0.0" + property-information "^6.0.0" + space-separated-tokens "^2.0.0" + style-to-object "^0.4.1" + unist-util-position "^4.0.0" + zwitch "^2.0.0" + +hast-util-whitespace@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" + integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== + +hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" + integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== + dependencies: + lru-cache "^7.5.1" + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^5.2.0, ignore@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== immutable@^4.0.0: - version "4.3.6" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.6.tgz#6a05f7858213238e587fb83586ffa3b4b27f0447" - integrity sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ== + version "4.3.7" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" + integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== import-fresh@^3.2.1: version "3.3.0" @@ -1179,16 +3364,16 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-meta-resolve@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz#f9db8bead9fafa61adb811db77a2bf22c5399706" - integrity sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw== - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1197,11 +3382,80 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inline-style-parser@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" + integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== + +internal-slot@^1.0.4, internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-alphabetical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" + integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== + +is-alphanumerical@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" + integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== + dependencies: + is-alphabetical "^2.0.0" + is-decimal "^2.0.0" + +is-arguments@^1.0.4, is-arguments@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1209,25 +3463,86 @@ is-binary-path@~2.1.0: dependencies: binary-extensions "^2.0.0" -is-builtin-module@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" - integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== dependencies: - builtin-modules "^3.3.0" + call-bind "^1.0.2" + has-tostringtag "^1.0.0" -is-core-module@^2.13.0: - version "2.13.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" - integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== +is-buffer@^2.0.0: + version "2.0.5" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-bun-module@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.1.0.tgz#a66b9830869437f6cdad440ba49ab6e4dc837269" + integrity sha512-4mTAVPlrXpaN3jtF0lsnPCMGnq4+qZjVIKq0HCpfcqf8OC1SM5oATCIAPM5V5FN05qp2NNnFndphmdZS9CV3hA== dependencies: - hasown "^2.0.0" + semver "^7.6.3" + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.8.1: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-decimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" + integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== + +is-deflate@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-deflate/-/is-deflate-1.0.0.tgz#c862901c3c161fb09dac7cdc7e784f80e98f2f14" + integrity sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ== is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.10, is-generator-function@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -1235,10 +3550,37 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" -is-module@^1.0.0: +is-gzip@^1.0.0: version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83" + integrity sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ== + +is-hexadecimal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" + integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-map@^2.0.2, is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" is-number@^7.0.0: version "7.0.0" @@ -1250,37 +3592,171 @@ is-path-inside@^3.0.3: resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== -is-reference@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" - integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== - dependencies: - "@types/estree" "*" +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== -is-reference@^3.0.0, is-reference@^3.0.1: +is-plain-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-reference@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== dependencies: "@types/estree" "*" +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.2, is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13, is-typed-array@^1.1.3: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isbot@^4.1.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/isbot/-/isbot-4.4.0.tgz#897ce9f2e498de6181027660ca80de8734d1ef81" + integrity sha512-8ZvOWUA68kyJO4hHJdWjyreq7TYNWTS9y15IzeqVdKxR9pPr3P/3r9AHcoIv9M0Rllkao5qWz2v1lmcyKIVCzQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -js-yaml@^4.1.0: +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@^3.1.2: + version "3.4.3" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" + integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +javascript-stringify@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79" + integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== dependencies: argparse "^2.0.1" +jsesc@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== +json-parse-even-better-errors@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz#b43d35e89c0f3be6b5fbbe9dc6c82467b30c28da" + integrity sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -1291,22 +3767,60 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -keyv@^4.5.4: +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.2.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" -kleur@^4.1.5: +kleur@^4.0.3: version "4.1.5" resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -known-css-properties@^0.31.0: - version "0.31.0" - resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.31.0.tgz#5c8d9d8777b3ca09482b2397f6a241e5d69a1023" - integrity sha512-sBPIUGTNF0czz0mwGGUoKKJC8Q7On1GPbCSFPfyEsfHb2DyBG0Y4QtV+EVWpINSaiGKZblDNuF5AezxSgOhesQ== +language-subtag-registry@^0.3.20: + version "0.3.23" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" + integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== + +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" levn@^0.4.1: version "0.4.1" @@ -1316,15 +3830,23 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lilconfig@^2.0.5: - version "2.1.0" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" - integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== +lilconfig@^3.0.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" + integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== -locate-character@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-character/-/locate-character-3.0.0.tgz#0305c5b8744f61028ef5d01f444009e00779f974" - integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA== +loader-utils@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" + integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== + +local-pkg@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" + integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== + dependencies: + mlly "^1.4.2" + pkg-types "^1.0.3" locate-path@^6.0.0: version "6.0.0" @@ -1333,40 +3855,569 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.camelcase@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -magic-string@^0.30.10, magic-string@^0.30.3, magic-string@^0.30.4, magic-string@^0.30.5: - version "0.30.10" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" - integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.15" +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -mdn-data@2.0.30: - version "2.0.30" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.30.tgz#ce4df6f80af6cfbe218ecd5c552ba13c4dfa08cc" - integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +longest-streak@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" + integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^10.2.0, lru-cache@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +markdown-extensions@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3" + integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q== + +mdast-util-definitions@^5.0.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" + integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + unist-util-visit "^4.0.0" + +mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" + integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + decode-named-character-reference "^1.0.0" + mdast-util-to-string "^3.1.0" + micromark "^3.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-decode-string "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-stringify-position "^3.0.0" + uvu "^0.5.0" + +mdast-util-frontmatter@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-1.0.1.tgz#79c46d7414eb9d3acabe801ee4a70a70b75e5af1" + integrity sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-to-markdown "^1.3.0" + micromark-extension-frontmatter "^1.0.0" + +mdast-util-mdx-expression@^1.0.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz#d027789e67524d541d6de543f36d51ae2586f220" + integrity sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-mdx-jsx@^2.0.0: + version "2.1.4" + resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz#7c1f07f10751a78963cfabee38017cbc8b7786d1" + integrity sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + ccount "^2.0.0" + mdast-util-from-markdown "^1.1.0" + mdast-util-to-markdown "^1.3.0" + parse-entities "^4.0.0" + stringify-entities "^4.0.0" + unist-util-remove-position "^4.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + +mdast-util-mdx@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz#49b6e70819b99bb615d7223c088d295e53bb810f" + integrity sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw== + dependencies: + mdast-util-from-markdown "^1.0.0" + mdast-util-mdx-expression "^1.0.0" + mdast-util-mdx-jsx "^2.0.0" + mdast-util-mdxjs-esm "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-mdxjs-esm@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz#645d02cd607a227b49721d146fd81796b2e2d15b" + integrity sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w== + dependencies: + "@types/estree-jsx" "^1.0.0" + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + mdast-util-to-markdown "^1.0.0" + +mdast-util-phrasing@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" + integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== + dependencies: + "@types/mdast" "^3.0.0" + unist-util-is "^5.0.0" + +mdast-util-to-hast@^12.1.0: + version "12.3.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" + integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-definitions "^5.0.0" + micromark-util-sanitize-uri "^1.1.0" + trim-lines "^3.0.0" + unist-util-generated "^2.0.0" + unist-util-position "^4.0.0" + unist-util-visit "^4.0.0" + +mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" + integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== + dependencies: + "@types/mdast" "^3.0.0" + "@types/unist" "^2.0.0" + longest-streak "^3.0.0" + mdast-util-phrasing "^3.0.0" + mdast-util-to-string "^3.0.0" + micromark-util-decode-string "^1.0.0" + unist-util-visit "^4.0.0" + zwitch "^2.0.0" + +mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" + integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== + dependencies: + "@types/mdast" "^3.0.0" + +media-query-parser@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29" + integrity sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w== + dependencies: + "@babel/runtime" "^7.12.5" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" + integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-factory-destination "^1.0.0" + micromark-factory-label "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-factory-title "^1.0.0" + micromark-factory-whitespace "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-classify-character "^1.0.0" + micromark-util-html-tag-name "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + +micromark-extension-frontmatter@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-1.1.1.tgz#2946643938e491374145d0c9aacc3249e38a865f" + integrity sha512-m2UH9a7n3W8VAH9JO9y01APpPKmNNNs71P0RbknEmYSaZU5Ghogv38BYO94AI5Xw6OYfxZRdHZZ2nYjs/Z+SZQ== + dependencies: + fault "^2.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-extension-mdx-expression@^1.0.0: + version "1.0.8" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz#5bc1f5fd90388e8293b3ef4f7c6f06c24aff6314" + integrity sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw== + dependencies: + "@types/estree" "^1.0.0" + micromark-factory-mdx-expression "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-events-to-acorn "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-extension-mdx-jsx@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz#e72d24b7754a30d20fb797ece11e2c4e2cae9e82" + integrity sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + estree-util-is-identifier-name "^2.0.0" + micromark-factory-mdx-expression "^1.0.0" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-extension-mdx-md@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz#595d4b2f692b134080dca92c12272ab5b74c6d1a" + integrity sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-extension-mdxjs-esm@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz#e4f8be9c14c324a80833d8d3a227419e2b25dec1" + integrity sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w== + dependencies: + "@types/estree" "^1.0.0" + micromark-core-commonmark "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-events-to-acorn "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-position-from-estree "^1.1.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-extension-mdxjs@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz#f78d4671678d16395efeda85170c520ee795ded8" + integrity sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q== + dependencies: + acorn "^8.0.0" + acorn-jsx "^5.0.0" + micromark-extension-mdx-expression "^1.0.0" + micromark-extension-mdx-jsx "^1.0.0" + micromark-extension-mdx-md "^1.0.0" + micromark-extension-mdxjs-esm "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-destination@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" + integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-label@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" + integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-factory-mdx-expression@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz#57ba4571b69a867a1530f34741011c71c73a4976" + integrity sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA== + dependencies: + "@types/estree" "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-events-to-acorn "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + unist-util-position-from-estree "^1.0.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-factory-space@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" + integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-title@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" + integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-factory-whitespace@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" + integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== + dependencies: + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-character@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" + integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== + dependencies: + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-chunked@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" + integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-classify-character@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" + integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-combine-extensions@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" + integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-types "^1.0.0" + +micromark-util-decode-numeric-character-reference@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" + integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-decode-string@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" + integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== + dependencies: + decode-named-character-reference "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" + integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== + +micromark-util-events-to-acorn@^1.0.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz#a4ab157f57a380e646670e49ddee97a72b58b557" + integrity sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w== + dependencies: + "@types/acorn" "^4.0.0" + "@types/estree" "^1.0.0" + "@types/unist" "^2.0.0" + estree-util-visit "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + vfile-message "^3.0.0" + +micromark-util-html-tag-name@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" + integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== + +micromark-util-normalize-identifier@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" + integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== + dependencies: + micromark-util-symbol "^1.0.0" + +micromark-util-resolve-all@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" + integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== + dependencies: + micromark-util-types "^1.0.0" + +micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" + integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== + dependencies: + micromark-util-character "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-symbol "^1.0.0" + +micromark-util-subtokenize@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" + integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== + dependencies: + micromark-util-chunked "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.0" + uvu "^0.5.0" + +micromark-util-symbol@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" + integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== + +micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" + integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== + +micromark@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" + integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== + dependencies: + "@types/debug" "^4.0.0" + debug "^4.0.0" + decode-named-character-reference "^1.0.0" + micromark-core-commonmark "^1.0.1" + micromark-factory-space "^1.0.0" + micromark-util-character "^1.0.0" + micromark-util-chunked "^1.0.0" + micromark-util-combine-extensions "^1.0.0" + micromark-util-decode-numeric-character-reference "^1.0.0" + micromark-util-encode "^1.0.0" + micromark-util-normalize-identifier "^1.0.0" + micromark-util-resolve-all "^1.0.0" + micromark-util-sanitize-uri "^1.0.0" + micromark-util-subtokenize "^1.0.0" + micromark-util-symbol "^1.0.0" + micromark-util-types "^1.0.1" + uvu "^0.5.0" + micromatch@^4.0.4: - version "4.0.7" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1" -min-indent@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" - integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" @@ -1375,17 +4426,10 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== +minimatch@^9.0.0, minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== dependencies: brace-expansion "^2.0.1" @@ -1394,28 +4438,113 @@ minimist@^1.2.0, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -mkdirp@^0.5.1: - version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== dependencies: - minimist "^1.2.6" + minipass "^3.0.0" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0: + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + +mkdirp@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mlly@^1.4.2, mlly@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" + integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== + dependencies: + acorn "^8.11.3" + pathe "^1.1.2" + pkg-types "^1.1.1" + ufo "^1.5.3" + +modern-ahocorasick@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz#dec373444f51b5458ac05216a8ec376e126dd283" + integrity sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA== + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" mri@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== -mrmime@^2.0.0: +mrmime@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" + integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== + +ms@2.0.0: version "2.0.0" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-2.0.0.tgz#151082a6e06e59a9a39b46b3e14d5cfe92b3abb4" - integrity sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw== + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" @@ -1426,18 +4555,173 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-package-data@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-5.0.0.tgz#abcb8d7e724c40d88462b84982f7cbf6859b4588" + integrity sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q== + dependencies: + hosted-git-info "^6.0.0" + is-core-module "^2.8.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -once@^1.3.0: +npm-install-checks@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-6.3.0.tgz#046552d8920e801fa9f919cad569545d60e826fe" + integrity sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw== + dependencies: + semver "^7.1.1" + +npm-normalize-package-bin@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" + integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== + +npm-package-arg@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-10.1.0.tgz#827d1260a683806685d17193073cc152d3c7e9b1" + integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== + dependencies: + hosted-git-info "^6.0.0" + proc-log "^3.0.0" + semver "^7.3.5" + validate-npm-package-name "^5.0.0" + +npm-pick-manifest@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz#2159778d9c7360420c925c1a2287b5a884c713aa" + integrity sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg== + dependencies: + npm-install-checks "^6.0.0" + npm-normalize-package-bin "^3.0.0" + npm-package-arg "^10.0.0" + semver "^7.3.5" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +object-is@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" + integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.groupby@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + +object.values@^1.1.6, object.values@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== dependencies: wrappy "1" +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -1450,6 +4734,26 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +outdent@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.8.0.tgz#2ebc3e77bf49912543f1008100ff8e7f44428eb0" + integrity sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A== + p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" @@ -1464,6 +4768,23 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +package-json-from-dist@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" + integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== + +pako@~0.2.0: + version "0.2.9" + resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" + integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== + parent-module@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" @@ -1471,6 +4792,30 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" +parse-entities@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" + integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== + dependencies: + "@types/unist" "^2.0.0" + character-entities "^2.0.0" + character-entities-legacy "^3.0.0" + character-reference-invalid "^2.0.0" + decode-named-character-reference "^1.0.0" + is-alphanumerical "^2.0.0" + is-decimal "^2.0.0" + is-hexadecimal "^2.0.0" + +parse-ms@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" + integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -1481,7 +4826,7 @@ path-is-absolute@^1.0.0: resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== -path-key@^3.1.0: +path-key@^3.0.0, path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== @@ -1491,12 +4836,39 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -periscopic@^3.1.0: +pathe@^1.1.1, pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +peek-stream@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67" + integrity sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA== + dependencies: + buffer-from "^1.0.0" + duplexify "^3.5.0" + through2 "^2.0.3" + +periscopic@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== @@ -1505,49 +4877,110 @@ periscopic@^3.1.0: estree-walker "^3.0.0" is-reference "^3.0.0" -picocolors@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" - integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" + integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -postcss-load-config@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" - integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== +pidtree@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" + integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== + +pkg-types@^1.0.3, pkg-types@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.2.0.tgz#d0268e894e93acff11a6279de147e83354ebd42d" + integrity sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA== dependencies: - lilconfig "^2.0.5" - yaml "^1.10.2" + confbox "^0.1.7" + mlly "^1.7.1" + pathe "^1.1.2" -postcss-safe-parser@^6.0.0: +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== + +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-modules-extract-imports@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" + integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.5" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" + integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" + integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-modules@^6.0.0: version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-safe-parser/-/postcss-safe-parser-6.0.0.tgz#bb4c29894171a94bc5c996b9a30317ef402adaa1" - integrity sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ== + resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-6.0.0.tgz#cac283dbabbbdc2558c45391cbd0e2df9ec50118" + integrity sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ== + dependencies: + generic-names "^4.0.0" + icss-utils "^5.1.0" + lodash.camelcase "^4.3.0" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + string-hash "^1.1.1" -postcss-scss@^4.0.9: - version "4.0.9" - resolved "https://registry.yarnpkg.com/postcss-scss/-/postcss-scss-4.0.9.tgz#a03c773cd4c9623cb04ce142a52afcec74806685" - integrity sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A== - -postcss-selector-parser@^6.0.16: - version "6.1.0" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz#49694cb4e7c649299fea510a29fa6577104bcf53" - integrity sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ== +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: + version "6.1.2" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" + integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== dependencies: cssesc "^3.0.0" util-deprecate "^1.0.2" -postcss@^8.4.38: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== +postcss-value-parser@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^8.4.19, postcss@^8.4.43: + version "8.4.45" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.45.tgz#538d13d89a16ef71edbf75d895284ae06b79e603" + integrity sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q== dependencies: nanoid "^3.3.7" - picocolors "^1.0.0" + picocolors "^1.0.1" source-map-js "^1.2.0" prelude-ls@^1.2.1: @@ -1555,26 +4988,235 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-plugin-svelte@^3.1.2: - version "3.2.4" - resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.4.tgz#50366d550b2fe64b736ec0c90998805cfc395a2c" - integrity sha512-tZv+ADfeOWFNQkXkRh6zUXE16w3Vla8x2Ug0B/EnSmjR4EnwdwZbGgL/liSwR1kcEALU5mAAyua98HBxheCxgg== +prettier@^2.7.1: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== -prettier@^3.1.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.1.tgz#e68935518dd90bb7ec4821ba970e68f8de16e1ac" - integrity sha512-7CAwy5dRsxs8PHXT3twixW9/OEll8MLE0VRPCJyl7CkS6VHGPSlsVaWTiASPTyGyYRyApxlaWTzwUxVNrhcwDg== +prettier@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" + integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== + +pretty-ms@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8" + integrity sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q== + dependencies: + parse-ms "^2.1.0" + +proc-log@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" + integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +prop-types-extra@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" + integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== + dependencies: + react-is "^16.3.2" + warning "^4.0.0" + +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +property-information@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" + integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pump@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.5.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" + integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== + dependencies: + duplexify "^3.6.0" + inherits "^2.0.3" + pump "^2.0.0" punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-bootstrap-icons@^1.11.4: + version "1.11.4" + resolved "https://registry.yarnpkg.com/react-bootstrap-icons/-/react-bootstrap-icons-1.11.4.tgz#f4d5a852af58b5e0523df7162758b77f6fef2eec" + integrity sha512-lnkOpNEZ/Zr7mNxvjA9efuarCPSgtOuGA55XiRj7ASJnBjb1wEAdtJOd2Aiv9t07r7FLI1IgyZPg9P6jqWD/IA== + dependencies: + prop-types "^15.7.2" + +react-bootstrap@^2.10.4: + version "2.10.4" + resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.10.4.tgz#ed92f5f8225a44919a7707829bac879558b71b70" + integrity sha512-W3398nBM2CBfmGP2evneEO3ZZwEMPtHs72q++eNw60uDGDAdiGn0f9yNys91eo7/y8CTF5Ke1C0QO8JFVPU40Q== + dependencies: + "@babel/runtime" "^7.24.7" + "@restart/hooks" "^0.4.9" + "@restart/ui" "^1.6.9" + "@types/react-transition-group" "^4.4.6" + classnames "^2.3.2" + dom-helpers "^5.2.1" + invariant "^2.2.4" + prop-types "^15.8.1" + prop-types-extra "^1.1.0" + react-transition-group "^4.4.5" + uncontrollable "^7.2.1" + warning "^4.0.3" + +react-dom@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-is@^16.13.1, react-is@^16.3.2: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-lifecycles-compat@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-refresh@^0.14.0: + version "0.14.2" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +react-router-dom@6.26.1: + version "6.26.1" + resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.26.1.tgz#a408892b41767a49dc94b3564b0e7d8e3959f623" + integrity sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw== + dependencies: + "@remix-run/router" "1.19.1" + react-router "6.26.1" + +react-router@6.26.1: + version "6.26.1" + resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.26.1.tgz#88c64837e05ffab6899a49df2a1484a22471e4ce" + integrity sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ== + dependencies: + "@remix-run/router" "1.19.1" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react@^18.2.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +readable-stream@^2.0.0, readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.1.1, readable-stream@^3.4.0: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + readdirp@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" @@ -1582,12 +5224,102 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +remark-frontmatter@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz#84560f7ccef114ef076d3d3735be6d69f8922309" + integrity sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-frontmatter "^1.0.0" + micromark-extension-frontmatter "^1.0.0" + unified "^10.0.0" + +remark-mdx-frontmatter@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/remark-mdx-frontmatter/-/remark-mdx-frontmatter-1.1.1.tgz#54cfb3821fbb9cb6057673e0570ae2d645f6fe32" + integrity sha512-7teX9DW4tI2WZkXS4DBxneYSY7NHiXl4AKdWDO9LXVweULlCT8OPWsOjLEnMIXViN1j+QcY8mfbq3k0EK6x3uA== + dependencies: + estree-util-is-identifier-name "^1.0.0" + estree-util-value-to-estree "^1.0.0" + js-yaml "^4.0.0" + toml "^3.0.0" + +remark-mdx@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-2.3.0.tgz#efe678025a8c2726681bde8bf111af4a93943db4" + integrity sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g== + dependencies: + mdast-util-mdx "^2.0.0" + micromark-extension-mdxjs "^1.0.0" + +remark-parse@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" + integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== + dependencies: + "@types/mdast" "^3.0.0" + mdast-util-from-markdown "^1.0.0" + unified "^10.0.0" + +remark-rehype@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" + integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== + dependencies: + "@types/hast" "^2.0.0" + "@types/mdast" "^3.0.0" + mdast-util-to-hast "^12.1.0" + unified "^10.0.0" + +"require-like@>= 0.1.1": + version "0.1.2" + resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" + integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.22.1: +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve.exports@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" + integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== + +resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -1596,41 +5328,63 @@ resolve@^1.22.1: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== + reusify@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.5.2: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== dependencies: glob "^7.1.3" -rollup@^4.13.0, rollup@^4.9.5: - version "4.18.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" - integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== +rollup@^4.20.0: + version "4.21.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.2.tgz#f41f277a448d6264e923dd1ea179f0a926aaf9b7" + integrity sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw== dependencies: "@types/estree" "1.0.5" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.18.0" - "@rollup/rollup-android-arm64" "4.18.0" - "@rollup/rollup-darwin-arm64" "4.18.0" - "@rollup/rollup-darwin-x64" "4.18.0" - "@rollup/rollup-linux-arm-gnueabihf" "4.18.0" - "@rollup/rollup-linux-arm-musleabihf" "4.18.0" - "@rollup/rollup-linux-arm64-gnu" "4.18.0" - "@rollup/rollup-linux-arm64-musl" "4.18.0" - "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0" - "@rollup/rollup-linux-riscv64-gnu" "4.18.0" - "@rollup/rollup-linux-s390x-gnu" "4.18.0" - "@rollup/rollup-linux-x64-gnu" "4.18.0" - "@rollup/rollup-linux-x64-musl" "4.18.0" - "@rollup/rollup-win32-arm64-msvc" "4.18.0" - "@rollup/rollup-win32-ia32-msvc" "4.18.0" - "@rollup/rollup-win32-x64-msvc" "4.18.0" + "@rollup/rollup-android-arm-eabi" "4.21.2" + "@rollup/rollup-android-arm64" "4.21.2" + "@rollup/rollup-darwin-arm64" "4.21.2" + "@rollup/rollup-darwin-x64" "4.21.2" + "@rollup/rollup-linux-arm-gnueabihf" "4.21.2" + "@rollup/rollup-linux-arm-musleabihf" "4.21.2" + "@rollup/rollup-linux-arm64-gnu" "4.21.2" + "@rollup/rollup-linux-arm64-musl" "4.21.2" + "@rollup/rollup-linux-powerpc64le-gnu" "4.21.2" + "@rollup/rollup-linux-riscv64-gnu" "4.21.2" + "@rollup/rollup-linux-s390x-gnu" "4.21.2" + "@rollup/rollup-linux-x64-gnu" "4.21.2" + "@rollup/rollup-linux-x64-musl" "4.21.2" + "@rollup/rollup-win32-arm64-msvc" "4.21.2" + "@rollup/rollup-win32-ia32-msvc" "4.21.2" + "@rollup/rollup-win32-x64-msvc" "4.21.2" fsevents "~2.3.2" run-parallel@^1.1.9: @@ -1640,41 +5394,133 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -sade@^1.7.4, sade@^1.8.1: +sade@^1.7.3: version "1.8.1" resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== dependencies: mri "^1.1.0" -sander@^0.5.0: - version "0.5.1" - resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad" - integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA== +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== dependencies: - es6-promise "^3.1.2" - graceful-fs "^4.1.3" - mkdirp "^0.5.1" - rimraf "^2.5.2" + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" -sass@^1.77.4: - version "1.77.4" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.4.tgz#92059c7bfc56b827c56eb116778d157ec017a5cd" - integrity sha512-vcF3Ckow6g939GMA4PeU7b2K/9FALXk2KF9J87txdHzXbUF9XRQRwSxcAs/fGaTnJeBFd7UoV22j3lzMLdM0Pw== +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass@^1.78.0: + version "1.78.0" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.78.0.tgz#cef369b2f9dc21ea1d2cf22c979f52365da60841" + integrity sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" -semver@^7.5.4, semver@^7.6.0: - version "7.6.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" - integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" -set-cookie-parser@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz#131921e50f62ff1a66a461d7d62d7b21d5d15a51" - integrity sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: + version "7.6.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" + integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-cookie-parser@^2.4.8, set-cookie-parser@^2.6.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz#ef5552b56dc01baae102acb5fc9fb8cd060c30f9" + integrity sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + 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" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== shebang-command@^2.0.0: version "2.0.0" @@ -1688,54 +5534,265 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -sirv@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.4.tgz#5dd9a725c578e34e449f332703eb2a74e46a29b0" - integrity sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ== +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== dependencies: - "@polka/url" "^1.0.0-next.24" - mrmime "^2.0.0" - totalist "^3.0.0" + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -sorcery@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.11.0.tgz#310c80ee993433854bb55bb9aa4003acd147fca8" - integrity sha512-J69LQ22xrQB1cIFJhPfgtLuI6BpWRiWu1Y3vSsIwK/eAScqJxd/+CJlUuHQRdX2C9NGFamq+KqNywGgaThwfHw== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.14" - buffer-crc32 "^0.2.5" - minimist "^1.2.0" - sander "^0.5.0" - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.2.0: +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== -strip-ansi@^6.0.1: +source-map-support@^0.5.21: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.7.0, source-map@^0.7.3: + version "0.7.4" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" + integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== + +space-separated-tokens@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" + integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== + +spdx-correct@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.20" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" + integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== + +ssri@^10.0.0: + version "10.0.6" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" + integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== + dependencies: + minipass "^7.0.3" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +stop-iteration-iterator@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" + integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== + dependencies: + internal-slot "^1.0.4" + +stream-shift@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" + integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== + +stream-slice@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/stream-slice/-/stream-slice-0.1.2.tgz#2dc4f4e1b936fb13f3eb39a2def1932798d07a4b" + integrity sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA== + +string-hash@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" + integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + name string-width-cjs + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.includes@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" + integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.matchall@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.repeat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" + integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.5" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringify-entities@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" + integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== + dependencies: + character-entities-html4 "^2.0.0" + character-entities-legacy "^3.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== dependencies: ansi-regex "^5.0.1" -strip-indent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" - integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== dependencies: - min-indent "^1.0.0" + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +style-to-object@^0.4.1: + version "0.4.4" + resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" + integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== + dependencies: + inline-style-parser "0.1.1" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -1748,79 +5805,61 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== -svelte-check@^3.6.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-3.8.0.tgz#e0850b876d3d32760465bfb26d06b32c4c9f98a1" - integrity sha512-7Nxn+3X97oIvMzYJ7t27w00qUf1Y52irE2RU2dQAd5PyvfGp4E7NLhFKVhb6PV2fx7dCRMpNKDIuazmGthjpSQ== - dependencies: - "@jridgewell/trace-mapping" "^0.3.17" - chokidar "^3.4.1" - fast-glob "^3.2.7" - import-fresh "^3.2.1" - picocolors "^1.0.0" - sade "^1.7.4" - svelte-preprocess "^5.1.3" - typescript "^5.0.3" +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== -"svelte-eslint-parser@>=0.36.0 <1.0.0": - version "0.36.0" - resolved "https://registry.yarnpkg.com/svelte-eslint-parser/-/svelte-eslint-parser-0.36.0.tgz#5390d86181180f2707c374b33c7d2fe42c1e1be2" - integrity sha512-/6YmUSr0FAVxW8dXNdIMydBnddPMHzaHirAZ7RrT21XYdgGGZMh0LQG6CZsvAFS4r2Y4ItUuCQc8TQ3urB30mQ== +tar-fs@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" + integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== dependencies: - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - postcss "^8.4.38" - postcss-scss "^4.0.9" + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.1.4" -svelte-hmr@^0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.16.0.tgz#9f345b7d1c1662f1613747ed7e82507e376c1716" - integrity sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA== - -svelte-preprocess@^5.1.3: - version "5.1.4" - resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz#14ada075c94bbd2b71c5ec70ff72f8ebe1c95b91" - integrity sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA== +tar-stream@^2.1.4: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== dependencies: - "@types/pug" "^2.0.6" - detect-indent "^6.1.0" - magic-string "^0.30.5" - sorcery "^0.11.0" - strip-indent "^3.0.0" + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" -svelte@^4.2.7: - version "4.2.18" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-4.2.18.tgz#33dbce74e83eb6dcc54dbea25f9758b1d8e8bb78" - integrity sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA== +tar@^6.1.11: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: - "@ampproject/remapping" "^2.2.1" - "@jridgewell/sourcemap-codec" "^1.4.15" - "@jridgewell/trace-mapping" "^0.3.18" - "@types/estree" "^1.0.1" - acorn "^8.9.0" - aria-query "^5.3.0" - axobject-query "^4.0.0" - code-red "^1.0.3" - css-tree "^2.3.1" - estree-walker "^3.0.3" - is-reference "^3.0.1" - locate-character "^3.0.0" - magic-string "^0.30.4" - periscopic "^3.1.0" + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tiny-glob@^0.2.9: - version "0.2.9" - resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" - integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== +through2@^2.0.3: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== dependencies: - globalyzer "0.1.0" - globrex "^0.1.2" + readable-stream "~2.3.6" + xtend "~4.0.1" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== to-regex-range@^5.0.1: version "5.0.1" @@ -1829,20 +5868,64 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -totalist@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.1.tgz#ba3a3d600c915b1a97872348f79c127475f6acf8" - integrity sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ== +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== -ts-api-utils@^1.3.0: +toml@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" + integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== + +trim-lines@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" + integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== + +trough@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" + integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== + +ts-api-utils@^1.0.1: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -tslib@^2.4.1: - version "2.6.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" - integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tsconfck@^3.0.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.3.tgz#a8202f51dab684c426314796cdb0bbd0fe0cdf80" + integrity sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ== + +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tsconfig-paths@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" + integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== + dependencies: + json5 "^2.2.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + +turbo-stream@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.3.0.tgz#b9188351588dacb927b7094c63e95a711cfd63d0" + integrity sha512-PhEr9mdexoVv+rJkQ3c8TjrN3DUghX37GNJkSMksoPR4KrXIPnM2MnqRt07sViIqX9IdlhrgtTSyjoVOASq6cg== type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" @@ -1851,19 +5934,210 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" -typescript-eslint@^8.0.0-alpha.20: - version "8.0.0-alpha.29" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.0.0-alpha.29.tgz#939c996f681e3ae6bb36c1159b1af16e4a84f2fd" - integrity sha512-NASQjd4tP+wukSs/Cj8vHjK/Ogk0nhVOr/kwzwg0AaXOWiz0g+rtE+lvqAaV+nhsCfMskuzKzc1TywFrhJlbvw== - dependencies: - "@typescript-eslint/eslint-plugin" "8.0.0-alpha.29" - "@typescript-eslint/parser" "8.0.0-alpha.29" - "@typescript-eslint/utils" "8.0.0-alpha.29" +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -typescript@^5.0.0, typescript@^5.0.3: - version "5.4.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" - integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript@^5.1.6: + version "5.5.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" + integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== + +ufo@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" + integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +uncontrollable@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" + integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== + dependencies: + "@babel/runtime" "^7.6.3" + "@types/react" ">=16.9.11" + invariant "^2.2.4" + react-lifecycles-compat "^3.0.4" + +uncontrollable@^8.0.1: + version "8.0.4" + resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-8.0.4.tgz#a0a8307f638795162fafd0550f4a1efa0f8c5eb6" + integrity sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ== + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +undici@^6.11.1: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" + integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== + +unified@^10.0.0: + version "10.1.2" + resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" + integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== + dependencies: + "@types/unist" "^2.0.0" + bail "^2.0.0" + extend "^3.0.0" + is-buffer "^2.0.0" + is-plain-obj "^4.0.0" + trough "^2.0.0" + vfile "^5.0.0" + +unique-filename@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" + integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== + dependencies: + unique-slug "^4.0.0" + +unique-slug@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" + integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== + dependencies: + imurmurhash "^0.1.4" + +unist-util-generated@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" + integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== + +unist-util-is@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" + integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz#8ac2480027229de76512079e377afbcabcfcce22" + integrity sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-position@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" + integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-remove-position@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz#a89be6ea72e23b1a402350832b02a91f6a9afe51" + integrity sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ== + dependencies: + "@types/unist" "^2.0.0" + unist-util-visit "^4.0.0" + +unist-util-stringify-position@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" + integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== + dependencies: + "@types/unist" "^2.0.0" + +unist-util-visit-parents@^5.1.1: + version "5.1.3" + resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" + integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + +unist-util-visit@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" + integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== + dependencies: + "@types/unist" "^2.0.0" + unist-util-is "^5.0.0" + unist-util-visit-parents "^5.1.1" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" uri-js@^4.2.2: version "4.4.1" @@ -1872,26 +6146,181 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" -util-deprecate@^1.0.2: +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -vite@^5.0.3: - version "5.2.13" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.13.tgz#945ababcbe3d837ae2479c29f661cd20bc5e1a80" - integrity sha512-SSq1noJfY9pR3I1TUENL3rQYDQCFqgD+lM6fTRAM8Nv6Lsg5hDLaXkjETVeBt+7vZBCMoibD+6IWnT2mJ+Zb/A== +util@^0.12.3: + version "0.12.5" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" + integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== dependencies: - esbuild "^0.20.1" - postcss "^8.4.38" - rollup "^4.13.0" + 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" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uvu@^0.5.0: + version "0.5.6" + resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" + integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== + dependencies: + dequal "^2.0.0" + diff "^5.0.0" + kleur "^4.0.3" + sade "^1.7.3" + +validate-npm-package-license@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validate-npm-package-name@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8" + integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +vfile-message@^3.0.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" + integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== + dependencies: + "@types/unist" "^2.0.0" + unist-util-stringify-position "^3.0.0" + +vfile@^5.0.0: + version "5.3.7" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" + integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== + dependencies: + "@types/unist" "^2.0.0" + is-buffer "^2.0.0" + unist-util-stringify-position "^3.0.0" + vfile-message "^3.0.0" + +vite-node@^1.2.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" + integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw== + dependencies: + cac "^6.7.14" + debug "^4.3.4" + pathe "^1.1.1" + picocolors "^1.0.0" + vite "^5.0.0" + +vite-tsconfig-paths@^4.2.1: + version "4.3.2" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9" + integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + +vite@^5.0.0, vite@^5.0.11, vite@^5.1.0: + version "5.4.3" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.3.tgz#771c470e808cb6732f204e1ee96c2ed65b97a0eb" + integrity sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" optionalDependencies: fsevents "~2.3.3" -vitefu@^0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.5.tgz#c1b93c377fbdd3e5ddd69840ea3aa70b40d90969" - integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q== +warning@^4.0.0, warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +web-encoding@1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.1.5.tgz#fc810cf7667364a6335c939913f5051d3e0c4864" + integrity sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA== + dependencies: + util "^0.12.3" + optionalDependencies: + "@zxing/text-encoding" "0.9.0" + +web-streams-polyfill@^3.1.1: + version "3.3.3" + resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" + integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" + integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== + dependencies: + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.2" + which-typed-array "^1.1.15" + +which-collection@^1.0.1, which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" which@^2.0.1: version "2.0.2" @@ -1900,22 +6329,72 @@ which@^2.0.1: dependencies: isexe "^2.0.0" +which@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1" + integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg== + dependencies: + isexe "^2.0.0" + word-wrap@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -yaml@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== +ws@^7.4.5: + version "7.5.10" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" + integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== + +xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^2.3.4: + version "2.5.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" + integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zwitch@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" + integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== From fa71f3fb2324e311384a99f1900c6ba211c749ff Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 5 Sep 2024 22:36:07 +0200 Subject: [PATCH 029/261] add Foxnouns.Frontend to solution in rider --- .idea/.idea.Foxnouns.NET/.idea/indexLayout.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml b/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml index 7b08163..a797372 100644 --- a/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml +++ b/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml @@ -1,7 +1,9 @@ - + + Foxnouns.Frontend + From acc54a55bc2b43dc47acaf1241fe50865fb940f4 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 6 Sep 2024 15:01:44 +0200 Subject: [PATCH 030/261] format(frontend): change line width to 100 --- .../Middleware/AuthorizationMiddleware.cs | 3 +- Foxnouns.Frontend/.prettierrc | 3 +- Foxnouns.Frontend/app/app.scss | 2 +- Foxnouns.Frontend/app/components/nav/Logo.tsx | 5 +- .../app/components/nav/Navbar.tsx | 59 +++++++++---------- Foxnouns.Frontend/app/entry.server.tsx | 26 ++------ Foxnouns.Frontend/app/lib/request.server.ts | 14 +---- Foxnouns.Frontend/app/root.tsx | 6 +- Foxnouns.Frontend/server.js | 9 +-- 9 files changed, 43 insertions(+), 84 deletions(-) diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 6d4bc49..1cab41c 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -18,7 +18,8 @@ public class AuthorizationMiddleware : IMiddleware var token = ctx.GetToken(); if (token == null) - throw new ApiError.Unauthorized("This endpoint requires an authenticated user.", ErrorCode.AuthenticationRequired); + throw new ApiError.Unauthorized("This endpoint requires an authenticated user.", + ErrorCode.AuthenticationRequired); 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()), ErrorCode.MissingScopes); diff --git a/Foxnouns.Frontend/.prettierrc b/Foxnouns.Frontend/.prettierrc index c959087..cb96cd0 100644 --- a/Foxnouns.Frontend/.prettierrc +++ b/Foxnouns.Frontend/.prettierrc @@ -1,3 +1,4 @@ { - "useTabs": true + "useTabs": true, + "printWidth": 100 } diff --git a/Foxnouns.Frontend/app/app.scss b/Foxnouns.Frontend/app/app.scss index 19155ce..c7b7086 100644 --- a/Foxnouns.Frontend/app/app.scss +++ b/Foxnouns.Frontend/app/app.scss @@ -18,4 +18,4 @@ $font-family-sans-serif: @import "@fontsource/firago/400.css"; @import "@fontsource/firago/400-italic.css"; -@import "@fontsource/firago/700.css"; \ No newline at end of file +@import "@fontsource/firago/700.css"; diff --git a/Foxnouns.Frontend/app/components/nav/Logo.tsx b/Foxnouns.Frontend/app/components/nav/Logo.tsx index 216eb54..f1c8e40 100644 --- a/Foxnouns.Frontend/app/components/nav/Logo.tsx +++ b/Foxnouns.Frontend/app/components/nav/Logo.tsx @@ -13,10 +13,7 @@ export default function Logo() { - + + - + - - - Theme - - } - align="end" - > - - Automatic - - - Dark mode - - - Light mode - - - ); } diff --git a/Foxnouns.Frontend/app/entry.server.tsx b/Foxnouns.Frontend/app/entry.server.tsx index 5e213e2..7b1af99 100644 --- a/Foxnouns.Frontend/app/entry.server.tsx +++ b/Foxnouns.Frontend/app/entry.server.tsx @@ -25,18 +25,8 @@ export default function handleRequest( loadContext: AppLoadContext, ) { return isbot(request.headers.get("user-agent") || "") - ? handleBotRequest( - request, - responseStatusCode, - responseHeaders, - remixContext, - ) - : handleBrowserRequest( - request, - responseStatusCode, - responseHeaders, - remixContext, - ); + ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) + : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); } function handleBotRequest( @@ -48,11 +38,7 @@ function handleBotRequest( return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onAllReady() { shellRendered = true; @@ -98,11 +84,7 @@ function handleBrowserRequest( return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( - , + , { onShellReady() { shellRendered = true; diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 144edbe..d7add9b 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -35,15 +35,11 @@ export default async function serverRequest( } as ApiError; } - if (resp.status < 200 || resp.status >= 400) - throw (await resp.json()) as ApiError; + if (resp.status < 200 || resp.status >= 400) throw (await resp.json()) as ApiError; return (await resp.json()) as T; } -export function getCookie( - req: Request, - cookieName: string, -): string | undefined { +export function getCookie(req: Request, cookieName: string): string | undefined { const header = req.headers.get("Cookie"); if (!header) return undefined; @@ -53,11 +49,7 @@ export function getCookie( const YEAR = 365 * 86400; -export const writeCookie = ( - cookieName: string, - value: string, - maxAge: number | undefined = YEAR, -) => +export const writeCookie = (cookieName: string, value: string, maxAge: number | undefined = YEAR) => serializeCookie(cookieName, value, { maxAge, path: "/", diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index d1c62f3..de5e053 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -32,11 +32,7 @@ export const loader: LoaderFunction = async ({ request }) => { const user = await serverRequest("GET", "/users/@me", { token }); meUser = user; - settings = await serverRequest( - "GET", - "/users/@me/settings", - { token }, - ); + settings = await serverRequest("GET", "/users/@me/settings", { token }); } catch (e) { // If we get an unauthorized error, clear the token, as it's not valid anymore. if ((e as ApiError).code === ErrorCode.AuthenticationRequired) { diff --git a/Foxnouns.Frontend/server.js b/Foxnouns.Frontend/server.js index ed053cf..e9e6c4b 100644 --- a/Foxnouns.Frontend/server.js +++ b/Foxnouns.Frontend/server.js @@ -31,10 +31,7 @@ if (viteDevServer) { app.use(viteDevServer.middlewares); } else { // Vite fingerprints its assets so we can cache forever. - app.use( - "/assets", - express.static("build/client/assets", { immutable: true, maxAge: "1y" }), - ); + app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" })); } // Everything else (like favicon.ico) is cached for an hour. You may want to be @@ -47,6 +44,4 @@ app.use(morgan("tiny")); app.all("*", remixHandler); const port = env.PORT || 3000; -app.listen(port, () => - console.log(`Express server listening at http://localhost:${port}`), -); +app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`)); From 344a0071e5492f236c386854b4feeff8dadd2128 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Sep 2024 14:37:59 +0200 Subject: [PATCH 031/261] start (actual) email auth, add CancellationToken to most async methods --- Foxnouns.Backend/Config.cs | 8 +++ .../Authentication/AuthController.cs | 4 +- .../Authentication/DiscordAuthController.cs | 30 ++++----- .../Authentication/EmailAuthController.cs | 53 ++++++++++++++-- .../Controllers/MembersController.cs | 20 +++--- .../Controllers/UsersController.cs | 18 +++--- .../Database/DatabaseQueryExtensions.cs | 29 +++++---- .../Extensions/AvatarObjectExtensions.cs | 8 +-- .../Extensions/KeyCacheExtensions.cs | 26 ++++++-- .../Extensions/WebApplicationExtensions.cs | 63 ++++++++++--------- Foxnouns.Backend/Foxnouns.Backend.csproj | 45 ++++++------- .../Mailables/AccountCreationMailable.cs | 19 ++++++ Foxnouns.Backend/Program.cs | 3 +- Foxnouns.Backend/Services/AuthService.cs | 39 +++++++++--- Foxnouns.Backend/Services/KeyCacheService.cs | 28 ++++----- Foxnouns.Backend/Services/MailService.cs | 24 +++++++ .../Services/ObjectStorageService.cs | 17 ++--- .../Services/RemoteAuthService.cs | 10 +-- Foxnouns.Backend/Views/Mail/Example.cshtml | 15 +++++ .../Views/Mail/_ViewImports.cshtml | 3 + Foxnouns.Backend/Views/Mail/_ViewStart.cshtml | 3 + Foxnouns.Backend/config.example.ini | 12 ++++ 22 files changed, 325 insertions(+), 152 deletions(-) create mode 100644 Foxnouns.Backend/Mailables/AccountCreationMailable.cs create mode 100644 Foxnouns.Backend/Services/MailService.cs create mode 100644 Foxnouns.Backend/Views/Mail/Example.cshtml create mode 100644 Foxnouns.Backend/Views/Mail/_ViewImports.cshtml create mode 100644 Foxnouns.Backend/Views/Mail/_ViewStart.cshtml diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 132a28c..96d724b 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Serilog.Events; namespace Foxnouns.Backend; @@ -15,6 +16,7 @@ public class Config public LoggingConfig Logging { get; init; } = new(); public DatabaseConfig Database { get; init; } = new(); public StorageConfig Storage { 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(); @@ -46,6 +48,12 @@ public class Config 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; diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index db2e21f..2a0f3d4 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -13,13 +13,13 @@ public class AuthController(Config config, KeyCacheService keyCacheSvc, ILogger [HttpPost("urls")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task UrlsAsync() + 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); - var state = HttpUtility.UrlEncode(await keyCacheSvc.GenerateAuthStateAsync()); + var state = HttpUtility.UrlEncode(await keyCacheSvc.GenerateAuthStateAsync(ct)); string? discord = null; if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) discord = diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index d717aba..6729fc0 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -27,31 +27,31 @@ public class DiscordAuthController( // leaving it here for documentation purposes [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req) + public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req, CancellationToken ct = default) { CheckRequirements(); - await keyCacheSvc.ValidateAuthStateAsync(req.State); + await keyCacheSvc.ValidateAuthStateAsync(req.State, ct); - var remoteUser = await remoteAuthSvc.RequestDiscordTokenAsync(req.Code, req.State); - var user = await authSvc.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); - if (user != null) return Ok(await GenerateUserTokenAsync(user)); + var remoteUser = await remoteAuthSvc.RequestDiscordTokenAsync(req.Code, req.State, ct); + var user = await authSvc.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id, ct: ct); + if (user != null) return Ok(await GenerateUserTokenAsync(user,ct)); _logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, remoteUser.Id); var ticket = AuthUtils.RandomToken(); - await keyCacheSvc.SetKeyAsync($"discord:{ticket}", remoteUser, Duration.FromMinutes(20)); + await keyCacheSvc.SetKeyAsync($"discord:{ticket}", remoteUser, Duration.FromMinutes(20), ct); return Ok(new AuthController.CallbackResponse(false, ticket, remoteUser.Username)); } [HttpPost("register")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req) + public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req, CancellationToken ct = default) { - var remoteUser = await keyCacheSvc.GetKeyAsync($"discord:{req.Ticket}"); + var remoteUser = await keyCacheSvc.GetKeyAsync($"discord:{req.Ticket}",ct:ct); 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)) + if (await db.AuthMethods.AnyAsync(a => a.AuthType == AuthType.Discord && a.RemoteId == remoteUser.Id, ct)) { _logger.Error("Discord user {Id} has valid ticket but is already linked to an existing account", remoteUser.Id); @@ -59,14 +59,14 @@ public class DiscordAuthController( } var user = await authSvc.CreateUserWithRemoteAuthAsync(req.Username, AuthType.Discord, remoteUser.Id, - remoteUser.Username); + remoteUser.Username, ct: ct); - return Ok(await GenerateUserTokenAsync(user)); + return Ok(await GenerateUserTokenAsync(user, ct)); } - private async Task GenerateUserTokenAsync(User user) + private async Task GenerateUserTokenAsync(User user, CancellationToken ct = default) { - var frontendApp = await db.GetFrontendApplicationAsync(); + var frontendApp = await db.GetFrontendApplicationAsync(ct); _logger.Debug("Logging user {Id} in with Discord", user.Id); var (tokenStr, token) = @@ -75,10 +75,10 @@ public class DiscordAuthController( _logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); - await db.SaveChangesAsync(); + await db.SaveChangesAsync(ct); return new AuthController.AuthResponse( - await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false), + await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), tokenStr, token.ExpiresAt ); diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 19dfc2f..317d62c 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -1,6 +1,10 @@ using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; @@ -9,21 +13,56 @@ namespace Foxnouns.Backend.Controllers.Authentication; public class EmailAuthController( DatabaseContext db, AuthService authSvc, + MailService mailSvc, + KeyCacheService keyCacheSvc, UserRendererService userRendererSvc, IClock clock, ILogger logger) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); + [HttpPost("register")] + public async Task RegisterAsync([FromBody] RegisterRequest req, CancellationToken ct = default) + { + if (!req.Email.Contains('@')) throw new ApiError.BadRequest("Email is invalid", "email", req.Email); + + var state = await keyCacheSvc.GenerateRegisterEmailStateAsync(req.Email, userId: null, ct); + if (await db.AuthMethods.AnyAsync(a => a.AuthType == AuthType.Email && a.RemoteId == req.Email, ct)) + return NoContent(); + + mailSvc.QueueAccountCreationEmail(req.Email, state); + return NoContent(); + } + + [HttpPost("callback")] + public async Task CallbackAsync([FromBody] CallbackRequest req, CancellationToken ct = default) + { + var state = await keyCacheSvc.GetRegisterEmailStateAsync(req.State, ct); + if (state == null) throw new ApiError.BadRequest("Invalid state", "state", req.State); + + if (state.ExistingUserId != null) + { + var authMethod = + await authSvc.AddAuthMethodAsync(state.ExistingUserId.Value, AuthType.Email, state.Email, ct: ct); + _logger.Debug("Added email auth {AuthId} for user {UserId}", authMethod.Id, state.ExistingUserId); + return NoContent(); + } + + var ticket = AuthUtils.RandomToken(); + await keyCacheSvc.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); + + return Ok(new AuthController.CallbackResponse(false, ticket, state.Email)); + } + [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task LoginAsync([FromBody] LoginRequest req) + public async Task LoginAsync([FromBody] LoginRequest req, CancellationToken ct = default) { - var (user, authenticationResult) = await authSvc.AuthenticateUserAsync(req.Email, req.Password); + var (user, authenticationResult) = await authSvc.AuthenticateUserAsync(req.Email, req.Password, ct); if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired) throw new NotImplementedException("MFA is not implemented yet"); - var frontendApp = await db.GetFrontendApplicationAsync(); + var frontendApp = await db.GetFrontendApplicationAsync(ct); _logger.Debug("Logging user {Id} in with email and password", user.Id); @@ -33,14 +72,18 @@ public class EmailAuthController( _logger.Debug("Generated token {TokenId} for {UserId}", token.Id, user.Id); - await db.SaveChangesAsync(); + await db.SaveChangesAsync(ct); return Ok(new AuthController.AuthResponse( - await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false), + await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), tokenStr, token.ExpiresAt )); } public record LoginRequest(string Email, string Password); + + public record RegisterRequest(string Email); + + public record CallbackRequest(string State); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 19a9569..5f09e35 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -25,24 +25,24 @@ public class MembersController( [HttpGet] [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task GetMembersAsync(string userRef) + public async Task GetMembersAsync(string userRef, CancellationToken ct = default) { - var user = await db.ResolveUserAsync(userRef, CurrentToken); + var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); return Ok(await memberRendererService.RenderUserMembersAsync(user, CurrentToken)); } [HttpGet("{memberRef}")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task GetMemberAsync(string userRef, string memberRef) + public async Task GetMemberAsync(string userRef, string memberRef, CancellationToken ct = default) { - var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken); + var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct); return Ok(memberRendererService.RenderMember(member, CurrentToken)); } [HttpPost("/api/v2/users/@me/members")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize("member.create")] - public async Task CreateMemberAsync([FromBody] CreateMemberRequest req) + public async Task CreateMemberAsync([FromBody] CreateMemberRequest req, CancellationToken ct = default) { ValidationUtils.Validate([ ("name", ValidationUtils.ValidateMemberName(req.Name)), @@ -71,7 +71,7 @@ public class MembersController( try { - await db.SaveChangesAsync(); + await db.SaveChangesAsync(ct); } catch (UniqueConstraintException) { @@ -88,18 +88,18 @@ public class MembersController( [HttpDelete("/api/v2/users/@me/members/{memberRef}")] [Authorize("member.update")] - public async Task DeleteMemberAsync(string memberRef) + public async Task DeleteMemberAsync(string memberRef, CancellationToken ct = default) { - var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef, ct); var deleteCount = await db.Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id) - .ExecuteDeleteAsync(); + .ExecuteDeleteAsync(ct); if (deleteCount == 0) { _logger.Warning("Successfully resolved member {Id} but could not delete them", member.Id); return NoContent(); } - await db.SaveChangesAsync(); + await db.SaveChangesAsync(ct); if (member.Avatar != null) await objectStorage.DeleteMemberAvatarAsync(member.Id, member.Avatar); return NoContent(); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index a2c6219..8b9299a 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -36,10 +36,10 @@ public class UsersController( [HttpPatch("@me")] [Authorize("user.update")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task UpdateUserAsync([FromBody] UpdateUserRequest req) + public async Task UpdateUserAsync([FromBody] UpdateUserRequest req, CancellationToken ct = default) { - await using var tx = await db.Database.BeginTransactionAsync(); - var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id); + await using var tx = await db.Database.BeginTransactionAsync(ct); + var 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) @@ -76,20 +76,20 @@ public class UsersController( queue.QueueInvocableWithPayload( new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); - await db.SaveChangesAsync(); - await tx.CommitAsync(); + await db.SaveChangesAsync(ct); + await tx.CommitAsync(ct); return Ok(await userRendererService.RenderUserAsync(user, CurrentUser, renderMembers: false, - renderAuthMethods: false)); + renderAuthMethods: false, ct: ct)); } [HttpPatch("@me/custom-preferences")] [Authorize("user.update")] [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task UpdateCustomPreferencesAsync([FromBody] List req) + public async Task UpdateCustomPreferencesAsync([FromBody] List req, CancellationToken ct = default) { ValidationUtils.Validate(ValidateCustomPreferences(req)); - var user = await db.ResolveUserAsync(CurrentUser!.Id); + var user = await db.ResolveUserAsync(CurrentUser!.Id, ct); var preferences = user.CustomPreferences.Where(x => req.Any(r => r.Id == x.Key)).ToDictionary(); foreach (var r in req) @@ -119,7 +119,7 @@ public class UsersController( } user.CustomPreferences = preferences; - await db.SaveChangesAsync(); + await db.SaveChangesAsync(ct); return Ok(user.CustomPreferences); } diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index b6ccaaa..76043d7 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -36,34 +36,36 @@ public static class DatabaseQueryExtensions 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) + public static async Task ResolveUserAsync(this DatabaseContext context, Snowflake id, + CancellationToken ct = default) { var user = await context.Users .Where(u => !u.Deleted) - .FirstOrDefaultAsync(u => u.Id == id); + .FirstOrDefaultAsync(u => u.Id == id, ct); 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) + public static async Task ResolveMemberAsync(this DatabaseContext context, Snowflake id, + CancellationToken ct = default) { var member = await context.Members .Include(m => m.User) .Where(m => !m.User.Deleted) - .FirstOrDefaultAsync(m => m.Id == id); + .FirstOrDefaultAsync(m => m.Id == id, ct); 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) + Token? token, CancellationToken ct = default) { - var user = await context.ResolveUserAsync(userRef, token); - return await context.ResolveMemberAsync(user.Id, memberRef); + var user = await context.ResolveUserAsync(userRef, token, ct); + return await context.ResolveMemberAsync(user.Id, memberRef, ct); } public static async Task ResolveMemberAsync(this DatabaseContext context, Snowflake userId, - string memberRef) + string memberRef, CancellationToken ct = default) { Member? member; if (Snowflake.TryParse(memberRef, out var snowflake)) @@ -71,21 +73,22 @@ public static class DatabaseQueryExtensions member = await context.Members .Include(m => m.User) .Where(m => !m.User.Deleted) - .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId); + .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId, ct); if (member != null) return member; } member = await context.Members .Include(m => m.User) .Where(m => !m.User.Deleted) - .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == 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.", code: ErrorCode.MemberNotFound); } - public static async Task GetFrontendApplicationAsync(this DatabaseContext context) + public static async Task GetFrontendApplicationAsync(this DatabaseContext context, + CancellationToken ct = default) { - var app = await context.Applications.FirstOrDefaultAsync(a => a.Id == new Snowflake(0)); + var app = await context.Applications.FirstOrDefaultAsync(a => a.Id == new Snowflake(0), ct); if (app != null) return app; app = new Application @@ -99,7 +102,7 @@ public static class DatabaseQueryExtensions }; context.Add(app); - await context.SaveChangesAsync(); + await context.SaveChangesAsync(ct); return app; } } \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index f7c2b6f..2d0fd16 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -14,12 +14,12 @@ public static class AvatarObjectExtensions private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"]; public static async Task - DeleteMemberAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash) => - await objectStorage.RemoveObjectAsync(MemberAvatarUpdateInvocable.Path(id, hash)); + DeleteMemberAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash, CancellationToken ct = default) => + await objectStorage.RemoveObjectAsync(MemberAvatarUpdateInvocable.Path(id, hash), ct); public static async Task - DeleteUserAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash) => - await objectStorage.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash)); + DeleteUserAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash, CancellationToken ct = default) => + await objectStorage.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); public static async Task ConvertBase64UriToAvatar(this string uri) { diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index 3b8e35c..eb1cecc 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -1,3 +1,4 @@ +using Foxnouns.Backend.Database; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using NodaTime; @@ -6,16 +7,31 @@ namespace Foxnouns.Backend.Extensions; public static class KeyCacheExtensions { - public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheSvc) + public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheSvc, CancellationToken ct = default) { var state = AuthUtils.RandomToken(); - await keyCacheSvc.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); + await keyCacheSvc.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct); return state; } - public static async Task ValidateAuthStateAsync(this KeyCacheService keyCacheSvc, string state) + public static async Task ValidateAuthStateAsync(this KeyCacheService keyCacheSvc, string state, CancellationToken ct = default) { - var val = await keyCacheSvc.GetKeyAsync($"oauth_state:{state}", delete: true); + var val = await keyCacheSvc.GetKeyAsync($"oauth_state:{state}", delete: true, ct); if (val == null) throw new ApiError.BadRequest("Invalid OAuth state"); } -} \ No newline at end of file + + public static async Task GenerateRegisterEmailStateAsync(this KeyCacheService keyCacheSvc, string email, + Snowflake? userId = null, CancellationToken ct = default) + { + var state = AuthUtils.RandomToken(); + await keyCacheSvc.SetKeyAsync($"email_state:{state}", new RegisterEmailState(email, userId), + Duration.FromDays(1), ct); + return state; + } + + public static async Task GetRegisterEmailStateAsync(this KeyCacheService keyCacheSvc, + string state, CancellationToken ct = default) => + await keyCacheSvc.GetKeyAsync($"email_state:{state}", delete: true, ct); +} + +public record RegisterEmailState(string Email, Snowflake? ExistingUserId); \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 1f1ee31..014eeb1 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -70,38 +70,43 @@ public static class WebApplicationExtensions } /// - /// Adds required services to the IServiceCollection. + /// 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 IServiceCollection services, Config config) + public static IServiceCollection AddServices(this WebApplicationBuilder builder, Config config) { - services - .AddQueue() - .AddDbContext() - .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() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - .AddScoped() - // Background services - .AddHostedService() - // Transient jobs - .AddTransient() - .AddTransient(); - - if (!config.Logging.EnableMetrics) - services.AddHostedService(); - - return services; + builder.Host.ConfigureServices((ctx, services) => + { + services + .AddQueue() + .AddMailer(ctx.Configuration) + .AddDbContext() + .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() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + // Background services + .AddHostedService() + // Transient jobs + .AddTransient() + .AddTransient(); + + if (!config.Logging.EnableMetrics) + services.AddHostedService(); + }); + + return builder.Services; } public static IServiceCollection AddCustomMiddleware(this IServiceCollection services) => services diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index c22fdf4..92abc6a 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -6,31 +6,32 @@ - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -39,6 +40,6 @@ - + diff --git a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs new file mode 100644 index 0000000..b55c9e6 --- /dev/null +++ b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs @@ -0,0 +1,19 @@ +using Coravel.Mailer.Mail; + +namespace Foxnouns.Backend.Mailables; + +public class AccountCreationMailable(Config config, AccountCreationMailableView view) : Mailable +{ + public override void Build() + { + To(view.To) + .From(config.EmailAuth.From!) + .View("~/Views/Mail/AccountCreation.cshtml", view); + } +} + +public class AccountCreationMailableView +{ + public required string To { get; init; } + public required string Code { get; init; } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 911c895..65e508a 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -58,8 +58,7 @@ JsonConvert.DefaultSettings = () => new JsonSerializerSettings } }; -builder.Services - .AddServices(config) +builder.AddServices(config) .AddCustomMiddleware() .AddEndpointsApiExplorer() .AddSwaggerGen(); diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index c75f926..8d6052d 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -42,11 +42,11 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// 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) + string remoteUsername, FediverseApplication? instance = null, CancellationToken ct = default) { AssertValidAuthType(authType, instance); - if (await db.Users.AnyAsync(u => u.Username == 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 @@ -76,20 +76,20 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// 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) + public async Task<(User, EmailAuthenticationResult)> AuthenticateUserAsync(string email, string password, CancellationToken ct = default) { var user = await db.Users.FirstOrDefaultAsync(u => - u.AuthMethods.Any(a => a.AuthType == AuthType.Email && a.RemoteId == email)); + 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"); - var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password)); + var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password), ct); if (pwResult == PasswordVerificationResult.Failed) throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) { - user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password)); - await db.SaveChangesAsync(); + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); + await db.SaveChangesAsync(ct); } return (user, EmailAuthenticationResult.AuthSuccessful); @@ -108,17 +108,38 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// 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) + 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)); + a.AuthType == authType && a.RemoteId == remoteId && a.FediverseApplication == instance), ct); + } + + public async Task AddAuthMethodAsync(Snowflake userId, AuthType authType, string remoteId, + string? remoteUsername = null, + CancellationToken ct = default) + { + AssertValidAuthType(authType, null); + + var authMethod = new AuthMethod + { + Id = snowflakeGenerator.GenerateSnowflake(), + AuthType = authType, + RemoteId = remoteId, + 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) diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index bd8a862..d8c5434 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -11,10 +11,10 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) { private readonly ILogger _logger = logger.ForContext(); - public Task SetKeyAsync(string key, string value, Duration expireAfter) => - SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter); + public Task SetKeyAsync(string key, string value, Duration expireAfter, CancellationToken ct = default) => + SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter, ct); - public async Task SetKeyAsync(string key, string value, Instant expires) + public async Task SetKeyAsync(string key, string value, Instant expires, CancellationToken ct = default) { db.TemporaryKeys.Add(new TemporaryKey { @@ -22,18 +22,18 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) Key = key, Value = value, }); - await db.SaveChangesAsync(); + await db.SaveChangesAsync(ct); } - public async Task GetKeyAsync(string key, bool delete = false) + public async Task GetKeyAsync(string key, bool delete = false, CancellationToken ct = default) { - var value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key); + var value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct); if (value == null) return null; if (delete) { - await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(); - await db.SaveChangesAsync(); + await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct); + await db.SaveChangesAsync(ct); } return value.Value; @@ -45,18 +45,18 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) if (count != 0) _logger.Information("Removed {Count} expired keys from the database", count); } - public Task SetKeyAsync(string key, T obj, Duration expiresAt) where T : class => - SetKeyAsync(key, obj, clock.GetCurrentInstant() + expiresAt); + public Task SetKeyAsync(string key, T obj, Duration expiresAt, CancellationToken ct = default) where T : class => + SetKeyAsync(key, obj, clock.GetCurrentInstant() + expiresAt, ct); - public async Task SetKeyAsync(string key, T obj, Instant expires) where T : class + public async Task SetKeyAsync(string key, T obj, Instant expires, CancellationToken ct = default) where T : class { var value = JsonConvert.SerializeObject(obj); - await SetKeyAsync(key, value, expires); + await SetKeyAsync(key, value, expires, ct); } - public async Task GetKeyAsync(string key, bool delete = false) where T : class + public async Task GetKeyAsync(string key, bool delete = false, CancellationToken ct = default) where T : class { - var value = await GetKeyAsync(key, delete: false); + var value = await GetKeyAsync(key, delete, ct); return value == null ? default : JsonConvert.DeserializeObject(value); } } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs new file mode 100644 index 0000000..271d41c --- /dev/null +++ b/Foxnouns.Backend/Services/MailService.cs @@ -0,0 +1,24 @@ +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) + { + _logger.Debug("Sending account creation email to {ToEmail}", to); + + queue.QueueAsyncTask(async () => + { + await mailer.SendAsync(new AccountCreationMailable(config, new AccountCreationMailableView + { + To = to, + Code = code + })); + }); + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs index de60074..45e9678 100644 --- a/Foxnouns.Backend/Services/ObjectStorageService.cs +++ b/Foxnouns.Backend/Services/ObjectStorageService.cs @@ -8,12 +8,13 @@ public class ObjectStorageService(ILogger logger, Config config, IMinioClient mi { private readonly ILogger _logger = logger.ForContext(); - public async Task RemoveObjectAsync(string path) + public async Task RemoveObjectAsync(string path, CancellationToken ct = default) { _logger.Debug("Deleting object at path {Path}", path); try { - await minio.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path)); + await minio.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path), + ct); } catch (InvalidObjectNameException) { @@ -21,17 +22,17 @@ public class ObjectStorageService(ILogger logger, Config config, IMinioClient mi } } - public async Task PutObjectAsync(string path, Stream data, string contentType) + 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 minio.PutObjectAsync(new PutObjectArgs() - .WithBucket(config.Storage.Bucket) - .WithObject(path) - .WithObjectSize(data.Length) - .WithStreamData(data) - .WithContentType(contentType) + .WithBucket(config.Storage.Bucket) + .WithObject(path) + .WithObjectSize(data.Length) + .WithStreamData(data) + .WithContentType(contentType), ct ); } } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/RemoteAuthService.cs b/Foxnouns.Backend/Services/RemoteAuthService.cs index 48d6b84..389412f 100644 --- a/Foxnouns.Backend/Services/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/RemoteAuthService.cs @@ -10,7 +10,7 @@ public class RemoteAuthService(Config config) 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, string state) + public async Task RequestDiscordTokenAsync(string code, string state, CancellationToken ct = default) { var redirectUri = $"{config.BaseUrl}/auth/login/discord"; var resp = await _httpClient.PostAsync(_discordTokenUri, new FormUrlEncodedContent( @@ -22,17 +22,17 @@ public class RemoteAuthService(Config config) { "code", code }, { "redirect_uri", redirectUri } } - )); + ), ct); resp.EnsureSuccessStatusCode(); - var token = await resp.Content.ReadFromJsonAsync(); + var 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.token_type} {token.access_token}"); - var resp2 = await _httpClient.SendAsync(req); + var resp2 = await _httpClient.SendAsync(req, ct); resp2.EnsureSuccessStatusCode(); - var user = await resp2.Content.ReadFromJsonAsync(); + var user = await resp2.Content.ReadFromJsonAsync(ct); if (user == null) throw new FoxnounsError("Discord user response was null"); return new RemoteUser(user.id, user.username); diff --git a/Foxnouns.Backend/Views/Mail/Example.cshtml b/Foxnouns.Backend/Views/Mail/Example.cshtml new file mode 100644 index 0000000..e1acaaa --- /dev/null +++ b/Foxnouns.Backend/Views/Mail/Example.cshtml @@ -0,0 +1,15 @@ +@{ + ViewBag.Heading = "Welcome!"; + ViewBag.Preview = "Example Email"; +} + +

+ Let's see what you can build! + To render a button inside your email, use the EmailLinkButton component: + @await Component.InvokeAsync("EmailLinkButton", new { text = "Click Me!", url = "https://www.google.com" }) +

+ +@section links +{ +
Coravel +} diff --git a/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml b/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml new file mode 100644 index 0000000..8c050b2 --- /dev/null +++ b/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using Foxnouns.Backend +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper *, Coravel.Mailer.ViewComponents diff --git a/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml b/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml new file mode 100644 index 0000000..1d54d45 --- /dev/null +++ b/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/Areas/Coravel/Pages/Mail/Template.cshtml"; +} diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index 27e5cbf..edc8a28 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -41,6 +41,18 @@ 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] +Driver = SMTP +Host = localhost +Port = 1025 +Username = smtp-username +Password = smtp-password + [DiscordAuth] ClientId = ClientSecret = From c77ee660ca7a3ce48ab49aad4df30cc0de317293 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Sep 2024 14:50:00 +0200 Subject: [PATCH 032/261] refactor: more consistent field names, also in STYLE.md --- .../Authentication/AuthController.cs | 4 ++-- .../Authentication/DiscordAuthController.cs | 24 +++++++++---------- .../Authentication/EmailAuthController.cs | 24 +++++++++---------- .../Controllers/DebugController.cs | 10 ++++---- .../Controllers/MembersController.cs | 12 +++++----- .../Controllers/UsersController.cs | 6 ++--- .../Extensions/AvatarObjectExtensions.cs | 8 +++---- .../Extensions/KeyCacheExtensions.cs | 18 +++++++------- .../Jobs/MemberAvatarUpdateInvocable.cs | 8 +++---- .../Jobs/UserAvatarUpdateInvocable.cs | 8 +++---- .../Services/MetricsCollectionService.cs | 4 ++-- .../Services/ObjectStorageService.cs | 7 +++--- .../Services/UserRendererService.cs | 4 ++-- STYLE.md | 20 ++++++++++++---- 14 files changed, 86 insertions(+), 71 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 2a0f3d4..92267e6 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -7,7 +7,7 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth")] -public class AuthController(Config config, KeyCacheService keyCacheSvc, ILogger logger) : ApiControllerBase +public class AuthController(Config config, KeyCacheService keyCache, ILogger logger) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); @@ -19,7 +19,7 @@ public class AuthController(Config config, KeyCacheService keyCacheSvc, ILogger config.DiscordAuth.Enabled, config.GoogleAuth.Enabled, config.TumblrAuth.Enabled); - var state = HttpUtility.UrlEncode(await keyCacheSvc.GenerateAuthStateAsync(ct)); + var state = HttpUtility.UrlEncode(await keyCache.GenerateAuthStateAsync(ct)); string? discord = null; if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) discord = diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 6729fc0..068c8e9 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -15,10 +15,10 @@ public class DiscordAuthController( ILogger logger, IClock clock, DatabaseContext db, - KeyCacheService keyCacheSvc, - AuthService authSvc, - RemoteAuthService remoteAuthSvc, - UserRendererService userRendererSvc) : ApiControllerBase + KeyCacheService keyCacheService, + AuthService authService, + RemoteAuthService remoteAuthService, + UserRendererService userRenderer) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); @@ -30,17 +30,17 @@ public class DiscordAuthController( public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req, CancellationToken ct = default) { CheckRequirements(); - await keyCacheSvc.ValidateAuthStateAsync(req.State, ct); + await keyCacheService.ValidateAuthStateAsync(req.State, ct); - var remoteUser = await remoteAuthSvc.RequestDiscordTokenAsync(req.Code, req.State, ct); - var user = await authSvc.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id, ct: ct); + var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code, req.State, ct); + var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id, ct: ct); if (user != null) return Ok(await GenerateUserTokenAsync(user,ct)); _logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, remoteUser.Id); var ticket = AuthUtils.RandomToken(); - await keyCacheSvc.SetKeyAsync($"discord:{ticket}", remoteUser, Duration.FromMinutes(20), ct); + await keyCacheService.SetKeyAsync($"discord:{ticket}", remoteUser, Duration.FromMinutes(20), ct); return Ok(new AuthController.CallbackResponse(false, ticket, remoteUser.Username)); } @@ -49,7 +49,7 @@ public class DiscordAuthController( [ProducesResponseType(StatusCodes.Status200OK)] public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req, CancellationToken ct = default) { - var remoteUser = await keyCacheSvc.GetKeyAsync($"discord:{req.Ticket}",ct:ct); + var remoteUser = await keyCacheService.GetKeyAsync($"discord:{req.Ticket}",ct:ct); 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, ct)) { @@ -58,7 +58,7 @@ public class DiscordAuthController( throw new FoxnounsError("Discord ticket was issued for user with existing link"); } - var user = await authSvc.CreateUserWithRemoteAuthAsync(req.Username, AuthType.Discord, remoteUser.Id, + var user = await authService.CreateUserWithRemoteAuthAsync(req.Username, AuthType.Discord, remoteUser.Id, remoteUser.Username, ct: ct); return Ok(await GenerateUserTokenAsync(user, ct)); @@ -70,7 +70,7 @@ public class DiscordAuthController( _logger.Debug("Logging user {Id} in with Discord", user.Id); var (tokenStr, token) = - authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); + authService.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); _logger.Debug("Generated token {TokenId} for {UserId}", user.Id, token.Id); @@ -78,7 +78,7 @@ public class DiscordAuthController( await db.SaveChangesAsync(ct); return new AuthController.AuthResponse( - await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), + await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), tokenStr, token.ExpiresAt ); diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 317d62c..4e006af 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -12,10 +12,10 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/email")] public class EmailAuthController( DatabaseContext db, - AuthService authSvc, - MailService mailSvc, - KeyCacheService keyCacheSvc, - UserRendererService userRendererSvc, + AuthService authService, + MailService mailService, + KeyCacheService keyCacheService, + UserRendererService userRenderer, IClock clock, ILogger logger) : ApiControllerBase { @@ -26,30 +26,30 @@ public class EmailAuthController( { if (!req.Email.Contains('@')) throw new ApiError.BadRequest("Email is invalid", "email", req.Email); - var state = await keyCacheSvc.GenerateRegisterEmailStateAsync(req.Email, userId: null, ct); + var state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, userId: null, ct); if (await db.AuthMethods.AnyAsync(a => a.AuthType == AuthType.Email && a.RemoteId == req.Email, ct)) return NoContent(); - mailSvc.QueueAccountCreationEmail(req.Email, state); + mailService.QueueAccountCreationEmail(req.Email, state); return NoContent(); } [HttpPost("callback")] public async Task CallbackAsync([FromBody] CallbackRequest req, CancellationToken ct = default) { - var state = await keyCacheSvc.GetRegisterEmailStateAsync(req.State, ct); + var state = await keyCacheService.GetRegisterEmailStateAsync(req.State, ct); if (state == null) throw new ApiError.BadRequest("Invalid state", "state", req.State); if (state.ExistingUserId != null) { var authMethod = - await authSvc.AddAuthMethodAsync(state.ExistingUserId.Value, AuthType.Email, state.Email, ct: ct); + await authService.AddAuthMethodAsync(state.ExistingUserId.Value, AuthType.Email, state.Email, ct: ct); _logger.Debug("Added email auth {AuthId} for user {UserId}", authMethod.Id, state.ExistingUserId); return NoContent(); } var ticket = AuthUtils.RandomToken(); - await keyCacheSvc.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); + await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); return Ok(new AuthController.CallbackResponse(false, ticket, state.Email)); } @@ -58,7 +58,7 @@ public class EmailAuthController( [ProducesResponseType(StatusCodes.Status200OK)] public async Task LoginAsync([FromBody] LoginRequest req, CancellationToken ct = default) { - var (user, authenticationResult) = await authSvc.AuthenticateUserAsync(req.Email, req.Password, ct); + var (user, authenticationResult) = await authService.AuthenticateUserAsync(req.Email, req.Password, ct); if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired) throw new NotImplementedException("MFA is not implemented yet"); @@ -67,7 +67,7 @@ public class EmailAuthController( _logger.Debug("Logging user {Id} in with email and password", user.Id); var (tokenStr, token) = - authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); + authService.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); _logger.Debug("Generated token {TokenId} for {UserId}", token.Id, user.Id); @@ -75,7 +75,7 @@ public class EmailAuthController( await db.SaveChangesAsync(ct); return Ok(new AuthController.AuthResponse( - await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), + await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), tokenStr, token.ExpiresAt )); diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs index a8d3ab2..2d22c03 100644 --- a/Foxnouns.Backend/Controllers/DebugController.cs +++ b/Foxnouns.Backend/Controllers/DebugController.cs @@ -9,8 +9,8 @@ namespace Foxnouns.Backend.Controllers; [Route("/api/v2/debug")] public class DebugController( DatabaseContext db, - AuthService authSvc, - UserRendererService userRendererSvc, + AuthService authService, + UserRendererService userRenderer, IClock clock, ILogger logger) : ApiControllerBase { @@ -22,17 +22,17 @@ public class DebugController( { _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 user = await authService.CreateUserWithPasswordAsync(req.Username, req.Email, req.Password); var frontendApp = await db.GetFrontendApplicationAsync(); var (tokenStr, token) = - authSvc.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); + authService.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); await db.SaveChangesAsync(); return Ok(new AuthController.AuthResponse( - await userRendererSvc.RenderUserAsync(user, selfUser: user, renderMembers: false), + await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false), tokenStr, token.ExpiresAt )); diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 5f09e35..f051ca1 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -16,9 +16,9 @@ namespace Foxnouns.Backend.Controllers; public class MembersController( ILogger logger, DatabaseContext db, - MemberRendererService memberRendererService, + MemberRendererService memberRenderer, ISnowflakeGenerator snowflakeGenerator, - ObjectStorageService objectStorage, + ObjectStorageService objectStorageService, IQueue queue) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); @@ -28,7 +28,7 @@ public class MembersController( public async Task GetMembersAsync(string userRef, CancellationToken ct = default) { var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); - return Ok(await memberRendererService.RenderUserMembersAsync(user, CurrentToken)); + return Ok(await memberRenderer.RenderUserMembersAsync(user, CurrentToken)); } [HttpGet("{memberRef}")] @@ -36,7 +36,7 @@ public class MembersController( public async Task GetMemberAsync(string userRef, string memberRef, CancellationToken ct = default) { var member = await db.ResolveMemberAsync(userRef, memberRef, CurrentToken, ct); - return Ok(memberRendererService.RenderMember(member, CurrentToken)); + return Ok(memberRenderer.RenderMember(member, CurrentToken)); } [HttpPost("/api/v2/users/@me/members")] @@ -83,7 +83,7 @@ public class MembersController( queue.QueueInvocableWithPayload( new AvatarUpdatePayload(member.Id, req.Avatar)); - return Ok(memberRendererService.RenderMember(member, CurrentToken)); + return Ok(memberRenderer.RenderMember(member, CurrentToken)); } [HttpDelete("/api/v2/users/@me/members/{memberRef}")] @@ -101,7 +101,7 @@ public class MembersController( await db.SaveChangesAsync(ct); - if (member.Avatar != null) await objectStorage.DeleteMemberAvatarAsync(member.Id, member.Avatar); + if (member.Avatar != null) await objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar); return NoContent(); } diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 8b9299a..c7052ec 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -14,7 +14,7 @@ namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users")] public class UsersController( DatabaseContext db, - UserRendererService userRendererService, + UserRendererService userRenderer, ISnowflakeGenerator snowflakeGenerator, IQueue queue) : ApiControllerBase { @@ -23,7 +23,7 @@ public class UsersController( public async Task GetUserAsync(string userRef, CancellationToken ct = default) { var user = await db.ResolveUserAsync(userRef, CurrentToken, ct); - return Ok(await userRendererService.RenderUserAsync( + return Ok(await userRenderer.RenderUserAsync( user, selfUser: CurrentUser, token: CurrentToken, @@ -78,7 +78,7 @@ public class UsersController( await db.SaveChangesAsync(ct); await tx.CommitAsync(ct); - return Ok(await userRendererService.RenderUserAsync(user, CurrentUser, renderMembers: false, + return Ok(await userRenderer.RenderUserAsync(user, CurrentUser, renderMembers: false, renderAuthMethods: false, ct: ct)); } diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index 2d0fd16..cb70adf 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -14,12 +14,12 @@ public static class AvatarObjectExtensions private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"]; public static async Task - DeleteMemberAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash, CancellationToken ct = default) => - await objectStorage.RemoveObjectAsync(MemberAvatarUpdateInvocable.Path(id, hash), ct); + DeleteMemberAvatarAsync(this ObjectStorageService objectStorageService, Snowflake id, string hash, CancellationToken ct = default) => + await objectStorageService.RemoveObjectAsync(MemberAvatarUpdateInvocable.Path(id, hash), ct); public static async Task - DeleteUserAvatarAsync(this ObjectStorageService objectStorage, Snowflake id, string hash, CancellationToken ct = default) => - await objectStorage.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); + DeleteUserAvatarAsync(this ObjectStorageService objectStorageService, Snowflake id, string hash, CancellationToken ct = default) => + await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); public static async Task ConvertBase64UriToAvatar(this string uri) { diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index eb1cecc..e67d72e 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -7,31 +7,33 @@ namespace Foxnouns.Backend.Extensions; public static class KeyCacheExtensions { - public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheSvc, CancellationToken ct = default) + public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheService, + CancellationToken ct = default) { var state = AuthUtils.RandomToken(); - await keyCacheSvc.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct); + await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct); return state; } - public static async Task ValidateAuthStateAsync(this KeyCacheService keyCacheSvc, string state, CancellationToken ct = default) + public static async Task ValidateAuthStateAsync(this KeyCacheService keyCacheService, string state, + CancellationToken ct = default) { - var val = await keyCacheSvc.GetKeyAsync($"oauth_state:{state}", delete: true, ct); + var val = await keyCacheService.GetKeyAsync($"oauth_state:{state}", delete: true, ct); if (val == null) throw new ApiError.BadRequest("Invalid OAuth state"); } - public static async Task GenerateRegisterEmailStateAsync(this KeyCacheService keyCacheSvc, string email, + public static async Task GenerateRegisterEmailStateAsync(this KeyCacheService keyCacheService, string email, Snowflake? userId = null, CancellationToken ct = default) { var state = AuthUtils.RandomToken(); - await keyCacheSvc.SetKeyAsync($"email_state:{state}", new RegisterEmailState(email, userId), + await keyCacheService.SetKeyAsync($"email_state:{state}", new RegisterEmailState(email, userId), Duration.FromDays(1), ct); return state; } - public static async Task GetRegisterEmailStateAsync(this KeyCacheService keyCacheSvc, + public static async Task GetRegisterEmailStateAsync(this KeyCacheService keyCacheService, string state, CancellationToken ct = default) => - await keyCacheSvc.GetKeyAsync($"email_state:{state}", delete: true, ct); + await keyCacheService.GetKeyAsync($"email_state:{state}", delete: true, ct); } public record RegisterEmailState(string Email, Snowflake? ExistingUserId); \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs index 56d5077..3beff48 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -6,7 +6,7 @@ using Foxnouns.Backend.Services; namespace Foxnouns.Backend.Jobs; -public class MemberAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService objectStorage, ILogger logger) +public class MemberAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService objectStorageService, ILogger logger) : IInvocable, IInvocableWithPayload { private readonly ILogger _logger = logger.ForContext(); @@ -36,13 +36,13 @@ public class MemberAvatarUpdateInvocable(DatabaseContext db, ObjectStorageServic image.Seek(0, SeekOrigin.Begin); var prevHash = member.Avatar; - await objectStorage.PutObjectAsync(Path(id, hash), image, "image/webp"); + await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); member.Avatar = hash; await db.SaveChangesAsync(); if (prevHash != null && prevHash != hash) - await objectStorage.RemoveObjectAsync(Path(id, prevHash)); + await objectStorageService.RemoveObjectAsync(Path(id, prevHash)); _logger.Information("Updated avatar for member {MemberId}", id); } @@ -69,7 +69,7 @@ public class MemberAvatarUpdateInvocable(DatabaseContext db, ObjectStorageServic return; } - await objectStorage.RemoveObjectAsync(Path(member.Id, member.Avatar)); + await objectStorageService.RemoveObjectAsync(Path(member.Id, member.Avatar)); member.Avatar = null; await db.SaveChangesAsync(); diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index f0b04b2..d1abd42 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -6,7 +6,7 @@ using Foxnouns.Backend.Services; namespace Foxnouns.Backend.Jobs; -public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService objectStorage, ILogger logger) +public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService objectStorageService, ILogger logger) : IInvocable, IInvocableWithPayload { private readonly ILogger _logger = logger.ForContext(); @@ -36,13 +36,13 @@ public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService image.Seek(0, SeekOrigin.Begin); var prevHash = user.Avatar; - await objectStorage.PutObjectAsync(Path(id, hash), image, "image/webp"); + await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); user.Avatar = hash; await db.SaveChangesAsync(); if (prevHash != null && prevHash != hash) - await objectStorage.RemoveObjectAsync(Path(id, prevHash)); + await objectStorageService.RemoveObjectAsync(Path(id, prevHash)); _logger.Information("Updated avatar for user {UserId}", id); } @@ -69,7 +69,7 @@ public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService return; } - await objectStorage.RemoveObjectAsync(Path(user.Id, user.Avatar)); + await objectStorageService.RemoveObjectAsync(Path(user.Id, user.Avatar)); user.Avatar = null; await db.SaveChangesAsync(); diff --git a/Foxnouns.Backend/Services/MetricsCollectionService.cs b/Foxnouns.Backend/Services/MetricsCollectionService.cs index f860650..c5c924e 100644 --- a/Foxnouns.Backend/Services/MetricsCollectionService.cs +++ b/Foxnouns.Backend/Services/MetricsCollectionService.cs @@ -47,7 +47,7 @@ public class MetricsCollectionService( } } -public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectionService innerService) +public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectionService metricsCollectionService) : BackgroundService { private readonly ILogger _logger = logger.ForContext(); @@ -60,7 +60,7 @@ public class BackgroundMetricsCollectionService(ILogger logger, MetricsCollectio while (await timer.WaitForNextTickAsync(ct)) { _logger.Debug("Collecting metrics"); - await innerService.CollectMetricsAsync(ct); + await metricsCollectionService.CollectMetricsAsync(ct); } } } \ No newline at end of file diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs index 45e9678..e31a270 100644 --- a/Foxnouns.Backend/Services/ObjectStorageService.cs +++ b/Foxnouns.Backend/Services/ObjectStorageService.cs @@ -4,7 +4,7 @@ using Minio.Exceptions; namespace Foxnouns.Backend.Services; -public class ObjectStorageService(ILogger logger, Config config, IMinioClient minio) +public class ObjectStorageService(ILogger logger, Config config, IMinioClient minioClient) { private readonly ILogger _logger = logger.ForContext(); @@ -13,7 +13,8 @@ public class ObjectStorageService(ILogger logger, Config config, IMinioClient mi _logger.Debug("Deleting object at path {Path}", path); try { - await minio.RemoveObjectAsync(new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path), + await minioClient.RemoveObjectAsync( + new RemoveObjectArgs().WithBucket(config.Storage.Bucket).WithObject(path), ct); } catch (InvalidObjectNameException) @@ -27,7 +28,7 @@ public class ObjectStorageService(ILogger logger, Config config, IMinioClient mi _logger.Debug("Putting object at path {Path} with length {Length} and content type {ContentType}", path, data.Length, contentType); - await minio.PutObjectAsync(new PutObjectArgs() + await minioClient.PutObjectAsync(new PutObjectArgs() .WithBucket(config.Storage.Bucket) .WithObject(path) .WithObjectSize(data.Length) diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 4449488..07bdb8b 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -7,7 +7,7 @@ using NodaTime; namespace Foxnouns.Backend.Services; -public class UserRendererService(DatabaseContext db, MemberRendererService memberRendererService, Config config) +public class UserRendererService(DatabaseContext db, MemberRendererService memberRenderer, Config config) { public async Task RenderUserAsync(User user, User? selfUser = null, Token? token = null, @@ -39,7 +39,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe return new UserResponse( user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, - renderMembers ? members.Select(memberRendererService.RenderPartialMember) : null, + renderMembers ? members.Select(memberRenderer.RenderPartialMember) : null, renderAuthMethods ? authMethods.Select(a => new AuthenticationMethodResponse( a.Id, a.AuthType, a.RemoteId, diff --git a/STYLE.md b/STYLE.md index 6acdab8..28fc9ce 100644 --- a/STYLE.md +++ b/STYLE.md @@ -2,10 +2,22 @@ ## C# code style -Code should be formatted with `dotnet format` or Rider's built-in formatter. -Variables should *always* be declared using `var`, unless the correct type -can't be inferred from the declaration (i.e. if the variable needs to be an -`IEnumerable` instead of a `List`, or if a variable is initialized as `null`). +- Code should be formatted with `dotnet format` or Rider's built-in formatter. +- Variables should *always* be declared using `var`, + unless the correct type can't be inferred from the declaration (i.e. if the variable needs to be an `IEnumerable` + instead of a `List`, or if a variable is initialized as `null`). + +### 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 From 13a0cac663d979481fe02f3c3454263805accaa0 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Sep 2024 02:39:07 +0200 Subject: [PATCH 033/261] feat(backend): email registration --- .../Authentication/EmailAuthController.cs | 54 ++++++++++++++++++- .../Controllers/DebugController.cs | 42 --------------- .../Extensions/KeyCacheExtensions.cs | 9 +++- .../Extensions/WebApplicationExtensions.cs | 2 +- .../Mailables/AccountCreationMailable.cs | 3 +- Foxnouns.Backend/Mailables/BaseView.cs | 7 +++ Foxnouns.Backend/Services/AuthService.cs | 4 +- Foxnouns.Backend/Services/KeyCacheService.cs | 12 ++--- Foxnouns.Backend/Services/MailService.cs | 19 ++++--- .../Views/Mail/AccountCreation.cshtml | 12 +++++ Foxnouns.Backend/Views/Mail/Example.cshtml | 15 ------ Foxnouns.Backend/Views/Mail/Layout.cshtml | 17 ++++++ .../Views/Mail/_ViewImports.cshtml | 1 - Foxnouns.Backend/Views/Mail/_ViewStart.cshtml | 2 +- Foxnouns.Backend/config.example.ini | 3 +- 15 files changed, 120 insertions(+), 82 deletions(-) delete mode 100644 Foxnouns.Backend/Controllers/DebugController.cs create mode 100644 Foxnouns.Backend/Mailables/BaseView.cs create mode 100644 Foxnouns.Backend/Views/Mail/AccountCreation.cshtml delete mode 100644 Foxnouns.Backend/Views/Mail/Example.cshtml create mode 100644 Foxnouns.Backend/Views/Mail/Layout.cshtml diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 4e006af..fcf8b8e 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -12,6 +12,7 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/email")] public class EmailAuthController( DatabaseContext db, + Config config, AuthService authService, MailService mailService, KeyCacheService keyCacheService, @@ -24,9 +25,13 @@ public class EmailAuthController( [HttpPost("register")] public async Task RegisterAsync([FromBody] RegisterRequest req, CancellationToken ct = default) { + CheckRequirements(); + if (!req.Email.Contains('@')) throw new ApiError.BadRequest("Email is invalid", "email", req.Email); var state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, userId: null, ct); + + // 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(); @@ -37,9 +42,12 @@ public class EmailAuthController( [HttpPost("callback")] public async Task CallbackAsync([FromBody] CallbackRequest req, CancellationToken ct = default) { + CheckRequirements(); + var state = await keyCacheService.GetRegisterEmailStateAsync(req.State, ct); if (state == null) throw new ApiError.BadRequest("Invalid state", "state", req.State); - + + // If this callback is for an existing user, add the email address to their auth methods if (state.ExistingUserId != null) { var authMethod = @@ -49,15 +57,49 @@ public class EmailAuthController( } var ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); + await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20), ct); return Ok(new AuthController.CallbackResponse(false, ticket, state.Email)); } + [HttpPost("complete-registration")] + public async Task CompleteRegistrationAsync([FromBody] CompleteRegistrationRequest req, + CancellationToken ct = default) + { + var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}", ct: ct); + if (email == null) throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket); + + // Check if username is valid at all + ValidationUtils.Validate([("username", ValidationUtils.ValidateUsername(req.Username))]); + // Check if username is already taken + if (await db.Users.AnyAsync(u => u.Username == req.Username, ct)) + throw new ApiError.BadRequest("Username is already taken", "username", req.Username); + + var user = await authService.CreateUserWithPasswordAsync(req.Username, email, req.Password, ct); + var frontendApp = await db.GetFrontendApplicationAsync(ct); + + var (tokenStr, token) = + authService.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); + db.Add(token); + + await db.SaveChangesAsync(ct); + + // Specifically do *not* pass the CancellationToken so we don't cancel the rendering after creating the user account. + await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}", ct: default); + + return Ok(new AuthController.AuthResponse( + await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: default), + tokenStr, + token.ExpiresAt + )); + } + [HttpPost("login")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task LoginAsync([FromBody] LoginRequest req, CancellationToken ct = default) { + CheckRequirements(); + var (user, authenticationResult) = await authService.AuthenticateUserAsync(req.Email, req.Password, ct); if (authenticationResult == AuthService.EmailAuthenticationResult.MfaRequired) throw new NotImplementedException("MFA is not implemented yet"); @@ -81,9 +123,17 @@ public class EmailAuthController( )); } + private void CheckRequirements() + { + if (!config.DiscordAuth.Enabled) + throw new ApiError.BadRequest("Email authentication is not enabled on this instance."); + } + public record LoginRequest(string Email, string Password); public record RegisterRequest(string Email); + public record CompleteRegistrationRequest(string Ticket, string Username, string Password); + public record CallbackRequest(string State); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/DebugController.cs b/Foxnouns.Backend/Controllers/DebugController.cs deleted file mode 100644 index 2d22c03..0000000 --- a/Foxnouns.Backend/Controllers/DebugController.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Foxnouns.Backend.Controllers.Authentication; -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 authService, - UserRendererService userRenderer, - IClock clock, - ILogger logger) : ApiControllerBase -{ - private readonly ILogger _logger = logger.ForContext(); - - [HttpPost("users")] - [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CreateUserAsync([FromBody] CreateUserRequest req) - { - _logger.Debug("Creating user with username {Username} and email {Email}", req.Username, req.Email); - - var user = await authService.CreateUserWithPasswordAsync(req.Username, req.Email, req.Password); - var frontendApp = await db.GetFrontendApplicationAsync(); - - var (tokenStr, token) = - authService.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); - db.Add(token); - - await db.SaveChangesAsync(); - - return Ok(new AuthController.AuthResponse( - await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false), - tokenStr, - token.ExpiresAt - )); - } - - public record CreateUserRequest(string Username, string Password, string Email); -} \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index e67d72e..7de7396 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -1,6 +1,7 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; +using Newtonsoft.Json; using NodaTime; namespace Foxnouns.Backend.Extensions; @@ -25,7 +26,8 @@ public static class KeyCacheExtensions public static async Task GenerateRegisterEmailStateAsync(this KeyCacheService keyCacheService, string email, Snowflake? userId = null, CancellationToken ct = default) { - var state = AuthUtils.RandomToken(); + // This state is used in links, not just as JSON values, so make it URL-safe + var state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); await keyCacheService.SetKeyAsync($"email_state:{state}", new RegisterEmailState(email, userId), Duration.FromDays(1), ct); return state; @@ -36,4 +38,7 @@ public static class KeyCacheExtensions await keyCacheService.GetKeyAsync($"email_state:{state}", delete: true, ct); } -public record RegisterEmailState(string Email, Snowflake? ExistingUserId); \ No newline at end of file +public record RegisterEmailState( + string Email, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + Snowflake? ExistingUserId); \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 014eeb1..55ba99e 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -79,7 +79,7 @@ public static class WebApplicationExtensions { services .AddQueue() - .AddMailer(ctx.Configuration) + .AddSmtpMailer(ctx.Configuration) .AddDbContext() .AddMetricServer(o => o.Port = config.Logging.MetricsPort) .AddMinio(c => diff --git a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs index b55c9e6..3fc1ff4 100644 --- a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs +++ b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs @@ -12,8 +12,7 @@ public class AccountCreationMailable(Config config, AccountCreationMailableView } } -public class AccountCreationMailableView +public class AccountCreationMailableView : BaseView { - public required string To { get; init; } public required string Code { get; init; } } \ No newline at end of file diff --git a/Foxnouns.Backend/Mailables/BaseView.cs b/Foxnouns.Backend/Mailables/BaseView.cs new file mode 100644 index 0000000..e664f4e --- /dev/null +++ b/Foxnouns.Backend/Mailables/BaseView.cs @@ -0,0 +1,7 @@ +namespace Foxnouns.Backend.Mailables; + +public abstract class BaseView +{ + public required string BaseUrl { get; init; } + public required string To { get; init; } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index 8d6052d..decb240 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -16,7 +16,7 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// 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) + public async Task CreateUserWithPasswordAsync(string username, string email, string password, CancellationToken ct = default) { var user = new User { @@ -31,7 +31,7 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s }; db.Add(user); - user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password)); + user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); return user; } diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index d8c5434..33b595f 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -30,15 +30,14 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) var value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct); if (value == null) return null; - if (delete) - { - await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct); - await db.SaveChangesAsync(ct); - } + if (delete) await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct); return value.Value; } + public async Task DeleteKeyAsync(string key, CancellationToken ct = default) => + await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct); + public async Task DeleteExpiredKeysAsync(CancellationToken ct) { var count = await db.TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()).ExecuteDeleteAsync(ct); @@ -54,7 +53,8 @@ public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) await SetKeyAsync(key, value, expires, ct); } - public async Task GetKeyAsync(string key, bool delete = false, CancellationToken ct = default) where T : class + public async Task GetKeyAsync(string key, bool delete = false, CancellationToken ct = default) + where T : class { var value = await GetKeyAsync(key, delete, ct); return value == null ? default : JsonConvert.DeserializeObject(value); diff --git a/Foxnouns.Backend/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs index 271d41c..e030425 100644 --- a/Foxnouns.Backend/Services/MailService.cs +++ b/Foxnouns.Backend/Services/MailService.cs @@ -10,15 +10,22 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co public void QueueAccountCreationEmail(string to, string code) { - _logger.Debug("Sending account creation email to {ToEmail}", to); - queue.QueueAsyncTask(async () => { - await mailer.SendAsync(new AccountCreationMailable(config, new AccountCreationMailableView + _logger.Debug("Sending account creation email to {ToEmail}", to); + try { - To = to, - Code = code - })); + await mailer.SendAsync(new AccountCreationMailable(config, new AccountCreationMailableView + { + BaseUrl = config.BaseUrl, + To = to, + Code = code + })); + } + catch (Exception exc) + { + _logger.Error(exc, "Sending account creation email"); + } }); } } \ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml b/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml new file mode 100644 index 0000000..b2b0f2e --- /dev/null +++ b/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml @@ -0,0 +1,12 @@ +@model Foxnouns.Backend.Mailables.AccountCreationMailableView + +

+ Please continue creating a new pronouns.cc account by using the following link: +
+ Confirm your email address +
+ 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/Example.cshtml b/Foxnouns.Backend/Views/Mail/Example.cshtml deleted file mode 100644 index e1acaaa..0000000 --- a/Foxnouns.Backend/Views/Mail/Example.cshtml +++ /dev/null @@ -1,15 +0,0 @@ -@{ - ViewBag.Heading = "Welcome!"; - ViewBag.Preview = "Example Email"; -} - -

- Let's see what you can build! - To render a button inside your email, use the EmailLinkButton component: - @await Component.InvokeAsync("EmailLinkButton", new { text = "Click Me!", url = "https://www.google.com" }) -

- -@section links -{ - Coravel -} diff --git a/Foxnouns.Backend/Views/Mail/Layout.cshtml b/Foxnouns.Backend/Views/Mail/Layout.cshtml new file mode 100644 index 0000000..b92faa5 --- /dev/null +++ b/Foxnouns.Backend/Views/Mail/Layout.cshtml @@ -0,0 +1,17 @@ + + + + + + + + + + +@RenderBody() + + \ No newline at end of file diff --git a/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml b/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml index 8c050b2..6ececef 100644 --- a/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml +++ b/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml @@ -1,3 +1,2 @@ @using Foxnouns.Backend @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, Coravel.Mailer.ViewComponents diff --git a/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml b/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml index 1d54d45..b74bab7 100644 --- a/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml +++ b/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml @@ -1,3 +1,3 @@ @{ - Layout = "~/Areas/Coravel/Pages/Mail/Template.cshtml"; + Layout = "~/Views/Mail/Layout.cshtml"; } diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index edc8a28..941c25c 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -46,8 +46,7 @@ Bucket = pronounscc From = noreply@accounts.pronouns.cc ; The Coravel mail driver configuration. Keys should be self-explanatory. -[Coravel.Mail] -Driver = SMTP +[Coravel:Mail] Host = localhost Port = 1025 Username = smtp-username From 3d2238568964bac103c0c5bac61c67cc39ce256a Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Sep 2024 16:53:43 +0200 Subject: [PATCH 034/261] feat: add rate limiter proxy --- .gitignore | 1 + .../Controllers/InternalController.cs | 83 ++++++++ Foxnouns.Backend/Database/DatabaseContext.cs | 3 + go.mod | 9 + go.sum | 7 + rate/README.md | 7 + rate/handler.go | 192 ++++++++++++++++++ rate/main.go | 46 +++++ rate/proxy-config.example.json | 5 + rate/rate_limiter.go | 145 +++++++++++++ 10 files changed, 498 insertions(+) create mode 100644 Foxnouns.Backend/Controllers/InternalController.cs create mode 100644 go.mod create mode 100644 go.sum create mode 100644 rate/README.md create mode 100644 rate/handler.go create mode 100644 rate/main.go create mode 100644 rate/proxy-config.example.json create mode 100644 rate/rate_limiter.go diff --git a/.gitignore b/.gitignore index 8e84a9d..2d0d096 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ obj/ .version config.ini *.DotSettings.user +proxy-config.json diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs new file mode 100644 index 0000000..859c2fe --- /dev/null +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -0,0 +1,83 @@ +using System.Security.Cryptography; +using System.Text.RegularExpressions; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing.Template; +using Microsoft.EntityFrameworkCore; +using NodaTime; + +namespace Foxnouns.Backend.Controllers; + +[ApiController] +[Route("/api/internal")] +public partial class InternalController(DatabaseContext db, IClock clock) : ControllerBase +{ + [GeneratedRegex(@"(\{\w+\})")] + private static partial Regex PathVarRegex(); + + private static string GetCleanedTemplate(string template) + { + if (template.StartsWith("api/v2")) template = template.Substring("api/v2".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 (template.Contains("{id}")) return template.Split("{id}")[0] + "{id}"; + return template; + } + + [HttpPost("request-data")] + public async Task GetRequestDataAsync([FromBody] RequestDataRequest req) + { + var endpoint = GetEndpoint(HttpContext, req.Path, req.Method); + if (endpoint == null) throw new ApiError.BadRequest("Path/method combination is invalid"); + + var actionDescriptor = endpoint.Metadata.GetMetadata(); + var 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 (req.Token == null) return Ok(new RequestDataResponse(null, template)); + if (!AuthUtils.TryFromBase64String(req.Token, out var rawToken)) + return Ok(new RequestDataResponse(null, template)); + + 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); + + return Ok(new RequestDataResponse(oauthToken?.UserId, template)); + } + + public record RequestDataRequest(string? Token, string Method, string Path); + + public record RequestDataResponse( + Snowflake? UserId, + string Template); + + private static Endpoint? GetEndpoint(HttpContext httpContext, string url, string requestMethod) + { + var endpointDataSource = httpContext.RequestServices.GetService(); + if (endpointDataSource == null) return null; + var endpoints = endpointDataSource.Endpoints.OfType(); + + foreach (var endpoint in endpoints) + { + if (endpoint.RoutePattern.RawText == null) continue; + + var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), new()); + if (!templateMatcher.TryMatch(url, new())) continue; + var httpMethodAttribute = endpoint.Metadata.GetMetadata(); + if (httpMethodAttribute != null && + !httpMethodAttribute.HttpMethods.Any(x => x.Equals(requestMethod, StringComparison.OrdinalIgnoreCase))) + continue; + return endpoint; + } + + return null; + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 26087b8..8ce0d6f 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -28,6 +28,9 @@ public class DatabaseContext : DbContext { Timeout = config.Database.Timeout ?? 5, MaxPoolSize = config.Database.MaxPoolSize ?? 50, + MinPoolSize = 0, + ConnectionPruningInterval = 10, + ConnectionIdleLifetime = 10, }.ConnectionString; var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4636156 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +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/go.sum b/go.sum new file mode 100644 index 0000000..cfbff79 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +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/rate/README.md b/rate/README.md new file mode 100644 index 0000000..d65f0f1 --- /dev/null +++ b/rate/README.md @@ -0,0 +1,7 @@ +# 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/rate/handler.go b/rate/handler.go new file mode 100644 index 0000000..5cbfff3 --- /dev/null +++ b/rate/handler.go @@ -0,0 +1,192 @@ +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"` + + limiter *Limiter + proxy *httputil.ReverseProxy + client *http.Client +} + +func (hn *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // all public api endpoints are prefixed with this + if !strings.HasPrefix(r.URL.Path, "/api/v2") { + 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/rate/main.go b/rate/main.go new file mode 100644 index 0000000..33cccfd --- /dev/null +++ b/rate/main.go @@ -0,0 +1,46 @@ +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) + } + + 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/rate/proxy-config.example.json b/rate/proxy-config.example.json new file mode 100644 index 0000000..1d2d6af --- /dev/null +++ b/rate/proxy-config.example.json @@ -0,0 +1,5 @@ +{ + "port": 5003, + "proxy_target": "http://localhost:5000", + "debug": true +} diff --git a/rate/rate_limiter.go b/rate/rate_limiter.go new file mode 100644 index 0000000..28368e2 --- /dev/null +++ b/rate/rate_limiter.go @@ -0,0 +1,145 @@ +package main + +import ( + "crypto/sha256" + "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 { + hasher := sha256.New() + _, err := hasher.Write([]byte(method + "-" + template)) + if err != nil { + panic(err) + } + return hex.EncodeToString(hasher.Sum(nil)) +} + +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 1, time.Second + case "DELETE": + return 1, 5 * time.Second + case "GET": + return 3, time.Second + default: + return 2, time.Second + } +} From 8054d68f7949e11132cb4bd12a2638d7589c5ccd Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Sep 2024 18:49:25 +0200 Subject: [PATCH 035/261] feat(rate): add customizable X-Powered-By header --- rate/handler.go | 5 +++++ rate/proxy-config.example.json | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/rate/handler.go b/rate/handler.go index 5cbfff3..7ab0b59 100644 --- a/rate/handler.go +++ b/rate/handler.go @@ -25,6 +25,7 @@ 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 @@ -32,6 +33,10 @@ type Handler struct { } 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") { w.WriteHeader(http.StatusNotFound) diff --git a/rate/proxy-config.example.json b/rate/proxy-config.example.json index 1d2d6af..427acef 100644 --- a/rate/proxy-config.example.json +++ b/rate/proxy-config.example.json @@ -1,5 +1,6 @@ { "port": 5003, "proxy_target": "http://localhost:5000", - "debug": true + "debug": true, + "powered_by": "5 gay rats" } From 2323810b06ad57771c05341a199158d5ebaf9a10 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Sep 2024 18:52:13 +0200 Subject: [PATCH 036/261] feat(backend): add option to disable postgres connection pooling --- Foxnouns.Backend/Config.cs | 1 + Foxnouns.Backend/Database/DatabaseContext.cs | 1 + Foxnouns.Backend/config.example.ini | 2 ++ 3 files changed, 4 insertions(+) diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 96d724b..0781443 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -36,6 +36,7 @@ public class Config 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; } } diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 8ce0d6f..70477f2 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -26,6 +26,7 @@ public class DatabaseContext : DbContext { var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) { + Pooling = config.Database.EnablePooling ?? true, Timeout = config.Database.Timeout ?? 5, MaxPoolSize = config.Database.MaxPoolSize ?? 50, MinPoolSize = 0, diff --git a/Foxnouns.Backend/config.example.ini b/Foxnouns.Backend/config.example.ini index 941c25c..7522cba 100644 --- a/Foxnouns.Backend/config.example.ini +++ b/Foxnouns.Backend/config.example.ini @@ -30,6 +30,8 @@ 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. From 498d79de4eb4a84f228ab7de09ac740cb4e6e6e1 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Sep 2024 20:33:22 +0200 Subject: [PATCH 037/261] feat(frontend): internationalization --- .../.idea/codeStyles/Project.xml | 61 ++ .../Extensions/WebApplicationExtensions.cs | 1 + Foxnouns.Backend/Program.cs | 3 - .../app/components/nav/Navbar.tsx | 24 +- Foxnouns.Frontend/app/entry.client.tsx | 60 +- Foxnouns.Frontend/app/entry.server.tsx | 122 +-- Foxnouns.Frontend/app/env.server.ts | 1 + Foxnouns.Frontend/app/i18n.ts | 5 + Foxnouns.Frontend/app/i18next.server.ts | 28 + Foxnouns.Frontend/app/root.tsx | 15 +- Foxnouns.Frontend/app/routes/_index.tsx | 36 +- .../app/routes/auth.log-out/route.tsx | 11 + Foxnouns.Frontend/i18next-parser.config.js | 5 + Foxnouns.Frontend/package.json | 12 +- Foxnouns.Frontend/public/locales/en.json | 12 + Foxnouns.Frontend/yarn.lock | 863 +++++++++++++++++- 16 files changed, 1092 insertions(+), 167 deletions(-) create mode 100644 .idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml create mode 100644 Foxnouns.Frontend/app/i18n.ts create mode 100644 Foxnouns.Frontend/app/i18next.server.ts create mode 100644 Foxnouns.Frontend/app/routes/auth.log-out/route.tsx create mode 100644 Foxnouns.Frontend/i18next-parser.config.js create mode 100644 Foxnouns.Frontend/public/locales/en.json diff --git a/.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml b/.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..c80f77e --- /dev/null +++ b/.idea/.idea.Foxnouns.NET/.idea/codeStyles/Project.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 55ba99e..77a8a7e 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -121,6 +121,7 @@ public static class WebApplicationExtensions public static async Task Initialize(this WebApplication app, string[] args) { + // Read version information from .version in the repository root await BuildInfo.ReadBuildInfo(); app.Services.ConfigureQueue().LogQueuedTaskProgress(app.Services.GetRequiredService>()); diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 65e508a..316b45a 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -9,9 +9,6 @@ using Newtonsoft.Json.Serialization; using Prometheus; using Sentry.Extensibility; -// Read version information from .version in the repository root -await BuildInfo.ReadBuildInfo(); - var builder = WebApplication.CreateBuilder(args); var config = builder.AddConfiguration(); diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx index 51a6d1a..75f7d7d 100644 --- a/Foxnouns.Frontend/app/components/nav/Navbar.tsx +++ b/Foxnouns.Frontend/app/components/nav/Navbar.tsx @@ -7,6 +7,7 @@ import Nav from "react-bootstrap/Nav"; import Navbar from "react-bootstrap/Navbar"; import NavDropdown from "react-bootstrap/NavDropdown"; import { BrightnessHigh, BrightnessHighFill, MoonFill } from "react-bootstrap-icons"; +import { useTranslation } from "react-i18next"; export default function MainNavbar({ user, @@ -17,23 +18,26 @@ export default function MainNavbar({ settings: UserSettings; }) { const fetcher = useFetcher(); + const { t } = useTranslation(); const userMenu = user ? ( @{user.username}} align="end"> - View profile + {t("navbar.view-profile")} - Settings + {t("navbar.settings")} - - Log out - + + + {t("navbar.log-out")} + + ) : ( - Log in or sign up + {t("navbar.log-in")} ); @@ -59,19 +63,19 @@ export default function MainNavbar({ - Theme + {t("navbar.theme")} } align="end" > - Automatic + {t("navbar.theme-auto")} - Dark mode + {t("navbar.theme-dark")} - Light mode + {t("navbar.theme-light")} diff --git a/Foxnouns.Frontend/app/entry.client.tsx b/Foxnouns.Frontend/app/entry.client.tsx index 305d71b..d082e6c 100644 --- a/Foxnouns.Frontend/app/entry.client.tsx +++ b/Foxnouns.Frontend/app/entry.client.tsx @@ -1,18 +1,50 @@ -/** - * By default, Remix will handle hydrating your app on the client for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.client - */ - import { RemixBrowser } from "@remix-run/react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; +import i18n from "./i18n"; +import i18next from "i18next"; +import { I18nextProvider, initReactI18next } from "react-i18next"; +import LanguageDetector from "i18next-browser-languagedetector"; +import Backend from "i18next-http-backend"; +import { getInitialNamespaces } from "remix-i18next/client"; -startTransition(() => { - hydrateRoot( - document, - - - , - ); -}); +async function hydrate() { + await i18next + .use(initReactI18next) // Tell i18next to use the react-i18next plugin + .use(LanguageDetector) // Set up a client-side language detector + .use(Backend) // Setup your backend + .init({ + ...i18n, // spread the configuration + // This function detects the namespaces your routes rendered while SSR use + ns: getInitialNamespaces(), + backend: { loadPath: "/locales/{{lng}}.json" }, + detection: { + // Here only enable htmlTag detection, we'll detect the language only + // server-side with remix-i18next, by using the `` attribute + // we can communicate to the client the language detected server-side + order: ["htmlTag"], + // Because we only use htmlTag, there's no reason to cache the language + // on the browser, so we disable it + caches: [], + }, + }); + + startTransition(() => { + hydrateRoot( + document, + + + + + , + ); + }); +} + +if (window.requestIdleCallback) { + window.requestIdleCallback(hydrate); +} else { + // Safari doesn't support requestIdleCallback + // https://caniuse.com/requestidlecallback + window.setTimeout(hydrate, 1); +} diff --git a/Foxnouns.Frontend/app/entry.server.tsx b/Foxnouns.Frontend/app/entry.server.tsx index 7b1af99..33fd4af 100644 --- a/Foxnouns.Frontend/app/entry.server.tsx +++ b/Foxnouns.Frontend/app/entry.server.tsx @@ -1,56 +1,56 @@ -/** - * By default, Remix will handle generating the HTTP Response for you. - * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ - * For more information, see https://remix.run/file-conventions/entry.server - */ - -import { PassThrough } from "node:stream"; - -import type { AppLoadContext, EntryContext } from "@remix-run/node"; -import { createReadableStreamFromReadable } from "@remix-run/node"; +import { PassThrough } from "stream"; +import { createReadableStreamFromReadable, type EntryContext } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; import { isbot } from "isbot"; import { renderToPipeableStream } from "react-dom/server"; +import { createInstance } from "i18next"; +import i18next from "./i18next.server"; +import { I18nextProvider, initReactI18next } from "react-i18next"; +import Backend from "i18next-fs-backend"; +import i18n from "./i18n"; // your i18n configuration file +import { resolve } from "node:path"; -const ABORT_DELAY = 5_000; +const ABORT_DELAY = 5000; -export default function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, - // This is ignored so we can keep it in the template for visibility. Feel - // free to delete this parameter in your app if you're not using it! - // eslint-disable-next-line @typescript-eslint/no-unused-vars - loadContext: AppLoadContext, -) { - return isbot(request.headers.get("user-agent") || "") - ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) - : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); -} - -function handleBotRequest( +export default async function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, ) { + const callbackName = isbot(request.headers.get("user-agent")) ? "onAllReady" : "onShellReady"; + + const instance = createInstance(); + const lng = await i18next.getLocale(request); + const ns = i18next.getRouteNamespaces(remixContext); + + await instance + .use(initReactI18next) // Tell our instance to use react-i18next + .use(Backend) // Set up our backend + .init({ + ...i18n, // spread the configuration + lng, // The locale we detected above + ns, // The namespaces the routes about to render wants to use + backend: { loadPath: resolve("./public/locales/{{lng}}.json") }, + }); + return new Promise((resolve, reject) => { - let shellRendered = false; + let didError = false; + const { pipe, abort } = renderToPipeableStream( - , + + + , { - onAllReady() { - shellRendered = true; + [callbackName]: () => { const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); - responseHeaders.set("Content-Type", "text/html"); resolve( new Response(stream, { headers: responseHeaders, - status: responseStatusCode, + status: didError ? 500 : responseStatusCode, }), ); @@ -60,59 +60,9 @@ function handleBotRequest( reject(error); }, onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } - }, - }, - ); - - setTimeout(abort, ABORT_DELAY); - }); -} - -function handleBrowserRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, -) { - return new Promise((resolve, reject) => { - let shellRendered = false; - const { pipe, abort } = renderToPipeableStream( - , - { - onShellReady() { - shellRendered = true; - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: responseStatusCode, - }), - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - responseStatusCode = 500; - // Log streaming rendering errors from inside the shell. Don't log - // errors encountered during initial shell rendering since they'll - // reject and get logged in handleDocumentRequest. - if (shellRendered) { - console.error(error); - } + didError = true; + + console.error(error); }, }, ); diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts index 4a81ebe..882d393 100644 --- a/Foxnouns.Frontend/app/env.server.ts +++ b/Foxnouns.Frontend/app/env.server.ts @@ -1,3 +1,4 @@ import { env } from "node:process"; export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; +export const LANGUAGE = env.LANGUAGE || "en"; diff --git a/Foxnouns.Frontend/app/i18n.ts b/Foxnouns.Frontend/app/i18n.ts new file mode 100644 index 0000000..8ef310e --- /dev/null +++ b/Foxnouns.Frontend/app/i18n.ts @@ -0,0 +1,5 @@ +export default { + supportedLngs: ["en", "en-XX"], + fallbackLng: "en", + defaultNS: "common", +}; diff --git a/Foxnouns.Frontend/app/i18next.server.ts b/Foxnouns.Frontend/app/i18next.server.ts new file mode 100644 index 0000000..28fbbf0 --- /dev/null +++ b/Foxnouns.Frontend/app/i18next.server.ts @@ -0,0 +1,28 @@ +import Backend from "i18next-fs-backend"; +import { resolve } from "node:path"; +import { RemixI18Next } from "remix-i18next/server"; +import i18n from "~/i18n"; +import { LANGUAGE } from "~/env.server"; + +const i18next = new RemixI18Next({ + detection: { + supportedLanguages: [LANGUAGE], + fallbackLanguage: LANGUAGE, + }, + // This is the configuration for i18next used + // when translating messages server-side only + i18next: { + ...i18n, + fallbackLng: LANGUAGE, + lng: LANGUAGE, + backend: { + loadPath: resolve("./public/locales/{{lng}}.json"), + }, + }, + // The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions. + // E.g. The Backend plugin for loading translations from the file system + // Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here + plugins: [Backend], +}); + +export default i18next; diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index de5e053..b8b9d09 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -9,6 +9,8 @@ import { } from "@remix-run/react"; import { LoaderFunction } from "@remix-run/node"; import SSRProvider from "react-bootstrap/SSRProvider"; +import { useChangeLanguage } from "remix-i18next/react"; +import { useTranslation } from "react-i18next"; import serverRequest, { getCookie, writeCookie } from "./lib/request.server"; import Meta from "./lib/api/meta"; @@ -18,6 +20,7 @@ import { ApiError, ErrorCode } from "./lib/api/error"; import "./app.scss"; import getLocalSettings from "./lib/settings.server"; +import { LANGUAGE } from "~/env.server"; export const loader: LoaderFunction = async ({ request }) => { const meta = await serverRequest("GET", "/meta"); @@ -29,8 +32,7 @@ export const loader: LoaderFunction = async ({ request }) => { let settings = getLocalSettings(request); if (token) { try { - const user = await serverRequest("GET", "/users/@me", { token }); - meUser = user; + meUser = await serverRequest("GET", "/users/@me", { token }); settings = await serverRequest("GET", "/users/@me/settings", { token }); } catch (e) { @@ -42,7 +44,7 @@ export const loader: LoaderFunction = async ({ request }) => { } return json( - { meta, meUser, settings }, + { meta, meUser, settings, locale: LANGUAGE }, { headers: { "Set-Cookie": setCookie }, }, @@ -50,10 +52,13 @@ export const loader: LoaderFunction = async ({ request }) => { }; export function Layout({ children }: { children: React.ReactNode }) { - const { settings } = useLoaderData(); + const { settings, locale } = useLoaderData(); + const { i18n } = useTranslation(); + i18n.language = locale; + useChangeLanguage(locale); return ( - + diff --git a/Foxnouns.Frontend/app/routes/_index.tsx b/Foxnouns.Frontend/app/routes/_index.tsx index 5f6fc14..2a906d2 100644 --- a/Foxnouns.Frontend/app/routes/_index.tsx +++ b/Foxnouns.Frontend/app/routes/_index.tsx @@ -6,40 +6,8 @@ export const meta: MetaFunction = () => { export default function Index() { return ( -
-

Welcome to Remix

- +
+

pronouns.cc

); } diff --git a/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx new file mode 100644 index 0000000..1b146e2 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx @@ -0,0 +1,11 @@ +import { ActionFunction } from "@remix-run/node"; +import { writeCookie } from "~/lib/request.server"; + +export const action: ActionFunction = async () => { + return new Response(null, { + headers: { + "Set-Cookie": writeCookie("pronounscc-token", "token", 0), + }, + status: 204, + }); +}; diff --git a/Foxnouns.Frontend/i18next-parser.config.js b/Foxnouns.Frontend/i18next-parser.config.js new file mode 100644 index 0000000..41d6da6 --- /dev/null +++ b/Foxnouns.Frontend/i18next-parser.config.js @@ -0,0 +1,5 @@ +import i18n from "./app/i18n.js"; + +export default { + locales: i18n.supportedLngs, +}; diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 673ec92..0b1aff2 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -9,7 +9,8 @@ "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "start": "cross-env NODE_ENV=production node ./server.js", "typecheck": "tsc", - "format": "prettier -w ." + "format": "prettier -w .", + "extract-translations": "i18next 'app/**/*.tsx' -o 'public/locales/$LOCALE.json'" }, "dependencies": { "@remix-run/express": "^2.11.2", @@ -22,12 +23,18 @@ "cookie": "^0.6.0", "cross-env": "^7.0.3", "express": "^4.19.2", + "i18next": "^23.15.1", + "i18next-browser-languagedetector": "^8.0.0", + "i18next-fs-backend": "^2.3.2", + "i18next-http-backend": "^2.6.1", "isbot": "^4.1.0", "morgan": "^1.10.0", "react": "^18.2.0", "react-bootstrap": "^2.10.4", "react-bootstrap-icons": "^1.11.4", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-i18next": "^15.0.1", + "remix-i18next": "^6.3.0" }, "devDependencies": { "@fontsource/firago": "^5.0.11", @@ -46,6 +53,7 @@ "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", + "i18next-parser": "^9.0.2", "prettier": "^3.3.3", "sass": "^1.78.0", "typescript": "^5.1.6", diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json new file mode 100644 index 0000000..f576541 --- /dev/null +++ b/Foxnouns.Frontend/public/locales/en.json @@ -0,0 +1,12 @@ +{ + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up", + "theme": "Theme", + "theme-auto": "Automatic", + "theme-dark": "Dark", + "theme-light": "Light" + } +} diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index f786a58..f4b109e 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -240,7 +240,7 @@ "@babel/plugin-transform-modules-commonjs" "^7.24.7" "@babel/plugin-transform-typescript" "^7.24.7" -"@babel/runtime@^7.12.5", "@babel/runtime@^7.21.0", "@babel/runtime@^7.24.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": +"@babel/runtime@^7.12.5", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.7": version "7.25.6" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== @@ -293,6 +293,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== +"@esbuild/aix-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz#51299374de171dbd80bb7d838e1cfce9af36f353" + integrity sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ== + "@esbuild/android-arm64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz#b11bd4e4d031bb320c93c83c137797b2be5b403b" @@ -308,6 +313,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== +"@esbuild/android-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz#58565291a1fe548638adb9c584237449e5e14018" + integrity sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw== + "@esbuild/android-arm@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.6.tgz#ac6b5674da2149997f6306b3314dae59bbe0ac26" @@ -323,6 +333,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== +"@esbuild/android-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.23.1.tgz#5eb8c652d4c82a2421e3395b808e6d9c42c862ee" + integrity sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ== + "@esbuild/android-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.6.tgz#18c48bf949046638fc209409ff684c6bb35a5462" @@ -338,6 +353,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== +"@esbuild/android-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.23.1.tgz#ae19d665d2f06f0f48a6ac9a224b3f672e65d517" + integrity sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg== + "@esbuild/darwin-arm64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz#b3fe19af1e4afc849a07c06318124e9c041e0646" @@ -353,6 +373,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== +"@esbuild/darwin-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz#05b17f91a87e557b468a9c75e9d85ab10c121b16" + integrity sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q== + "@esbuild/darwin-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz#f4dacd1ab21e17b355635c2bba6a31eba26ba569" @@ -368,6 +393,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== +"@esbuild/darwin-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz#c58353b982f4e04f0d022284b8ba2733f5ff0931" + integrity sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw== + "@esbuild/freebsd-arm64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz#ea4531aeda70b17cbe0e77b0c5c36298053855b4" @@ -383,6 +413,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== +"@esbuild/freebsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz#f9220dc65f80f03635e1ef96cfad5da1f446f3bc" + integrity sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA== + "@esbuild/freebsd-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz#1896170b3c9f63c5e08efdc1f8abc8b1ed7af29f" @@ -398,6 +433,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== +"@esbuild/freebsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz#69bd8511fa013b59f0226d1609ac43f7ce489730" + integrity sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g== + "@esbuild/linux-arm64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz#967dfb951c6b2de6f2af82e96e25d63747f75079" @@ -413,6 +453,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== +"@esbuild/linux-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz#8050af6d51ddb388c75653ef9871f5ccd8f12383" + integrity sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g== + "@esbuild/linux-arm@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz#097a0ee2be39fed3f37ea0e587052961e3bcc110" @@ -428,6 +473,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== +"@esbuild/linux-arm@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz#ecaabd1c23b701070484990db9a82f382f99e771" + integrity sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ== + "@esbuild/linux-ia32@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz#a38a789d0ed157495a6b5b4469ec7868b59e5278" @@ -443,6 +493,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== +"@esbuild/linux-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz#3ed2273214178109741c09bd0687098a0243b333" + integrity sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ== + "@esbuild/linux-loong64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz#ae3983d0fb4057883c8246f57d2518c2af7cf2ad" @@ -458,6 +513,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== +"@esbuild/linux-loong64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz#a0fdf440b5485c81b0fbb316b08933d217f5d3ac" + integrity sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw== + "@esbuild/linux-mips64el@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz#15fbbe04648d944ec660ee5797febdf09a9bd6af" @@ -473,6 +533,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== +"@esbuild/linux-mips64el@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz#e11a2806346db8375b18f5e104c5a9d4e81807f6" + integrity sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q== + "@esbuild/linux-ppc64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz#38210094e8e1a971f2d1fd8e48462cc65f15ef19" @@ -488,6 +553,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== +"@esbuild/linux-ppc64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz#06a2744c5eaf562b1a90937855b4d6cf7c75ec96" + integrity sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw== + "@esbuild/linux-riscv64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz#bc3c66d5578c3b9951a6ed68763f2a6856827e4a" @@ -503,6 +573,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== +"@esbuild/linux-riscv64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz#65b46a2892fc0d1af4ba342af3fe0fa4a8fe08e7" + integrity sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA== + "@esbuild/linux-s390x@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz#d7ba7af59285f63cfce6e5b7f82a946f3e6d67fc" @@ -518,6 +593,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== +"@esbuild/linux-s390x@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz#e71ea18c70c3f604e241d16e4e5ab193a9785d6f" + integrity sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw== + "@esbuild/linux-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz#ba51f8760a9b9370a2530f98964be5f09d90fed0" @@ -533,6 +613,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== +"@esbuild/linux-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz#d47f97391e80690d4dfe811a2e7d6927ad9eed24" + integrity sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ== + "@esbuild/netbsd-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz#e84d6b6fdde0261602c1e56edbb9e2cb07c211b9" @@ -548,6 +633,16 @@ resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== +"@esbuild/netbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz#44e743c9778d57a8ace4b72f3c6b839a3b74a653" + integrity sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA== + +"@esbuild/openbsd-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz#05c5a1faf67b9881834758c69f3e51b7dee015d7" + integrity sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q== + "@esbuild/openbsd-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz#cf4b9fb80ce6d280a673d54a731d9c661f88b083" @@ -563,6 +658,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== +"@esbuild/openbsd-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz#2e58ae511bacf67d19f9f2dcd9e8c5a93f00c273" + integrity sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA== + "@esbuild/sunos-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz#a6838e246079b24d962b9dcb8d208a3785210a73" @@ -578,6 +678,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== +"@esbuild/sunos-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz#adb022b959d18d3389ac70769cef5a03d3abd403" + integrity sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA== + "@esbuild/win32-arm64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz#ace0186e904d109ea4123317a3ba35befe83ac21" @@ -593,6 +698,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== +"@esbuild/win32-arm64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz#84906f50c212b72ec360f48461d43202f4c8b9a2" + integrity sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A== + "@esbuild/win32-ia32@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz#7fb3f6d4143e283a7f7dffc98a6baf31bb365c7e" @@ -608,6 +718,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== +"@esbuild/win32-ia32@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz#5e3eacc515820ff729e90d0cb463183128e82fac" + integrity sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ== + "@esbuild/win32-x64@0.17.6": version "0.17.6" resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz#563ff4277f1230a006472664fa9278a83dd124da" @@ -623,6 +738,11 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== +"@esbuild/win32-x64@0.23.1": + version "0.23.1" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz#81fd50d11e2c32b2d6241470e3185b70c7b30699" + integrity sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg== + "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" @@ -660,6 +780,13 @@ resolved "https://registry.yarnpkg.com/@fontsource/firago/-/firago-5.0.11.tgz#8fe3c8b47cc1d8148bc50c80189ed3aac8555cb7" integrity sha512-XfFsLxSFMTbJTN+94yFTJyuFGmoxtykt+6rL0fj9unCeXslllirpH6KetIlbZO73NzTUmKYRvtOJdOgVbBGtaQ== +"@gulpjs/to-absolute-glob@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@gulpjs/to-absolute-glob/-/to-absolute-glob-4.0.0.tgz#1fc2460d3953e1d9b9f2dfdb4bcc99da4710c021" + integrity sha512-kjotm7XJrJ6v+7knhPaRgaT6q8F8K2jiafwYdNHLzmV0uGLuZY43FK6smNSHUPrhq5kX2slCUy+RGG/xGqmIKA== + dependencies: + is-negated-glob "^1.0.0" + "@humanwhocodes/config-array@^0.11.14": version "0.11.14" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" @@ -1226,6 +1353,11 @@ resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + "@types/morgan@^1.9.9": version "1.9.9" resolved "https://registry.yarnpkg.com/@types/morgan/-/morgan-1.9.9.tgz#d60dec3979e16c203a000159daa07d3fb7270d7f" @@ -1304,6 +1436,11 @@ "@types/node" "*" "@types/send" "*" +"@types/symlink-or-copy@^1.2.0": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz#51b1c00b516a5774ada5d611e65eb123f988ef8d" + integrity sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA== + "@types/unist@^2", "@types/unist@^2.0.0": version "2.0.11" resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" @@ -1536,7 +1673,7 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@~3.1.2: +anymatch@^3.1.3, anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -1687,6 +1824,11 @@ axobject-query@^4.1.0: resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== +b4a@^1.6.4: + version "1.6.6" + resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.6.tgz#a4cc349a3851987c3c4ac2d7785c18744f6da9ba" + integrity sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg== + bail@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" @@ -1697,6 +1839,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== +bare-events@^2.2.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.4.2.tgz#3140cca7a0e11d49b3edc5041ab560659fd8e1f8" + integrity sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q== + base64-js@^1.3.1: version "1.5.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" @@ -1723,6 +1870,15 @@ bl@^4.0.3, bl@^4.1.0: inherits "^2.0.4" readable-stream "^3.4.0" +bl@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" + integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== + dependencies: + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^3.4.0" + body-parser@1.20.2: version "1.20.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" @@ -1741,6 +1897,11 @@ body-parser@1.20.2: type-is "~1.6.18" unpipe "1.0.0" +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + bootstrap@^5.3.3: version "5.3.3" resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.3.tgz#de35e1a765c897ac940021900fcbb831602bac38" @@ -1768,6 +1929,38 @@ braces@^3.0.3, braces@~3.0.2: dependencies: fill-range "^7.1.1" +broccoli-node-api@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz#391aa6edecd2a42c63c111b4162956b2fa288cb6" + integrity sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw== + +broccoli-node-info@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz#feb01c13020792f429e01d7f7845dc5b3a7932b3" + integrity sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg== + +broccoli-output-wrapper@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz#514b17801c92922a2c2f87fd145df2a25a11bc5f" + integrity sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw== + dependencies: + fs-extra "^8.1.0" + heimdalljs-logger "^0.1.10" + symlink-or-copy "^1.2.0" + +broccoli-plugin@^4.0.7: + version "4.0.7" + resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz#dd176a85efe915ed557d913744b181abe05047db" + integrity sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg== + dependencies: + broccoli-node-api "^1.7.0" + broccoli-output-wrapper "^3.2.5" + fs-merger "^3.2.1" + promise-map-series "^0.3.0" + quick-temp "^0.1.8" + rimraf "^3.0.2" + symlink-or-copy "^1.3.1" + browserify-zlib@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" @@ -1798,6 +1991,14 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + bytes@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" @@ -1894,6 +2095,35 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== +cheerio-select@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" + integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== + dependencies: + boolbase "^1.0.0" + css-select "^5.1.0" + css-what "^6.1.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.0.1" + +cheerio@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81" + integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww== + dependencies: + cheerio-select "^2.1.0" + dom-serializer "^2.0.0" + domhandler "^5.0.3" + domutils "^3.1.0" + encoding-sniffer "^0.2.0" + htmlparser2 "^9.1.0" + parse5 "^7.1.2" + parse5-htmlparser2-tree-adapter "^7.0.0" + parse5-parser-stream "^7.1.2" + undici "^6.19.5" + whatwg-mimetype "^4.0.0" + "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1, chokidar@^3.5.3: version "3.6.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" @@ -1941,11 +2171,21 @@ cli-spinners@^2.5.0: resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== +clone-stats@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" + integrity sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag== + clone@^1.0.2: version "1.0.4" resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== +clone@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" + integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== + color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1970,11 +2210,21 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== +commander@~12.1.0: + version "12.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + compressible@~2.0.16: version "2.0.18" resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" @@ -2049,6 +2299,13 @@ cross-env@^7.0.3: dependencies: cross-spawn "^7.0.1" +cross-fetch@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -2058,6 +2315,17 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + css-what@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" @@ -2110,7 +2378,7 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" -debug@2.6.9: +debug@2.6.9, debug@^2.2.0: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -2256,6 +2524,36 @@ dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^3.0.1, domutils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + dotenv@^16.0.0: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" @@ -2301,6 +2599,14 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== +encoding-sniffer@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5" + integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg== + dependencies: + iconv-lite "^0.6.3" + whatwg-encoding "^3.1.1" + end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -2316,6 +2622,21 @@ enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" +ensure-posix-path@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz#3c62bdb19fa4681544289edb2b382adc029179ce" + integrity sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw== + +entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +eol@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd" + integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg== + err-code@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" @@ -2523,6 +2844,36 @@ esbuild@^0.21.3: "@esbuild/win32-ia32" "0.21.5" "@esbuild/win32-x64" "0.21.5" +esbuild@^0.23.0: + version "0.23.1" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.1.tgz#40fdc3f9265ec0beae6f59824ade1bd3d3d2dab8" + integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== + optionalDependencies: + "@esbuild/aix-ppc64" "0.23.1" + "@esbuild/android-arm" "0.23.1" + "@esbuild/android-arm64" "0.23.1" + "@esbuild/android-x64" "0.23.1" + "@esbuild/darwin-arm64" "0.23.1" + "@esbuild/darwin-x64" "0.23.1" + "@esbuild/freebsd-arm64" "0.23.1" + "@esbuild/freebsd-x64" "0.23.1" + "@esbuild/linux-arm" "0.23.1" + "@esbuild/linux-arm64" "0.23.1" + "@esbuild/linux-ia32" "0.23.1" + "@esbuild/linux-loong64" "0.23.1" + "@esbuild/linux-mips64el" "0.23.1" + "@esbuild/linux-ppc64" "0.23.1" + "@esbuild/linux-riscv64" "0.23.1" + "@esbuild/linux-s390x" "0.23.1" + "@esbuild/linux-x64" "0.23.1" + "@esbuild/netbsd-x64" "0.23.1" + "@esbuild/openbsd-arm64" "0.23.1" + "@esbuild/openbsd-x64" "0.23.1" + "@esbuild/sunos-x64" "0.23.1" + "@esbuild/win32-arm64" "0.23.1" + "@esbuild/win32-ia32" "0.23.1" + "@esbuild/win32-x64" "0.23.1" + "esbuild@npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0": version "0.19.12" resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" @@ -2909,6 +3260,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-fifo@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" + integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== + fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" @@ -2930,7 +3286,7 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fastq@^1.6.0: +fastq@^1.13.0, fastq@^1.6.0: version "1.17.1" resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== @@ -3037,6 +3393,35 @@ fs-extra@^10.0.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.0: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^8.0.1, fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" + +fs-merger@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/fs-merger/-/fs-merger-3.2.1.tgz#a225b11ae530426138294b8fbb19e82e3d4e0b3b" + integrity sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug== + dependencies: + broccoli-node-api "^1.7.0" + broccoli-node-info "^2.1.0" + fs-extra "^8.0.1" + fs-tree-diff "^2.0.1" + walk-sync "^2.2.0" + fs-minipass@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" @@ -3051,6 +3436,25 @@ fs-minipass@^3.0.0: dependencies: minipass "^7.0.3" +fs-mkdirp-stream@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz#1e82575c4023929ad35cf69269f84f1a8c973aa7" + integrity sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw== + dependencies: + graceful-fs "^4.2.8" + streamx "^2.12.0" + +fs-tree-diff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz#343e4745ab435ec39ebac5f9059ad919cd034afa" + integrity sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A== + dependencies: + "@types/symlink-or-copy" "^1.2.0" + heimdalljs-logger "^0.1.7" + object-assign "^4.1.0" + path-posix "^1.0.0" + symlink-or-copy "^1.1.8" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -3144,6 +3548,20 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" +glob-stream@^8.0.0: + version "8.0.2" + resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-8.0.2.tgz#09e5818e41c16dd85274d72c7a7158d307426313" + integrity sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw== + dependencies: + "@gulpjs/to-absolute-glob" "^4.0.0" + anymatch "^3.1.3" + fastq "^1.13.0" + glob-parent "^6.0.2" + is-glob "^4.0.3" + is-negated-glob "^1.0.0" + normalize-path "^3.0.0" + streamx "^2.12.5" + glob@^10.2.2: version "10.4.5" resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" @@ -3212,7 +3630,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.8: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -3222,6 +3640,13 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +gulp-sort@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/gulp-sort/-/gulp-sort-2.0.0.tgz#c6762a2f1f0de0a3fc595a21599d3fac8dba1aca" + integrity sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g== + dependencies: + through2 "^2.0.1" + gunzip-maybe@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac" @@ -3306,6 +3731,21 @@ hast-util-whitespace@^2.0.0: resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== +heimdalljs-logger@^0.1.10, heimdalljs-logger@^0.1.7: + version "0.1.10" + resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz#90cad58aabb1590a3c7e640ddc6a4cd3a43faaf7" + integrity sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g== + dependencies: + debug "^2.2.0" + heimdalljs "^0.2.6" + +heimdalljs@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.6.tgz#b0eebabc412813aeb9542f9cc622cb58dbdcd9fe" + integrity sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA== + dependencies: + rsvp "~3.2.1" + hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" @@ -3313,6 +3753,23 @@ hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: dependencies: lru-cache "^7.5.1" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + +htmlparser2@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23" + integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.3" + domutils "^3.1.0" + entities "^4.5.0" + http-errors@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" @@ -3329,6 +3786,55 @@ human-signals@^2.1.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +i18next-browser-languagedetector@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz#b6fdd9b43af67c47f2c26c9ba27710a1eaf31e2f" + integrity sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw== + dependencies: + "@babel/runtime" "^7.23.2" + +i18next-fs-backend@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-2.3.2.tgz#580b91c9a306b452112e0a1ad3b07e9fd266e567" + integrity sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q== + +i18next-http-backend@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz#186c3a1359e10245c9119a13129f9b5bf328c9a7" + integrity sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog== + dependencies: + cross-fetch "4.0.0" + +i18next-parser@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-9.0.2.tgz#f9d627422d33c352967556c8724975d58f1f5a95" + integrity sha512-Q1yTZljBp1DcVAQD7LxduEqFRpjIeZc+5VnQ+gU8qG9WvY3U5rqK0IVONRWNtngh3orb197bfy1Sz4wlwcplxg== + dependencies: + "@babel/runtime" "^7.23.2" + broccoli-plugin "^4.0.7" + cheerio "^1.0.0" + colors "1.4.0" + commander "~12.1.0" + eol "^0.9.1" + esbuild "^0.23.0" + fs-extra "^11.1.0" + gulp-sort "^2.0.0" + i18next "^23.5.1" + js-yaml "4.1.0" + lilconfig "^3.0.0" + rsvp "^4.8.2" + sort-keys "^5.0.0" + typescript "^5.0.4" + vinyl "~3.0.0" + vinyl-fs "^4.0.0" + +i18next@^23.15.1, i18next@^23.5.1: + version "23.15.1" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.15.1.tgz#c50de337bf12ca5195e697cc0fbe5f32304871d9" + integrity sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -3336,12 +3842,19 @@ iconv-lite@0.4.24: dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.6.3, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-utils@^5.0.0, icss-utils@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== -ieee754@^1.1.13: +ieee754@^1.1.13, ieee754@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== @@ -3570,6 +4083,11 @@ is-map@^2.0.2, is-map@^2.0.3: resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== +is-negated-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" + integrity sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug== + is-negative-zero@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" @@ -3660,6 +4178,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +is-valid-glob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" + integrity sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA== + is-weakmap@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" @@ -3730,7 +4253,7 @@ javascript-stringify@^2.0.1: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^4.0.0, js-yaml@^4.1.0: +js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -3779,6 +4302,13 @@ json5@^2.2.2, json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonfile@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== + optionalDependencies: + graceful-fs "^4.1.6" + jsonfile@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" @@ -3822,6 +4352,11 @@ language-tags@^1.0.9: dependencies: language-subtag-registry "^0.3.20" +lead@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/lead/-/lead-4.0.0.tgz#5317a49effb0e7ec3a0c8fb9c1b24fb716aab939" + integrity sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg== + levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -3917,6 +4452,14 @@ markdown-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3" integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q== +matcher-collection@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-2.0.1.tgz#90be1a4cf58d6f2949864f65bb3b0f3e41303b29" + integrity sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ== + dependencies: + "@types/minimatch" "^3.0.3" + minimatch "^3.0.2" + mdast-util-definitions@^5.0.0: version "5.1.2" resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" @@ -4419,7 +4962,7 @@ minimatch@9.0.3: dependencies: brace-expansion "^2.0.1" -minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -4494,6 +5037,11 @@ mkdirp@^1.0.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mktemp@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" + integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A== + mlly@^1.4.2, mlly@^1.7.1: version "1.7.1" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" @@ -4560,6 +5108,13 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-releases@^2.0.18: version "2.0.18" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" @@ -4575,11 +5130,18 @@ normalize-package-data@^5.0.0: semver "^7.3.5" validate-npm-package-license "^3.0.4" -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@3.0.0, normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +now-and-later@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-3.0.0.tgz#cdc045dc5b894b35793cf276cc3206077bb7302d" + integrity sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg== + dependencies: + once "^1.4.0" + npm-install-checks@^6.0.0: version "6.3.0" resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-6.3.0.tgz#046552d8920e801fa9f919cad569545d60e826fe" @@ -4619,7 +5181,14 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-assign@^4.1.1: +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -4811,6 +5380,28 @@ parse-ms@^2.1.0: resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== +parse5-htmlparser2-tree-adapter@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" + integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== + dependencies: + domhandler "^5.0.2" + parse5 "^7.0.0" + +parse5-parser-stream@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1" + integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== + dependencies: + parse5 "^7.0.0" + +parse5@^7.0.0, parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -4836,6 +5427,11 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== +path-posix@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" + integrity sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA== + path-scurry@^1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" @@ -5020,6 +5616,11 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== +promise-map-series@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/promise-map-series/-/promise-map-series-0.3.0.tgz#41873ca3652bb7a042b387d538552da9b576f8a1" + integrity sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA== + promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -5100,6 +5701,20 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +queue-tick@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" + integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== + +quick-temp@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/quick-temp/-/quick-temp-0.1.8.tgz#bab02a242ab8fb0dd758a3c9776b32f9a5d94408" + integrity sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA== + dependencies: + mktemp "~0.4.0" + rimraf "^2.5.4" + underscore.string "~3.3.4" + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -5148,6 +5763,14 @@ react-dom@^18.2.0: loose-envify "^1.1.0" scheduler "^0.23.2" +react-i18next@^15.0.1: + version "15.0.1" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.0.1.tgz#fc662d93829ecb39683fe2757a47ebfbc5c912a0" + integrity sha512-NwxLqNM6CLbeGA9xPsjits0EnXdKgCRSS6cgkgOdNcPXqL+1fYNl8fBg1wmnnHvFy812Bt4IWTPE9zjoPmFj3w== + dependencies: + "@babel/runtime" "^7.24.8" + html-parse-stringify "^3.0.1" + react-is@^16.13.1, react-is@^16.3.2: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -5299,6 +5922,21 @@ remark-rehype@^10.0.0: mdast-util-to-hast "^12.1.0" unified "^10.0.0" +remix-i18next@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/remix-i18next/-/remix-i18next-6.3.0.tgz#1a086486ec0d6b13c262baf3b4420227fd4d8f8f" + integrity sha512-QisIBEv/XR29/FldR9NDwrQ712FRXceJlzstE+2dES2fG8K0TOcGan/bTOD+e+WLEwDqTf1lbBrqp5P7Ik/Eww== + +remove-trailing-separator@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== + +replace-ext@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06" + integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug== + "require-like@>= 0.1.1": version "0.1.2" resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" @@ -5309,6 +5947,13 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-options@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-2.0.0.tgz#a1a57a9949db549dd075de3f5550675f02f1e4c5" + integrity sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A== + dependencies: + value-or-function "^4.0.0" + resolve-pkg-maps@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" @@ -5355,6 +6000,13 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== +rimraf@^2.5.4: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -5387,6 +6039,16 @@ rollup@^4.20.0: "@rollup/rollup-win32-x64-msvc" "4.21.2" fsevents "~2.3.2" +rsvp@^4.8.2: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +rsvp@~3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" + integrity sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg== + run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -5430,7 +6092,7 @@ safe-regex-test@^1.0.3: es-errors "^1.3.0" is-regex "^1.1.4" -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -5559,6 +6221,13 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +sort-keys@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.1.0.tgz#50a3f3d1ad3c5a76d043e0aeeba7299241e9aa5c" + integrity sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ== + dependencies: + is-plain-obj "^4.0.0" + "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" @@ -5613,6 +6282,11 @@ spdx-license-ids@^3.0.0: resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== +sprintf-js@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + ssri@^10.0.0: version "10.0.6" resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" @@ -5632,6 +6306,13 @@ stop-iteration-iterator@^1.0.0: dependencies: internal-slot "^1.0.4" +stream-composer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stream-composer/-/stream-composer-1.0.2.tgz#7ee61ca1587bf5f31b2e29aa2093cbf11442d152" + integrity sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w== + dependencies: + streamx "^2.13.2" + stream-shift@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" @@ -5642,6 +6323,17 @@ stream-slice@^0.1.2: resolved "https://registry.yarnpkg.com/stream-slice/-/stream-slice-0.1.2.tgz#2dc4f4e1b936fb13f3eb39a2def1932798d07a4b" integrity sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA== +streamx@^2.12.0, streamx@^2.12.5, streamx@^2.13.2, streamx@^2.14.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.0.tgz#5f3608483499a9346852122b26042f964ceec931" + integrity sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ== + dependencies: + fast-fifo "^1.3.2" + queue-tick "^1.0.1" + text-decoder "^1.1.0" + optionalDependencies: + bare-events "^2.2.0" + string-hash@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" @@ -5805,6 +6497,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symlink-or-copy@^1.1.8, symlink-or-copy@^1.2.0, symlink-or-copy@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz#9506dd64d8e98fa21dcbf4018d1eab23e77f71fe" + integrity sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA== + tapable@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -5843,12 +6540,26 @@ tar@^6.1.11: mkdirp "^1.0.3" yallist "^4.0.0" +teex@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/teex/-/teex-1.0.1.tgz#b8fa7245ef8e8effa8078281946c85ab780a0b12" + integrity sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg== + dependencies: + streamx "^2.12.5" + +text-decoder@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.1.1.tgz#5df9c224cebac4a7977720b9f083f9efa1aefde8" + integrity sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA== + dependencies: + b4a "^1.6.4" + text-table@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -through2@^2.0.3: +through2@^2.0.1, through2@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -5868,6 +6579,13 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +to-through@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/to-through/-/to-through-3.0.0.tgz#bf4956eaca5a0476474850a53672bed6906ace54" + integrity sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw== + dependencies: + streamx "^2.12.5" + toidentifier@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" @@ -5878,6 +6596,11 @@ toml@^3.0.0: resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" @@ -5991,6 +6714,11 @@ typed-array-length@^1.0.6: is-typed-array "^1.1.13" possible-typed-array-names "^1.0.0" +typescript@^5.0.4: + version "5.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" + integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== + typescript@^5.1.6: version "5.5.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" @@ -6026,12 +6754,20 @@ uncontrollable@^8.0.1: resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-8.0.4.tgz#a0a8307f638795162fafd0550f4a1efa0f8c5eb6" integrity sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ== +underscore.string@~3.3.4: + version "3.3.6" + resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.6.tgz#ad8cf23d7423cb3b53b898476117588f4e2f9159" + integrity sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ== + dependencies: + sprintf-js "^1.1.1" + util-deprecate "^1.0.2" + undici-types@~6.19.2: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== -undici@^6.11.1: +undici@^6.11.1, undici@^6.19.5: version "6.19.8" resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== @@ -6121,6 +6857,11 @@ unist-util-visit@^4.0.0: unist-util-is "^5.0.0" unist-util-visit-parents "^5.1.1" +universalify@^0.1.0: + version "0.1.2" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + universalify@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" @@ -6190,6 +6931,11 @@ validate-npm-package-name@^5.0.0: resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8" integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ== +value-or-function@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-4.0.0.tgz#70836b6a876a010dc3a2b884e7902e9db064378d" + integrity sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg== + vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" @@ -6213,6 +6959,57 @@ vfile@^5.0.0: unist-util-stringify-position "^3.0.0" vfile-message "^3.0.0" +vinyl-contents@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/vinyl-contents/-/vinyl-contents-2.0.0.tgz#cc2ba4db3a36658d069249e9e36d9e2b41935d89" + integrity sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q== + dependencies: + bl "^5.0.0" + vinyl "^3.0.0" + +vinyl-fs@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-4.0.0.tgz#06cb36efc911c6e128452f230b96584a9133c3a1" + integrity sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw== + dependencies: + fs-mkdirp-stream "^2.0.1" + glob-stream "^8.0.0" + graceful-fs "^4.2.11" + iconv-lite "^0.6.3" + is-valid-glob "^1.0.0" + lead "^4.0.0" + normalize-path "3.0.0" + resolve-options "^2.0.0" + stream-composer "^1.0.2" + streamx "^2.14.0" + to-through "^3.0.0" + value-or-function "^4.0.0" + vinyl "^3.0.0" + vinyl-sourcemap "^2.0.0" + +vinyl-sourcemap@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz#422f410a0ea97cb54cebd698d56a06d7a22e0277" + integrity sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q== + dependencies: + convert-source-map "^2.0.0" + graceful-fs "^4.2.10" + now-and-later "^3.0.0" + streamx "^2.12.5" + vinyl "^3.0.0" + vinyl-contents "^2.0.0" + +vinyl@^3.0.0, vinyl@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-3.0.0.tgz#11e14732bf56e2faa98ffde5157fe6c13259ff30" + integrity sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g== + dependencies: + clone "^2.1.2" + clone-stats "^1.0.0" + remove-trailing-separator "^1.1.0" + replace-ext "^2.0.0" + teex "^1.0.1" + vite-node@^1.2.0: version "1.6.0" resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" @@ -6244,6 +7041,21 @@ vite@^5.0.0, vite@^5.0.11, vite@^5.1.0: optionalDependencies: fsevents "~2.3.3" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + +walk-sync@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-2.2.0.tgz#80786b0657fcc8c0e1c0b1a042a09eae2966387a" + integrity sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg== + dependencies: + "@types/minimatch" "^3.0.3" + ensure-posix-path "^1.1.0" + matcher-collection "^2.0.0" + minimatch "^3.0.4" + warning@^4.0.0, warning@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" @@ -6272,6 +7084,31 @@ web-streams-polyfill@^3.1.1: resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + which-boxed-primitive@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" From be34c4c77e436f19ff8f94b5949e96762471d5fd Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Sep 2024 21:24:40 +0200 Subject: [PATCH 038/261] feat(frontend): working email login --- Foxnouns.Backend/Services/AuthService.cs | 6 +- .../app/components/nav/Navbar.tsx | 2 +- Foxnouns.Frontend/app/lib/api/auth.ts | 7 +++ Foxnouns.Frontend/app/root.tsx | 7 +-- .../app/routes/$username/route.tsx | 4 +- .../app/routes/auth.log-in/route.tsx | 62 +++++++++++++++++++ Foxnouns.Frontend/i18next-parser.config.js | 4 +- Foxnouns.Frontend/public/locales/en.json | 6 ++ 8 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 Foxnouns.Frontend/app/lib/api/auth.ts create mode 100644 Foxnouns.Frontend/app/routes/auth.log-in/route.tsx diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index decb240..22a28d2 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -81,11 +81,11 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s var 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"); + throw new ApiError.NotFound("No user with that email address found, or password is incorrect", ErrorCode.UserNotFound); var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, password), ct); - if (pwResult == PasswordVerificationResult.Failed) - throw new ApiError.NotFound("No user with that email address found, or password is incorrect"); + 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 Task.Run(() => _passwordHasher.HashPassword(user, password), ct); diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx index 75f7d7d..dbccac7 100644 --- a/Foxnouns.Frontend/app/components/nav/Navbar.tsx +++ b/Foxnouns.Frontend/app/components/nav/Navbar.tsx @@ -36,7 +36,7 @@ export default function MainNavbar({ ) : ( - + {t("navbar.log-in")} ); diff --git a/Foxnouns.Frontend/app/lib/api/auth.ts b/Foxnouns.Frontend/app/lib/api/auth.ts new file mode 100644 index 0000000..8938c05 --- /dev/null +++ b/Foxnouns.Frontend/app/lib/api/auth.ts @@ -0,0 +1,7 @@ +import { User } from "~/lib/api/user"; + +export type AuthResponse = { + user: User; + token: string; + expires_at: string; +}; diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index b8b9d09..d70be26 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -7,8 +7,7 @@ import { ScrollRestoration, useLoaderData, } from "@remix-run/react"; -import { LoaderFunction } from "@remix-run/node"; -import SSRProvider from "react-bootstrap/SSRProvider"; +import { LoaderFunctionArgs } from "@remix-run/node"; import { useChangeLanguage } from "remix-i18next/react"; import { useTranslation } from "react-i18next"; @@ -22,7 +21,7 @@ import "./app.scss"; import getLocalSettings from "./lib/settings.server"; import { LANGUAGE } from "~/env.server"; -export const loader: LoaderFunction = async ({ request }) => { +export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest("GET", "/meta"); const token = getCookie(request, "pronounscc-token"); @@ -66,7 +65,7 @@ export function Layout({ children }: { children: React.ReactNode }) { - {children} + {children} diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index a26fb49..418b3e8 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -1,4 +1,4 @@ -import { json, LoaderFunction, MetaFunction } from "@remix-run/node"; +import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { redirect, useLoaderData } from "@remix-run/react"; import { User } from "~/lib/api/user"; import serverRequest from "~/lib/request.server"; @@ -9,7 +9,7 @@ export const meta: MetaFunction = ({ data }) => { return [{ title: `@${user.username} - pronouns.cc` }]; }; -export const loader: LoaderFunction = async ({ params }) => { +export const loader = async ({ params }: LoaderFunctionArgs) => { let username = params.username!; if (!username.startsWith("@")) throw redirect(`/@${username}`); username = username.substring("@".length); diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx new file mode 100644 index 0000000..4fd891f --- /dev/null +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -0,0 +1,62 @@ +import { MetaFunction, json, LoaderFunctionArgs, ActionFunction, redirect } from "@remix-run/node"; +import { Form as RemixForm } from "@remix-run/react"; +import Form from "react-bootstrap/Form"; +import Button from "react-bootstrap/Button"; +import { useTranslation } from "react-i18next"; +import i18n from "~/i18next.server"; +import serverRequest, { writeCookie } from "~/lib/request.server"; +import { AuthResponse } from "~/lib/api/auth"; + +export const meta: MetaFunction = ({ data }) => { + return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; +}; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); + + return json({ + meta: { title: t("log-in.title") }, + }); +}; + +export const action: ActionFunction = async ({ request }) => { + const body = await request.formData(); + const email = body.get("email") as string | null; + const password = body.get("password") as string | null; + + console.log(email, password); + + const resp = await serverRequest("POST", "/auth/email/login", { + body: { email, password }, + }); + + return redirect("/", { + status: 303, + headers: { + "Set-Cookie": writeCookie("pronounscc-token", resp.token), + }, + }); +}; + +export default function LoginPage() { + const { t } = useTranslation(); + + return ( + +
+ + {t("log-in.email")} + + + + {t("log-in.password")} + + + + +
+
+ ); +} diff --git a/Foxnouns.Frontend/i18next-parser.config.js b/Foxnouns.Frontend/i18next-parser.config.js index 41d6da6..a1e6625 100644 --- a/Foxnouns.Frontend/i18next-parser.config.js +++ b/Foxnouns.Frontend/i18next-parser.config.js @@ -1,5 +1,3 @@ -import i18n from "./app/i18n.js"; - export default { - locales: i18n.supportedLngs, + locales: ["en"], }; diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index f576541..17b3948 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -8,5 +8,11 @@ "theme-auto": "Automatic", "theme-dark": "Dark", "theme-light": "Light" + }, + "log-in": { + "title": "Log in", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in" } } From 2682cabfb01b3d9220e8b115e22a40de43ee990f Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Sep 2024 16:23:45 +0200 Subject: [PATCH 039/261] refactor: add DatabaseContext.GetToken method --- .../Authentication/EmailAuthController.cs | 30 +++++++++---------- .../Controllers/InternalController.cs | 10 ++----- .../Database/DatabaseQueryExtensions.cs | 14 +++++++++ .../Middleware/AuthenticationMiddleware.cs | 9 ++---- Foxnouns.Backend/Services/AuthService.cs | 12 +++++--- Foxnouns.Backend/Utils/AuthUtils.cs | 11 +++++++ 6 files changed, 51 insertions(+), 35 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index fcf8b8e..e396b9c 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -30,7 +30,7 @@ public class EmailAuthController( if (!req.Email.Contains('@')) throw new ApiError.BadRequest("Email is invalid", "email", req.Email); var state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, userId: null, ct); - + // 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(); @@ -40,55 +40,53 @@ public class EmailAuthController( } [HttpPost("callback")] - public async Task CallbackAsync([FromBody] CallbackRequest req, CancellationToken ct = default) + public async Task CallbackAsync([FromBody] CallbackRequest req) { CheckRequirements(); - var state = await keyCacheService.GetRegisterEmailStateAsync(req.State, ct); + var state = await keyCacheService.GetRegisterEmailStateAsync(req.State); if (state == null) throw new ApiError.BadRequest("Invalid state", "state", req.State); - + // If this callback is for an existing user, add the email address to their auth methods if (state.ExistingUserId != null) { var authMethod = - await authService.AddAuthMethodAsync(state.ExistingUserId.Value, AuthType.Email, state.Email, ct: ct); + await authService.AddAuthMethodAsync(state.ExistingUserId.Value, AuthType.Email, state.Email); _logger.Debug("Added email auth {AuthId} for user {UserId}", authMethod.Id, state.ExistingUserId); return NoContent(); } var ticket = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20), ct); + await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); return Ok(new AuthController.CallbackResponse(false, ticket, state.Email)); } [HttpPost("complete-registration")] - public async Task CompleteRegistrationAsync([FromBody] CompleteRegistrationRequest req, - CancellationToken ct = default) + public async Task CompleteRegistrationAsync([FromBody] CompleteRegistrationRequest req) { - var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}", ct: ct); + var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); if (email == null) throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket); // Check if username is valid at all ValidationUtils.Validate([("username", ValidationUtils.ValidateUsername(req.Username))]); // Check if username is already taken - if (await db.Users.AnyAsync(u => u.Username == req.Username, ct)) + if (await db.Users.AnyAsync(u => u.Username == req.Username)) throw new ApiError.BadRequest("Username is already taken", "username", req.Username); - var user = await authService.CreateUserWithPasswordAsync(req.Username, email, req.Password, ct); - var frontendApp = await db.GetFrontendApplicationAsync(ct); + var user = await authService.CreateUserWithPasswordAsync(req.Username, email, req.Password); + var frontendApp = await db.GetFrontendApplicationAsync(); var (tokenStr, token) = authService.GenerateToken(user, frontendApp, ["*"], clock.GetCurrentInstant() + Duration.FromDays(365)); db.Add(token); - await db.SaveChangesAsync(ct); + await db.SaveChangesAsync(); - // Specifically do *not* pass the CancellationToken so we don't cancel the rendering after creating the user account. - await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}", ct: default); + await keyCacheService.DeleteKeyAsync($"email:{req.Ticket}"); return Ok(new AuthController.AuthResponse( - await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: default), + await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false), tokenStr, token.ExpiresAt )); diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 859c2fe..eb22881 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -40,16 +40,10 @@ public partial class InternalController(DatabaseContext db, IClock clock) : Cont template = GetCleanedTemplate(template); // If no token was supplied, or it isn't valid base 64, return a null user ID (limiting by IP) - if (req.Token == null) return Ok(new RequestDataResponse(null, template)); - if (!AuthUtils.TryFromBase64String(req.Token, out var rawToken)) + if (!AuthUtils.TryParseToken(req.Token, out var rawToken)) return Ok(new RequestDataResponse(null, template)); - 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); - + var oauthToken = await db.GetToken(rawToken); return Ok(new RequestDataResponse(oauthToken?.UserId, template)); } diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 76043d7..6fe4c58 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -105,4 +105,18 @@ public static class DatabaseQueryExtensions await context.SaveChangesAsync(ct); return app; } + + public static async Task GetToken(this DatabaseContext context, byte[] rawToken, + CancellationToken ct = default) + { + var hash = SHA512.HashData(rawToken); + var 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; + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index 516813b..36cbcb3 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -20,18 +20,13 @@ public class AuthenticationMiddleware(DatabaseContext db, IClock clock) : IMiddl return; } - var header = ctx.Request.Headers.Authorization.ToString(); - if (!AuthUtils.TryFromBase64String(header, out var rawToken)) + if (!AuthUtils.TryParseToken(ctx.Request.Headers.Authorization.ToString(), out var rawToken)) { await next(ctx); return; } - 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); + var oauthToken = await db.GetToken(rawToken); if (oauthToken == null) { await next(ctx); diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index 22a28d2..f69441a 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -16,7 +16,8 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// 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) + public async Task CreateUserWithPasswordAsync(string username, string email, string password, + CancellationToken ct = default) { var user = new User { @@ -76,16 +77,19 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// 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) + public async Task<(User, EmailAuthenticationResult)> AuthenticateUserAsync(string email, string password, + CancellationToken ct = default) { var 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); + throw new ApiError.NotFound("No user with that email address found, or password is incorrect", + ErrorCode.UserNotFound); var pwResult = await Task.Run(() => _passwordHasher.VerifyHashedPassword(user, user.Password!, 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); + throw new ApiError.NotFound("No user with that email address found, or password is incorrect", + ErrorCode.UserNotFound); if (pwResult == PasswordVerificationResult.SuccessRehashNeeded) { user.Password = await Task.Run(() => _passwordHasher.HashPassword(user, password), ct); diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index badc19b..26965e2 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -79,6 +79,17 @@ public static class AuthUtils 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 RandomToken(int bytes = 48) => Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); From 4ac00017953569e6e789788044c19c22f66b110a Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Sep 2024 16:34:08 +0200 Subject: [PATCH 040/261] fix: only query user ID in /api/internal/request-data --- .idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml | 4 ++++ Foxnouns.Backend/Controllers/InternalController.cs | 4 ++-- Foxnouns.Backend/Database/DatabaseQueryExtensions.cs | 10 ++++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 .idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml diff --git a/.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml b/.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml new file mode 100644 index 0000000..fb0d65a --- /dev/null +++ b/.idea/.idea.Foxnouns.NET/.idea/watcherTasks.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index eb22881..cda2edb 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -43,8 +43,8 @@ public partial class InternalController(DatabaseContext db, IClock clock) : Cont if (!AuthUtils.TryParseToken(req.Token, out var rawToken)) return Ok(new RequestDataResponse(null, template)); - var oauthToken = await db.GetToken(rawToken); - return Ok(new RequestDataResponse(oauthToken?.UserId, template)); + var userId = await db.GetTokenUserId(rawToken); + return Ok(new RequestDataResponse(userId, template)); } public record RequestDataRequest(string? Token, string Method, string Path); diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 6fe4c58..f8a544c 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -110,6 +110,7 @@ public static class DatabaseQueryExtensions CancellationToken ct = default) { var hash = SHA512.HashData(rawToken); + var oauthToken = await context.Tokens .Include(t => t.Application) .Include(t => t.User) @@ -119,4 +120,13 @@ public static class DatabaseQueryExtensions return oauthToken; } + + public static async Task GetTokenUserId(this DatabaseContext context, byte[] rawToken, + CancellationToken ct = default) + { + var 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 From 116d0577a7ee2203e24737fc953ffe8e37ebd6ea Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Sep 2024 19:13:54 +0200 Subject: [PATCH 041/261] improve login page --- .../app/components/ErrorAlert.tsx | 38 +++++ Foxnouns.Frontend/app/lib/api/auth.ts | 6 + Foxnouns.Frontend/app/lib/request.server.ts | 2 + Foxnouns.Frontend/app/root.tsx | 70 +++++++++- .../app/routes/auth.log-in/route.tsx | 130 ++++++++++++++---- Foxnouns.Frontend/public/locales/en.json | 26 +++- 6 files changed, 238 insertions(+), 34 deletions(-) create mode 100644 Foxnouns.Frontend/app/components/ErrorAlert.tsx diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx new file mode 100644 index 0000000..e30c516 --- /dev/null +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -0,0 +1,38 @@ +import { TFunction } from "i18next"; +import Alert from "react-bootstrap/Alert"; +import { useTranslation } from "react-i18next"; +import { ApiError, ErrorCode } from "~/lib/api/error"; + +export default function ErrorAlert({ error }: { error: ApiError }) { + const { t } = useTranslation(); + + return ( + + {t("error.heading")} + {errorCodeDesc(t, error.code)} + + ); +} + +export const errorCodeDesc = (t: TFunction, code: ErrorCode) => { + switch (code) { + case ErrorCode.AuthenticationError: + return t("error.errors.authentication-error"); + case ErrorCode.AuthenticationRequired: + return t("error.errors.authentication-required"); + case ErrorCode.BadRequest: + return t("error.errors.bad-request"); + case ErrorCode.Forbidden: + return t("error.errors.forbidden"); + case ErrorCode.GenericApiError: + return t("error.errors.generic-error"); + case ErrorCode.InternalServerError: + return t("error.errors.internal-server-error"); + case ErrorCode.MemberNotFound: + return t("error.errors.member-not-found"); + case ErrorCode.UserNotFound: + return t("error.errors.user-not-found"); + } + + return t("error.errors.generic-error"); +}; diff --git a/Foxnouns.Frontend/app/lib/api/auth.ts b/Foxnouns.Frontend/app/lib/api/auth.ts index 8938c05..bda9925 100644 --- a/Foxnouns.Frontend/app/lib/api/auth.ts +++ b/Foxnouns.Frontend/app/lib/api/auth.ts @@ -5,3 +5,9 @@ export type AuthResponse = { token: string; expires_at: string; }; + +export type AuthUrls = { + discord?: string; + google?: string; + tumblr?: string; +}; diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index d7add9b..d6192ae 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -39,6 +39,8 @@ export default async function serverRequest( return (await resp.json()) as T; } +export const getToken = (req: Request) => getCookie(req, "pronounscc-token"); + export function getCookie(req: Request, cookieName: string): string | undefined { const header = req.headers.get("Cookie"); if (!header) return undefined; diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index d70be26..8452919 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -6,6 +6,8 @@ import { Scripts, ScrollRestoration, useLoaderData, + useRouteError, + useRouteLoaderData, } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/node"; import { useChangeLanguage } from "remix-i18next/react"; @@ -20,6 +22,7 @@ import { ApiError, ErrorCode } from "./lib/api/error"; import "./app.scss"; import getLocalSettings from "./lib/settings.server"; import { LANGUAGE } from "~/env.server"; +import { errorCodeDesc } from "./components/ErrorAlert"; export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest("GET", "/meta"); @@ -51,13 +54,29 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }; export function Layout({ children }: { children: React.ReactNode }) { - const { settings, locale } = useLoaderData(); + const { locale, settings } = useRouteLoaderData("root") || { + meta: { + users: { + total: 0, + active_month: 0, + active_week: 0, + active_day: 0, + }, + members: 0, + version: "", + hash: "", + }, + }; const { i18n } = useTranslation(); - i18n.language = locale; - useChangeLanguage(locale); + i18n.language = locale || "en"; + useChangeLanguage(locale || "en"); return ( - + @@ -73,6 +92,49 @@ export function Layout({ children }: { children: React.ReactNode }) { ); } +export function ErrorBoundary() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const error: any = useRouteError(); + const { t } = useTranslation(); + + console.log(error); + + const errorElem = + "code" in error && "message" in error ? ( + + ) : ( + <>{t("error.errors.generic-error")} + ); + + return ( + + + + <>{t("error.title")} - pronouns.cc</> + + + + + + {errorElem} + + + + ); +} + +function ApiErrorElem({ error }: { error: ApiError }) { + const { t } = useTranslation(); + const errorDesc = errorCodeDesc(t, error.code); + + return ( + <> +

{t("error.heading")}

+

{errorDesc}

+ + ); +} + export default function App() { const { meta, meUser, settings } = useLoaderData(); diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index 4fd891f..3d0d387 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -1,11 +1,23 @@ -import { MetaFunction, json, LoaderFunctionArgs, ActionFunction, redirect } from "@remix-run/node"; -import { Form as RemixForm } from "@remix-run/react"; +import { + MetaFunction, + json, + LoaderFunctionArgs, + redirect, + ActionFunctionArgs, +} from "@remix-run/node"; +import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react"; import Form from "react-bootstrap/Form"; import Button from "react-bootstrap/Button"; +import ButtonGroup from "react-bootstrap/ButtonGroup"; +import ListGroup from "react-bootstrap/ListGroup"; +import { Container, Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; -import serverRequest, { writeCookie } from "~/lib/request.server"; -import { AuthResponse } from "~/lib/api/auth"; +import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; +import { AuthResponse, AuthUrls } from "~/lib/api/auth"; +import { ApiError, ErrorCode } from "~/lib/api/error"; +import ErrorAlert from "~/components/ErrorAlert"; +import { User } from "~/lib/api/user"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; @@ -13,50 +25,110 @@ export const meta: MetaFunction = ({ data }) => { export const loader = async ({ request }: LoaderFunctionArgs) => { const t = await i18n.getFixedT(request); + const token = getToken(request); + if (token) { + try { + await serverRequest("GET", "/users/@me", { token }); + return redirect("/?err=already-logged-in", 303); + } catch (e) { + // ignore + } + } + + const urls = await serverRequest("POST", "/auth/urls"); return json({ meta: { title: t("log-in.title") }, + urls, }); }; -export const action: ActionFunction = async ({ request }) => { +export const action = async ({ request }: ActionFunctionArgs) => { const body = await request.formData(); const email = body.get("email") as string | null; const password = body.get("password") as string | null; console.log(email, password); - const resp = await serverRequest("POST", "/auth/email/login", { - body: { email, password }, - }); + try { + const resp = await serverRequest("POST", "/auth/email/login", { + body: { email, password }, + }); - return redirect("/", { - status: 303, - headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token), - }, - }); + return redirect("/", { + status: 303, + headers: { + "Set-Cookie": writeCookie("pronounscc-token", resp.token), + }, + }); + } catch (e) { + return json({ error: e as ApiError }); + } }; export default function LoginPage() { const { t } = useTranslation(); + const { urls } = useLoaderData(); + const actionData = useActionData(); return ( - -
- - {t("log-in.email")} - - - - {t("log-in.password")} - - + + + +

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

+ {actionData?.error && } + + + + {t("log-in.email")} + + + + {t("log-in.password")} + + - - - + + + + + +
+ + +

{t("log-in.3rd-party.title")}

+

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

+ + {urls.discord && ( + + {t("log-in.3rd-party.discord")} + + )} + {urls.google && ( + + {t("log-in.3rd-party.google")} + + )} + {urls.tumblr && ( + + {t("log-in.3rd-party.tumblr")} + + )} + + + + ); } + +function LoginError({ error }: { error: ApiError }) { + const { t } = useTranslation(); + + if (error.code !== ErrorCode.UserNotFound) return ; + + return <>{t("log-in.invalid-credentials")}; +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 17b3948..d383266 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,4 +1,18 @@ { + "error": { + "heading": "An error occurred", + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "Error" + }, "navbar": { "view-profile": "View profile", "settings": "Settings", @@ -11,8 +25,18 @@ }, "log-in": { "title": "Log in", + "form-title": "Log in with email", "email": "Email address", "password": "Password", - "log-in-button": "Log in" + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." } } From ff22530f0a20f6768db1b60c16aa001b8cec0420 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 13 Sep 2024 14:56:38 +0200 Subject: [PATCH 042/261] feat(frontend): add discord callback page this only handles existing accounts for now, still need to write an action function --- .../inspectionProfiles/Project_Default.xml | 3 + .../Authentication/AuthController.cs | 15 +++- .../Authentication/DiscordAuthController.cs | 23 +++-- .../Authentication/EmailAuthController.cs | 3 +- .../Controllers/InternalController.cs | 5 +- .../Extensions/KeyCacheExtensions.cs | 2 +- .../Middleware/AuthenticationMiddleware.cs | 5 +- .../Services/RemoteAuthService.cs | 12 ++- Foxnouns.Frontend/app/lib/api/auth.ts | 9 ++ Foxnouns.Frontend/app/root.tsx | 5 +- .../routes/auth.callback.discord/route.tsx | 84 +++++++++++++++++ .../app/routes/auth.log-in/route.tsx | 6 +- Foxnouns.Frontend/public/locales/en.json | 90 ++++++++++--------- 13 files changed, 196 insertions(+), 66 deletions(-) create mode 100644 Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx diff --git a/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml index 03d9549..ec33848 100644 --- a/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml @@ -2,5 +2,8 @@ \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 92267e6..4c83ce4 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -2,6 +2,7 @@ using System.Web; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; @@ -26,7 +27,7 @@ public class AuthController(Config config, KeyCacheService keyCache, ILogger log $"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/login/discord")}"; + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; return Ok(new UrlsResponse(discord, null, null)); } @@ -45,8 +46,16 @@ public class AuthController(Config config, KeyCacheService keyCache, ILogger log public record CallbackResponse( bool HasAccount, // If true, user has an account, but it's deleted - string Ticket, - string? RemoteUsername + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? Ticket, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? RemoteUsername, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + UserRendererService.UserResponse? User, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? Token, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + Instant? ExpiresAt ); public record OauthRegisterRequest(string Ticket, string Username); diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 068c8e9..20840ad 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -25,7 +25,6 @@ public class DiscordAuthController( [HttpPost("callback")] // TODO: duplicating attribute doesn't work, find another way to mark both as possible response // leaving it here for documentation purposes - [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req, CancellationToken ct = default) { @@ -42,7 +41,14 @@ public class DiscordAuthController( var ticket = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync($"discord:{ticket}", remoteUser, Duration.FromMinutes(20), ct); - return Ok(new AuthController.CallbackResponse(false, ticket, remoteUser.Username)); + return Ok(new AuthController.CallbackResponse( + HasAccount: false, + Ticket: ticket, + RemoteUsername: remoteUser.Username, + User: null, + Token: null, + ExpiresAt: null + )); } [HttpPost("register")] @@ -64,7 +70,7 @@ public class DiscordAuthController( return Ok(await GenerateUserTokenAsync(user, ct)); } - private async Task GenerateUserTokenAsync(User user, CancellationToken ct = default) + private async Task GenerateUserTokenAsync(User user, CancellationToken ct = default) { var frontendApp = await db.GetFrontendApplicationAsync(ct); _logger.Debug("Logging user {Id} in with Discord", user.Id); @@ -77,10 +83,13 @@ public class DiscordAuthController( await db.SaveChangesAsync(ct); - return new AuthController.AuthResponse( - await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), - tokenStr, - token.ExpiresAt + return new AuthController.CallbackResponse( + HasAccount: true, + Ticket: null, + RemoteUsername: null, + User: await userRenderer.RenderUserAsync(user, selfUser: user, renderMembers: false, ct: ct), + Token: tokenStr, + ExpiresAt: token.ExpiresAt ); } diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index e396b9c..b7e8ff4 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -59,7 +59,8 @@ public class EmailAuthController( var ticket = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); - return Ok(new AuthController.CallbackResponse(false, ticket, state.Email)); + return Ok(new AuthController.CallbackResponse(HasAccount: false, Ticket: ticket, RemoteUsername: state.Email, + User: null, Token: null, ExpiresAt: null)); } [HttpPost("complete-registration")] diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index cda2edb..265cf3d 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -1,4 +1,3 @@ -using System.Security.Cryptography; using System.Text.RegularExpressions; using Foxnouns.Backend.Database; using Foxnouns.Backend.Utils; @@ -6,14 +5,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing.Template; -using Microsoft.EntityFrameworkCore; -using NodaTime; namespace Foxnouns.Backend.Controllers; [ApiController] [Route("/api/internal")] -public partial class InternalController(DatabaseContext db, IClock clock) : ControllerBase +public partial class InternalController(DatabaseContext db) : ControllerBase { [GeneratedRegex(@"(\{\w+\})")] private static partial Regex PathVarRegex(); diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index 7de7396..4f70bee 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -11,7 +11,7 @@ public static class KeyCacheExtensions public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheService, CancellationToken ct = default) { - var state = AuthUtils.RandomToken(); + var state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct); return state; } diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index 36cbcb3..5c69b79 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -1,13 +1,10 @@ -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, IClock clock) : IMiddleware +public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware { public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { diff --git a/Foxnouns.Backend/Services/RemoteAuthService.cs b/Foxnouns.Backend/Services/RemoteAuthService.cs index 389412f..b08b0a5 100644 --- a/Foxnouns.Backend/Services/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/RemoteAuthService.cs @@ -3,8 +3,9 @@ using System.Web; namespace Foxnouns.Backend.Services; -public class RemoteAuthService(Config config) +public class RemoteAuthService(Config config, ILogger logger) { + private readonly ILogger _logger = logger.ForContext(); private readonly HttpClient _httpClient = new(); private readonly Uri _discordTokenUri = new("https://discord.com/api/oauth2/token"); @@ -12,7 +13,7 @@ public class RemoteAuthService(Config config) public async Task RequestDiscordTokenAsync(string code, string state, CancellationToken ct = default) { - var redirectUri = $"{config.BaseUrl}/auth/login/discord"; + var redirectUri = $"{config.BaseUrl}/auth/callback/discord"; var resp = await _httpClient.PostAsync(_discordTokenUri, new FormUrlEncodedContent( new Dictionary { @@ -23,6 +24,13 @@ public class RemoteAuthService(Config config) { "redirect_uri", redirectUri } } ), ct); + if (!resp.IsSuccessStatusCode) + { + var 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"); + } + resp.EnsureSuccessStatusCode(); var token = await resp.Content.ReadFromJsonAsync(ct); if (token == null) throw new FoxnounsError("Discord token response was null"); diff --git a/Foxnouns.Frontend/app/lib/api/auth.ts b/Foxnouns.Frontend/app/lib/api/auth.ts index bda9925..a0d5bf1 100644 --- a/Foxnouns.Frontend/app/lib/api/auth.ts +++ b/Foxnouns.Frontend/app/lib/api/auth.ts @@ -6,6 +6,15 @@ export type AuthResponse = { expires_at: string; }; +export type CallbackResponse = { + has_account: boolean; + ticket?: string; + remote_username?: string; + user?: User; + token?: string; + expires_at?: string; +}; + export type AuthUrls = { discord?: string; google?: string; diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index 8452919..a3296b4 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -23,6 +23,7 @@ import "./app.scss"; import getLocalSettings from "./lib/settings.server"; import { LANGUAGE } from "~/env.server"; import { errorCodeDesc } from "./components/ErrorAlert"; +import { Container } from "react-bootstrap"; export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest("GET", "/meta"); @@ -141,7 +142,9 @@ export default function App() { return ( <> - + + + ); } diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx new file mode 100644 index 0000000..f946ee0 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -0,0 +1,84 @@ +import { json, LoaderFunctionArgs } from "@remix-run/node"; +import { type ApiError, ErrorCode } from "~/lib/api/error"; +import serverRequest, { writeCookie } from "~/lib/request.server"; +import { CallbackResponse } from "~/lib/api/auth"; +import { Form as RemixForm, Link, useLoaderData } from "@remix-run/react"; +import { Trans, useTranslation } from "react-i18next"; +import Form from "react-bootstrap/Form"; +import Button from "react-bootstrap/Button"; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const url = new URL(request.url); + + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + + if (!code || !state) + throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code or state" } as ApiError; + + const resp = await serverRequest("POST", "/auth/discord/callback", { + body: { code, state } + }); + + if (resp.has_account) { + return json( + { hasAccount: true, user: resp.user!, ticket: null, remoteUser: null }, + { + headers: { + "Set-Cookie": writeCookie("pronounscc-token", resp.token!) + } + } + ); + } + + return json({ + hasAccount: false, + user: null, + ticket: resp.ticket!, + remoteUser: resp.remote_username! + }); +}; + +// TODO: action function + +export default function DiscordCallbackPage() { + const { t } = useTranslation(); + const data = useLoaderData(); + + if (data.hasAccount) { + const username = data.user!.username; + + return ( + <> +

{t("log-in.callback.success")}

+

+ + {/* @ts-expect-error react-i18next handles interpolation here */} + Welcome back, @{{username}}! + +
+ {t("log-in.callback.redirect-hint")} +

+ + ); + } + + return ( + +
+ + {t("log-in.callback.remote-username.discord")} + + + + {t("log-in.callback.username")} + + + + +
+
+ ); +} diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index 3d0d387..1f55fda 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -10,7 +10,7 @@ import Form from "react-bootstrap/Form"; import Button from "react-bootstrap/Button"; import ButtonGroup from "react-bootstrap/ButtonGroup"; import ListGroup from "react-bootstrap/ListGroup"; -import { Container, Row, Col } from "react-bootstrap"; +import { Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; @@ -72,7 +72,7 @@ export default function LoginPage() { const actionData = useActionData(); return ( - + <>

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

@@ -121,7 +121,7 @@ export default function LoginPage() {
-
+ ); } diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index d383266..83455f7 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,42 +1,52 @@ { - "error": { - "heading": "An error occurred", - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "Error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up", - "theme": "Theme", - "theme-auto": "Automatic", - "theme-dark": "Dark", - "theme-light": "Light" - }, - "log-in": { - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - } + "error": { + "heading": "An error occurred", + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "Error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up", + "theme": "Theme", + "theme-auto": "Automatic", + "theme-dark": "Dark", + "theme-light": "Light" + }, + "log-in": { + "callback": { + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up" + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + } } From 103ba24555345e3ff33aedb97c7358edc28eb13f Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Sep 2024 16:37:27 +0200 Subject: [PATCH 043/261] feat(frontend): create account from discord, better error alert --- .../app/components/ErrorAlert.tsx | 112 +++++++++++++++++- Foxnouns.Frontend/app/lib/api/error.ts | 30 ++++- .../routes/auth.callback.discord/route.tsx | 106 +++++++++++++++-- Foxnouns.Frontend/public/locales/en.json | 110 +++++++++-------- 4 files changed, 292 insertions(+), 66 deletions(-) diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx index e30c516..cedb69a 100644 --- a/Foxnouns.Frontend/app/components/ErrorAlert.tsx +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -1,7 +1,13 @@ import { TFunction } from "i18next"; import Alert from "react-bootstrap/Alert"; -import { useTranslation } from "react-i18next"; -import { ApiError, ErrorCode } from "~/lib/api/error"; +import { Trans, useTranslation } from "react-i18next"; +import { + ApiError, + ErrorCode, + ValidationError, + validationErrorType, + ValidationErrorType, +} from "~/lib/api/error"; export default function ErrorAlert({ error }: { error: ApiError }) { const { t } = useTranslation(); @@ -10,10 +16,112 @@ export default function ErrorAlert({ error }: { error: ApiError }) { {t("error.heading")} {errorCodeDesc(t, error.code)} + {error.errors && ( +
    + {error.errors.map((e, i) => ( + + ))} +
+ )}
); } +function ValidationErrors({ errorKey, errors }: { errorKey: string; errors: ValidationError[] }) { + return ( +
  • + + {errorKey} + + : +
      + {errors.map((e, i) => ( +
    • + +
    • + ))} +
    +
  • + ); +} + +function ValidationErrorEntry({ error }: { error: ValidationError }) { + const { t } = useTranslation(); + + const { + min_length: minLength, + max_length: maxLength, + actual_length: actualLength, + message: reason, + actual_value: actualValue, + allowed_values: allowedValues, + } = error; + + switch (validationErrorType(error)) { + case ValidationErrorType.LengthError: + if (error.actual_length! > error.max_length!) { + return ( + + Value is too long, maximum length is {{ maxLength }}, current length is{" "} + {{ actualLength }}. + + ); + } + + if (error.actual_length! < error.min_length!) { + return ( + + Value is too short, minimum length is {{ minLength }}, current length is{" "} + {{ actualLength }}. + + ); + } + + break; + + case ValidationErrorType.DisallowedValueError: + return ( + v.toString()).join(", "), + }} + > + {/* @ts-expect-error i18next handles interpolation */} + The value {{ actualValue }} is not allowed here. Allowed values are:{" "} + {/* @ts-expect-error i18next handles interpolation */} + {{ allowedValues }} + + ); + + default: + if (error.actual_value) { + return ( + + {/* @ts-expect-error i18next handles interpolation */} + The value {{ actualValue }} is not allowed here. Reason: {{ reason }} + + ); + } + + return <>{t("error.validation.generic-no-value", { reason: error.message })}; + } +} + export const errorCodeDesc = (t: TFunction, code: ErrorCode) => { switch (code) { case ErrorCode.AuthenticationError: diff --git a/Foxnouns.Frontend/app/lib/api/error.ts b/Foxnouns.Frontend/app/lib/api/error.ts index 02e871c..0a3d9b9 100644 --- a/Foxnouns.Frontend/app/lib/api/error.ts +++ b/Foxnouns.Frontend/app/lib/api/error.ts @@ -3,7 +3,7 @@ export type ApiError = { status: number; message: string; code: ErrorCode; - errors?: ValidationError[]; + errors?: Array<{ key: string; errors: ValidationError[] }>; }; export enum ErrorCode { @@ -26,3 +26,31 @@ export type ValidationError = { 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: ApiError, 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; +}; + +export enum ValidationErrorType { + LengthError = 0, + DisallowedValueError = 1, + GenericValidationError = 2, +} + +export const validationErrorType = (error: ValidationError) => { + if (error.min_length && error.max_length && error.actual_length) { + return ValidationErrorType.LengthError; + } + if (error.allowed_values && error.actual_value) { + return ValidationErrorType.DisallowedValueError; + } + return ValidationErrorType.GenericValidationError; +}; diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index f946ee0..31d4d64 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -1,11 +1,23 @@ -import { json, LoaderFunctionArgs } from "@remix-run/node"; -import { type ApiError, ErrorCode } from "~/lib/api/error"; +import { ActionFunctionArgs, json, redirect, LoaderFunctionArgs } from "@remix-run/node"; +import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; import serverRequest, { writeCookie } from "~/lib/request.server"; -import { CallbackResponse } from "~/lib/api/auth"; -import { Form as RemixForm, Link, useLoaderData } from "@remix-run/react"; +import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; +import { + Form as RemixForm, + Link, + useActionData, + useLoaderData, + ShouldRevalidateFunction, +} from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; import Form from "react-bootstrap/Form"; import Button from "react-bootstrap/Button"; +import ErrorAlert from "~/components/ErrorAlert"; +import Alert from "react-bootstrap/Alert"; + +export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { + return !actionResult; +}; export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); @@ -17,7 +29,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code or state" } as ApiError; const resp = await serverRequest("POST", "/auth/discord/callback", { - body: { code, state } + body: { code, state }, }); if (resp.has_account) { @@ -25,9 +37,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { { hasAccount: true, user: resp.user!, ticket: null, remoteUser: null }, { headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token!) - } - } + "Set-Cookie": writeCookie("pronounscc-token", resp.token!), + }, + }, ); } @@ -35,26 +47,62 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { hasAccount: false, user: null, ticket: resp.ticket!, - remoteUser: resp.remote_username! + remoteUser: resp.remote_username!, }); }; -// TODO: action function +export const action = async ({ request }: ActionFunctionArgs) => { + 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 json({ + error: { + status: 403, + code: ErrorCode.BadRequest, + message: "Invalid username or ticket", + } as ApiError, + user: null, + }); + + try { + const resp = await serverRequest("POST", "/auth/discord/register", { + body: { username, ticket }, + }); + + return redirect("/auth/welcome", { + headers: { + "Set-Cookie": writeCookie("pronounscc-token", resp.token), + }, + status: 303, + }); + } catch (e) { + JSON.stringify(e); + + return json({ error: e as ApiError }); + } +}; export default function DiscordCallbackPage() { const { t } = useTranslation(); const data = useLoaderData(); + const actionData = useActionData(); if (data.hasAccount) { const username = data.user!.username; - + return ( <>

    {t("log-in.callback.success")}

    - + {/* @ts-expect-error react-i18next handles interpolation here */} - Welcome back, @{{username}}! + Welcome back, @{{ username }}!
    {t("log-in.callback.redirect-hint")} @@ -66,6 +114,7 @@ export default function DiscordCallbackPage() { return (

    + {actionData?.error && } {t("log-in.callback.remote-username.discord")} @@ -82,3 +131,34 @@ export default function DiscordCallbackPage() { ); } + +function RegisterError({ error }: { error: ApiError }) { + const { t } = useTranslation(); + + // TODO: maybe turn these messages into their own error codes? + const ticketMessage = firstErrorFor(error, "ticket")?.message; + const usernameMessage = firstErrorFor(error, "username")?.message; + + if (ticketMessage === "Invalid ticket") { + return ( + + {t("error.heading")} + + Invalid ticket (it might have been too long since you logged in with Discord), please{" "} + try again. + + + ); + } + + if (usernameMessage === "Username is already taken") { + return ( + + {t("log-in.callback.invalid-username")} + {t("log-in.callback.username-taken")} + + ); + } + + return ; +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 83455f7..f2dfbf2 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,52 +1,62 @@ { - "error": { - "heading": "An error occurred", - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "Error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up", - "theme": "Theme", - "theme-auto": "Automatic", - "theme-dark": "Dark", - "theme-light": "Light" - }, - "log-in": { - "callback": { - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up" - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - } + "error": { + "heading": "An error occurred", + "validation": { + "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", + "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", + "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", + "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", + "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" + }, + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "Error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up", + "theme": "Theme", + "theme-auto": "Automatic", + "theme-dark": "Dark", + "theme-light": "Light" + }, + "log-in": { + "callback": { + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up", + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else." + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + } } From 2cef7523d23055d03d6977bb055f3afda8d0b0e9 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Sep 2024 16:37:52 +0200 Subject: [PATCH 044/261] chore(backend): silence some more resharper errors --- Foxnouns.Backend/Config.cs | 1 - .../Authentication/AuthController.cs | 2 +- .../Authentication/DiscordAuthController.cs | 23 +++++++++++-------- .../Authentication/EmailAuthController.cs | 3 ++- .../Controllers/InternalController.cs | 6 ++--- .../Controllers/MembersController.cs | 8 +++---- .../Database/DatabaseQueryExtensions.cs | 3 +-- .../Database/Models/Application.cs | 2 +- Foxnouns.Backend/ExpectedError.cs | 6 ++--- Foxnouns.Backend/Foxnouns.Backend.csproj | 1 + .../Middleware/ErrorHandlerMiddleware.cs | 1 - Foxnouns.Backend/Services/AuthService.cs | 1 + Foxnouns.Backend/Services/KeyCacheService.cs | 1 - .../Services/RemoteAuthService.cs | 15 ++++++++---- 14 files changed, 38 insertions(+), 35 deletions(-) diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 0781443..6b31a40 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using Serilog.Events; namespace Foxnouns.Backend; diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 4c83ce4..b9570c0 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -45,7 +45,7 @@ public class AuthController(Config config, KeyCacheService keyCache, ILogger log ); public record CallbackResponse( - bool HasAccount, // If true, user has an account, but it's deleted + bool HasAccount, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 20840ad..a1c3eed 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -11,7 +12,7 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/discord")] public class DiscordAuthController( - Config config, + [UsedImplicitly] Config config, ILogger logger, IClock clock, DatabaseContext db, @@ -26,14 +27,15 @@ public class DiscordAuthController( // TODO: duplicating attribute doesn't work, find another way to mark both as possible response // leaving it here for documentation purposes [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req, CancellationToken ct = default) + public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req, + CancellationToken ct = default) { CheckRequirements(); await keyCacheService.ValidateAuthStateAsync(req.State, ct); var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code, req.State, ct); var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id, ct: ct); - if (user != null) return Ok(await GenerateUserTokenAsync(user,ct)); + if (user != null) return Ok(await GenerateUserTokenAsync(user, ct)); _logger.Debug("Discord user {Username} ({Id}) authenticated with no local account", remoteUser.Username, remoteUser.Id); @@ -53,24 +55,25 @@ public class DiscordAuthController( [HttpPost("register")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req, CancellationToken ct = default) + public async Task RegisterAsync([FromBody] AuthController.OauthRegisterRequest req) { - var remoteUser = await keyCacheService.GetKeyAsync($"discord:{req.Ticket}",ct:ct); + var 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, ct)) + 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 FoxnounsError("Discord ticket was issued for user with existing link"); + throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); } var user = await authService.CreateUserWithRemoteAuthAsync(req.Username, AuthType.Discord, remoteUser.Id, - remoteUser.Username, ct: ct); + remoteUser.Username); - return Ok(await GenerateUserTokenAsync(user, ct)); + return Ok(await GenerateUserTokenAsync(user)); } - private async Task GenerateUserTokenAsync(User user, CancellationToken ct = default) + private async Task GenerateUserTokenAsync(User user, + CancellationToken ct = default) { var frontendApp = await db.GetFrontendApplicationAsync(ct); _logger.Debug("Logging user {Id} in with Discord", user.Id); diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index b7e8ff4..1649948 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; +using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -11,8 +12,8 @@ namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/v2/auth/email")] public class EmailAuthController( + [UsedImplicitly] Config config, DatabaseContext db, - Config config, AuthService authService, MailService mailService, KeyCacheService keyCacheService, diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 265cf3d..e63b579 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -17,7 +17,7 @@ public partial class InternalController(DatabaseContext db) : ControllerBase private static string GetCleanedTemplate(string template) { - if (template.StartsWith("api/v2")) template = template.Substring("api/v2".Length); + if (template.StartsWith("api/v2")) template = template["api/v2".Length..]; template = PathVarRegex() .Replace(template, "{id}") // Replace all path variables (almost always IDs) with `{id}` .Replace("@me", "{id}"); // Also replace hardcoded `@me` with `{id}` @@ -50,7 +50,7 @@ public partial class InternalController(DatabaseContext db) : ControllerBase Snowflake? UserId, string Template); - private static Endpoint? GetEndpoint(HttpContext httpContext, string url, string requestMethod) + private static RouteEndpoint? GetEndpoint(HttpContext httpContext, string url, string requestMethod) { var endpointDataSource = httpContext.RequestServices.GetService(); if (endpointDataSource == null) return null; @@ -60,7 +60,7 @@ public partial class InternalController(DatabaseContext db) : ControllerBase { if (endpoint.RoutePattern.RawText == null) continue; - var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), new()); + var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), new RouteValueDictionary()); if (!templateMatcher.TryMatch(url, new())) continue; var httpMethodAttribute = endpoint.Metadata.GetMetadata(); if (httpMethodAttribute != null && diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index f051ca1..a92f947 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -88,19 +88,17 @@ public class MembersController( [HttpDelete("/api/v2/users/@me/members/{memberRef}")] [Authorize("member.update")] - public async Task DeleteMemberAsync(string memberRef, CancellationToken ct = default) + public async Task DeleteMemberAsync(string memberRef) { - var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef, ct); + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); var deleteCount = await db.Members.Where(m => m.UserId == CurrentUser!.Id && m.Id == member.Id) - .ExecuteDeleteAsync(ct); + .ExecuteDeleteAsync(); if (deleteCount == 0) { _logger.Warning("Successfully resolved member {Id} but could not delete them", member.Id); return NoContent(); } - await db.SaveChangesAsync(ct); - if (member.Avatar != null) await objectStorageService.DeleteMemberAvatarAsync(member.Id, member.Avatar); return NoContent(); } diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index f8a544c..60d4499 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -1,7 +1,6 @@ using System.Security.Cryptography; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.EntityFrameworkCore; using NodaTime; @@ -95,7 +94,7 @@ public static class DatabaseQueryExtensions { Id = new Snowflake(0), ClientId = RandomNumberGenerator.GetHexString(32, true), - ClientSecret = AuthUtils.RandomToken(48), + ClientSecret = AuthUtils.RandomToken(), Name = "pronouns.cc", Scopes = ["*"], RedirectUris = [], diff --git a/Foxnouns.Backend/Database/Models/Application.cs b/Foxnouns.Backend/Database/Models/Application.cs index f64bfc9..49b711e 100644 --- a/Foxnouns.Backend/Database/Models/Application.cs +++ b/Foxnouns.Backend/Database/Models/Application.cs @@ -9,7 +9,7 @@ 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; set; } + public required string[] RedirectUris { get; init; } public static Application Create(ISnowflakeGenerator snowflakeGenerator, string name, string[] scopes, string[] redirectUrls) diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index f277265..3c1c355 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -1,6 +1,4 @@ -using System.Collections.ObjectModel; using System.Net; -using Foxnouns.Backend.Middleware; using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -51,7 +49,7 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo { { "status", (int)HttpStatusCode.BadRequest }, { "message", Message }, - { "code", ErrorCode.BadRequest.ToString() } + { "code", "BAD_REQUEST" } }; if (errors == null) return o; @@ -84,7 +82,7 @@ public class ApiError(string message, HttpStatusCode? statusCode = null, ErrorCo { { "status", (int)HttpStatusCode.BadRequest }, { "message", Message }, - { "code", ErrorCode.BadRequest.ToString() } + { "code", "BAD_REQUEST" } }; if (modelState == null) return o; diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 92abc6a..6c4ea28 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -10,6 +10,7 @@ + diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index c52c3f0..6b6da6d 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,7 +1,6 @@ using System.Net; using Foxnouns.Backend.Utils; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; namespace Foxnouns.Backend.Middleware; diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/AuthService.cs index f69441a..f1e907b 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/AuthService.cs @@ -74,6 +74,7 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s /// /// 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 diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 33b595f..78ea3ae 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -1,6 +1,5 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using NodaTime; diff --git a/Foxnouns.Backend/Services/RemoteAuthService.cs b/Foxnouns.Backend/Services/RemoteAuthService.cs index b08b0a5..fefaf16 100644 --- a/Foxnouns.Backend/Services/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/RemoteAuthService.cs @@ -1,5 +1,5 @@ using System.Diagnostics.CodeAnalysis; -using System.Web; +using JetBrains.Annotations; namespace Foxnouns.Backend.Services; @@ -27,10 +27,11 @@ public class RemoteAuthService(Config config, ILogger logger) if (!resp.IsSuccessStatusCode) { var respBody = await resp.Content.ReadAsStringAsync(ct); - _logger.Error("Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", (int)resp.StatusCode, respBody); + _logger.Error("Received error status {StatusCode} when exchanging OAuth token: {ErrorBody}", + (int)resp.StatusCode, respBody); throw new FoxnounsError("Invalid Discord OAuth response"); } - + resp.EnsureSuccessStatusCode(); var token = await resp.Content.ReadFromJsonAsync(ct); if (token == null) throw new FoxnounsError("Discord token response was null"); @@ -46,10 +47,14 @@ public class RemoteAuthService(Config config, ILogger logger) return new RemoteUser(user.id, user.username); } - [SuppressMessage("ReSharper", "InconsistentNaming")] + [SuppressMessage("ReSharper", "InconsistentNaming", + Justification = "Easier to use snake_case here, rather than passing in JSON converter options")] + [UsedImplicitly] private record DiscordTokenResponse(string access_token, string token_type); - [SuppressMessage("ReSharper", "InconsistentNaming")] + [SuppressMessage("ReSharper", "InconsistentNaming", + Justification = "Easier to use snake_case here, rather than passing in JSON converter options")] + [UsedImplicitly] private record DiscordUserResponse(string id, string username); public record RemoteUser(string Id, string Username); From 821712f43b2cffaa081ebd5108a52221240eb9a5 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Sep 2024 16:45:33 +0200 Subject: [PATCH 045/261] fix(backend): use packages.lock file when restoring --- Foxnouns.Backend/Foxnouns.Backend.csproj | 1 + Foxnouns.Backend/packages.lock.json | 1532 ++++++++++++++++++++++ 2 files changed, 1533 insertions(+) create mode 100644 Foxnouns.Backend/packages.lock.json diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 6c4ea28..8a48573 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -3,6 +3,7 @@ net8.0 enable enable + true diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json new file mode 100644 index 0000000..a704380 --- /dev/null +++ b/Foxnouns.Backend/packages.lock.json @@ -0,0 +1,1532 @@ +{ + "version": 1, + "dependencies": { + "net8.0": { + "Coravel": { + "type": "Direct", + "requested": "[5.0.4, )", + "resolved": "5.0.4", + "contentHash": "Bp3G5tmJgTpTYAYB86OljIbNZb2qt5iByWLUMYUZGA/hccoavKg2SWl5gs29WcrlHn96DVA+O+9NUEjVq3nUng==", + "dependencies": { + "Microsoft.Extensions.Caching.Memory": "3.1.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": "[5.0.1, )", + "resolved": "5.0.1", + "contentHash": "tU6CXDqZRZZGNM8yLPoz91OnvEQaougEqEPp0ZKjR5xa9bLUOujlX3xU2CfxVbb3kS+nMjY2Y3vlOgmbO6qGFA==", + "dependencies": { + "MailKit": "2.5.1", + "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.0" + } + }, + "EFCore.NamingConventions": { + "type": "Direct", + "requested": "[8.0.3, )", + "resolved": "8.0.3", + "contentHash": "TdDarM6kyIS2oVIhrs3W+r+xL/76ooFJxIXxfhzsNJQu0pB9VdFZwuyKvKJnhoc7OHYFNTBP08AN37kr4CPc+Q==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "[8.0.0, 9.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[8.0.0, 9.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "EntityFrameworkCore.Exceptions.PostgreSQL": { + "type": "Direct", + "requested": "[8.1.2, )", + "resolved": "8.1.2", + "contentHash": "SIIHSTcfN04sCY5YMC3azD3lTvGLIT+VjO5/8zaqSqxzAvAaJ9x7ZVr5M6j8ORAxeezIe9+X/XRRZ+mRNbM89w==", + "dependencies": { + "EntityFrameworkCore.Exceptions.Common": "8.1.2", + "Npgsql": "8.0.1" + } + }, + "JetBrains.Annotations": { + "type": "Direct", + "requested": "[2024.2.0, )", + "resolved": "2024.2.0", + "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" + }, + "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { + "type": "Direct", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "Y8v0z0Zrc28wKxY98sGBwjnHqXeSDFh3VQ5ZEsIxGHD/1g9z2zulDJ8ue6Vww1F4gcUkY9XCQUHljNQu3my35A==", + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "8.0.7", + "Newtonsoft.Json": "13.0.3", + "Newtonsoft.Json.Bson": "1.0.2" + } + }, + "Microsoft.AspNetCore.OpenApi": { + "type": "Direct", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "9SBDNvlwA88r5oD7yUbTmwr9ylkmZWdPQgohBWCdz6cESDAo6JgCD5vEOZS/nq2WIL5SCn3/RamAStcdiRzd4g==", + "dependencies": { + "Microsoft.OpenApi": "1.4.3" + } + }, + "Microsoft.EntityFrameworkCore": { + "type": "Direct", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "UOyPNAgyzw/E4hUCurqvZxi0WWVLQAGZuntFPzkTXtvJLTqRjKvokvhv+XazAUSODLsU1DZ67GjZ4mT9d82+0g==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Abstractions": "8.0.7", + "Microsoft.EntityFrameworkCore.Analyzers": "8.0.7", + "Microsoft.Extensions.Caching.Memory": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0" + } + }, + "Microsoft.EntityFrameworkCore.Design": { + "type": "Direct", + "requested": "[8.0.7, )", + "resolved": "8.0.7", + "contentHash": "EUPY49Hi5BbpnkiX9ik/2fD9GPEbvKx6wvDmDNZTHZGlXAg1kcR9vt2QA2af1mIoa7gG1wqEvyQRWf9/A8gWqQ==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.5.0", + "Microsoft.EntityFrameworkCore.Relational": "8.0.7", + "Microsoft.Extensions.DependencyModel": "8.0.1", + "Mono.TextTemplating": "2.2.1" + } + }, + "Minio": { + "type": "Direct", + "requested": "[6.0.3, )", + "resolved": "6.0.3", + "contentHash": "WHlkouclHtiK/pIXPHcjVmbeELHPtElj2qRSopFVpSmsFhZXeM10sPvczrkSPePsmwuvZdFryJ/hJzKu3XeLVg==", + "dependencies": { + "CommunityToolkit.HighPerformance": "8.2.2", + "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.1.11, )", + "resolved": "3.1.11", + "contentHash": "AYSiCHp1PLzWKVf7hEL3MJ0q9kzOWMNIaTVysXk4XKrDBzK5PF2wpd4LsAl+EIQ2Hbvu+vw4oFaexcXzCuY1lQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "4.7.1" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL": { + "type": "Direct", + "requested": "[8.0.4, )", + "resolved": "8.0.4", + "contentHash": "/hHd9MqTRVDgIpsToCcxMDxZqla0HAQACiITkq1+L9J2hmHKV6lBAPlauF+dlNSfHpus7rrljWx4nAanKD6qAw==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "8.0.4", + "Microsoft.EntityFrameworkCore.Abstractions": "8.0.4", + "Microsoft.EntityFrameworkCore.Relational": "8.0.4", + "Npgsql": "8.0.3" + } + }, + "Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": { + "type": "Direct", + "requested": "[8.0.4, )", + "resolved": "8.0.4", + "contentHash": "IJqmyj4BkqbCAm1MDZEwaUuxzYVbhtqghfkP2B9u089uCQgOtdcGbJYQwN2dyxO1ze16VDhTLUZwiq7Us1jdvg==", + "dependencies": { + "Npgsql.EntityFrameworkCore.PostgreSQL": "8.0.4", + "Npgsql.NodaTime": "8.0.3" + } + }, + "Npgsql.Json.NET": { + "type": "Direct", + "requested": "[8.0.3, )", + "resolved": "8.0.3", + "contentHash": "5YqEP+RDlbP4ecQ2ndw7+NeiLOD2PxnJE5JnyBendn5ipuOGD6iVt+rS7pxyPKRyNAmrRRT3iOJrUPUDRW/Ncw==", + "dependencies": { + "Newtonsoft.Json": "13.0.3", + "Npgsql": "8.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" + } + }, + "Sentry.AspNetCore": { + "type": "Direct", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "927iNu4O4f+q8mVXNmekRHil0KIZGhFH7w8TJ4wHXxrZiP0z7Q21yWfYamyWO8z2rXFwm4G7WlSaEjU6RIZ3Uw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Sentry.Extensions.Logging": "4.9.0" + } + }, + "Serilog": { + "type": "Direct", + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "pzeDRXdpSLSsgBHpZcmpIDxqMy845Ab4s+dfnBg0sN9h8q/4Wo3vAoe0QCGPze1Q06EVtEPupS+UvLm8iXQmTQ==" + }, + "Serilog.AspNetCore": { + "type": "Direct", + "requested": "[8.0.1, )", + "resolved": "8.0.1", + "contentHash": "B/X+wAfS7yWLVOTD83B+Ip9yl4MkhioaXj90JSoWi1Ayi8XHepEnsBdrkojg08eodCnmOKmShFUN2GgEc6c0CQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Serilog": "3.1.1", + "Serilog.Extensions.Hosting": "8.0.0", + "Serilog.Extensions.Logging": "8.0.0", + "Serilog.Formatting.Compact": "2.0.0", + "Serilog.Settings.Configuration": "8.0.0", + "Serilog.Sinks.Console": "5.0.0", + "Serilog.Sinks.Debug": "2.0.0", + "Serilog.Sinks.File": "5.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": "[8.0.0, )", + "resolved": "8.0.0", + "contentHash": "z5ig56/qzjkX6Fj4U/9m1g8HQaQiYPMZS4Uevtjg1I+WWzoGSf5t/E+6JbMP/jbZYhU63bA5NJN5y0x+qqx2Bw==", + "dependencies": { + "Serilog": "4.0.0", + "Serilog.Sinks.File": "5.0.0" + } + }, + "SixLabors.ImageSharp": { + "type": "Direct", + "requested": "[3.1.5, )", + "resolved": "3.1.5", + "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" + }, + "Swashbuckle.AspNetCore": { + "type": "Direct", + "requested": "[6.6.2, )", + "resolved": "6.6.2", + "contentHash": "+NB4UYVYN6AhDSjW0IJAd1AGD8V33gemFNLPaxKTtPkHB+HaKAKf9MGAEUPivEWvqeQfcKIw8lJaHq6LHljRuw==", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "6.0.5", + "Swashbuckle.AspNetCore.Swagger": "6.6.2", + "Swashbuckle.AspNetCore.SwaggerGen": "6.6.2", + "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" + } + }, + "CommunityToolkit.HighPerformance": { + "type": "Transitive", + "resolved": "8.2.2", + "contentHash": "+zIp8d3sbtYaRbM6hqDs4Ui/z34j7DcUmleruZlYLE4CVxXq+MO8XJyIs42vzeTYFX+k0Iq1dEbBUnQ4z/Gnrw==" + }, + "EntityFrameworkCore.Exceptions.Common": { + "type": "Transitive", + "resolved": "8.1.2", + "contentHash": "Yy1qw+mdXhHyptH42o2suEaNDZlcmwiaQZ56v8tUVUxUq33GQSYyTJ6wE2WuB2AjunTa4tPhieu2E+m6z/GcTg==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Relational": "8.0.0" + } + }, + "Humanizer.Core": { + "type": "Transitive", + "resolved": "2.14.1", + "contentHash": "lQKvtaTDOXnoVJ20ibTuSIOf2i0uO0MPbDhd1jm238I+U/2ZnRENj0cktKZhtchBMtCUSRQ5v4xBCUbKNmyVMw==" + }, + "MailKit": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "0sLYQsWszoqdQorVnfCMsRswBYwodpO03fS5Ij1m7Qx6B02IlgXYLwsLqIJHkvUOoEOazrwxIerIZwujV1/Mbw==", + "dependencies": { + "MimeKit": "2.5.1", + "System.Net.NameResolution": "4.3.0", + "System.Net.Security": "4.3.2", + "System.Runtime.Serialization.Primitives": "4.3.0" + } + }, + "Microsoft.AspNetCore.JsonPatch": { + "type": "Transitive", + "resolved": "8.0.7", + "contentHash": "/cNcH8Qd+3yH4yVUlOsZnxPcsmwnYSnkybKX7PMgrJkaH7ilp2IrcqJN4M376As+y1f+MVUyRqRtosB+PWWQbg==", + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "Newtonsoft.Json": "13.0.3" + } + }, + "Microsoft.AspNetCore.Mvc.Razor.Extensions": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "M0h+ChPgydX2xY17agiphnAVa/Qh05RAP8eeuqGGhQKT10claRBlLNO6d2/oSV8zy0RLHzwLnNZm5xuC/gckGA==", + "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "6.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.0" + } + }, + "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bf4kbla/8Qiu53MgFPz1u3H1ThoookPpFy+Ya9Q9p531wXK1pZ3tfz/Gtx8SKy41yz99jhZHTUM1QqLl7eJRgQ==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.0", + "Microsoft.CodeAnalysis.Razor": "6.0.0", + "Microsoft.Extensions.DependencyModel": "6.0.0" + } + }, + "Microsoft.AspNetCore.Razor.Language": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "yCtBr1GSGzJrrp1NJUb4ltwFYMKHw/tJLnIDvg9g/FnkGIEzmE19tbCQqXARIJv5kdtBgsoVIdGLL+zmjxvM/A==" + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + }, + "Microsoft.CodeAnalysis.Analyzers": { + "type": "Transitive", + "resolved": "3.3.3", + "contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ==" + }, + "Microsoft.CodeAnalysis.Common": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "lwAbIZNdnY0SUNoDmZHkVUwLO8UyNnyyh1t/4XsbFxi4Ounb3xszIYZaWhyj5ZjyfcwqwmtMbE7fUTVCqQEIdQ==", + "dependencies": { + "Microsoft.CodeAnalysis.Analyzers": "3.3.3", + "System.Collections.Immutable": "6.0.0", + "System.Reflection.Metadata": "6.0.1", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Text.Encoding.CodePages": "6.0.0" + } + }, + "Microsoft.CodeAnalysis.CSharp": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "cM59oMKAOxvdv76bdmaKPy5hfj+oR+zxikWoueEB7CwTko7mt9sVKZI8Qxlov0C/LuKEG+WQwifepqL3vuTiBQ==", + "dependencies": { + "Microsoft.CodeAnalysis.Common": "[4.5.0]" + } + }, + "Microsoft.CodeAnalysis.CSharp.Workspaces": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "h74wTpmGOp4yS4hj+EvNzEiPgg/KVs2wmSfTZ81upJZOtPkJsVkgfsgtxxqmAeapjT/vLKfmYV0bS8n5MNVP+g==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.CodeAnalysis.CSharp": "[4.5.0]", + "Microsoft.CodeAnalysis.Common": "[4.5.0]", + "Microsoft.CodeAnalysis.Workspaces.Common": "[4.5.0]" + } + }, + "Microsoft.CodeAnalysis.Razor": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "uqdzuQXxD7XrJCbIbbwpI/LOv0PBJ9VIR0gdvANTHOfK5pjTaCir+XcwvYvBZ5BIzd0KGzyiamzlEWw1cK1q0w==", + "dependencies": { + "Microsoft.AspNetCore.Razor.Language": "6.0.0", + "Microsoft.CodeAnalysis.CSharp": "4.0.0", + "Microsoft.CodeAnalysis.Common": "4.0.0" + } + }, + "Microsoft.CodeAnalysis.Workspaces.Common": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "l4dDRmGELXG72XZaonnOeORyD/T5RpEu5LGHOUIhnv+MmUWDY/m1kWXGwtcgQ5CJ5ynkFiRnIYzTKXYjUs7rbw==", + "dependencies": { + "Humanizer.Core": "2.14.1", + "Microsoft.Bcl.AsyncInterfaces": "6.0.0", + "Microsoft.CodeAnalysis.Common": "[4.5.0]", + "System.Composition": "6.0.0", + "System.IO.Pipelines": "6.0.3", + "System.Threading.Channels": "6.0.0" + } + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "8.0.7", + "contentHash": "DHX6nxcg4/tpWfTjAleKrXveDiNFY/OGOK6nm27GipUXNI2Uofev9cH5SYXmtGIgHWxlvfn754TXN4WnrixOwg==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "8.0.7", + "contentHash": "nerD0vEOYJVhVapamRVH9DrUYbDNMJ5bPfWze4SibDDaDaekzgwQqBht97/tV+8pgdKoPAXmtiJsB+lDajwVrQ==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "8.0.7", + "contentHash": "Hn86yScnW+VXb+A2LGrVGkGmjsQ9KLWR0T8GQBEcESWk8u9JYhBiRtdxz76Aq0ir82Ei48sLEZTN4VE0sJ3yIg==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "8.0.7", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "6.0.5", + "contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "8.0.1", + "contentHash": "5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==", + "dependencies": { + "System.Text.Encodings.Web": "8.0.0", + "System.Text.Json": "8.0.4" + } + }, + "Microsoft.Extensions.Diagnostics": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.Diagnostics.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "System.Diagnostics.DiagnosticSource": "8.0.0" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Diagnostics": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + } + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", + "dependencies": { + "Microsoft.Extensions.Configuration": "8.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "udvKco0sAVgYGTBnHUb0tY9JQzJ/nPDiv/8PIyz69wl1AibeCDZOLVVI+6156dPfHmJH7ws5oUJRiW4ZmAvuuA==" + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Primitives": "8.0.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.OpenApi": { + "type": "Transitive", + "resolved": "1.6.14", + "contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==" + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "MimeKit": { + "type": "Transitive", + "resolved": "2.5.1", + "contentHash": "KcySIws+ZT1ubER/Z09T9FQ2fsliqFORuLayKCr3mj0Sk4mqXKMvuX2B/W7FHMrLYocYrnU6ngvwVExxwxFrmA==", + "dependencies": { + "Portable.BouncyCastle": "1.8.5", + "System.Data.Common": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Text.Encoding.CodePages": "4.3.0" + } + }, + "Mono.TextTemplating": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==", + "dependencies": { + "System.CodeDom": "4.4.0" + } + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.1" + } + }, + "Npgsql": { + "type": "Transitive", + "resolved": "8.0.3", + "contentHash": "6WEmzsQJCZAlUG1pThKg/RmeF6V+I0DmBBBE/8YzpRtEzhyZzKcK7ulMANDm5CkxrALBEC8H+5plxHWtIL7xnA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + } + }, + "Npgsql.NodaTime": { + "type": "Transitive", + "resolved": "8.0.3", + "contentHash": "C0TzQcc4+/6jpRGb/YxphmCwRwSuWde9Yz9GWap1xfX2m6/QkYBhluv/EjsPCTWq3/UZaP9UgiUZKscTLNZt4g==", + "dependencies": { + "NodaTime": "3.1.9", + "Npgsql": "8.0.3" + } + }, + "Portable.BouncyCastle": { + "type": "Transitive", + "resolved": "1.8.5", + "contentHash": "EaCgmntbH1sOzemRTqyXSqYjB6pLH7VCYHhhDYZ59guHSD5qPwhIYa7kfy0QUlmTRt9IXhaXdFhNuBUArp70Ng==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Security": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "M2nN92ePS8BgQ2oi6Jj3PlTUzadYSIWLdZrHY1n1ZcW9o4wAQQ6W+aQ2lfq1ysZQfVCgDwY58alUdowrzezztg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" + }, + "Sentry": { + "type": "Transitive", + "resolved": "4.9.0", + "contentHash": "dvafAJjs1nyv/sXPjHjMiIq/XN1jjRCvTvn6BDdkeavjZXi0eX6JMpAKwGU5C8IlHY6Xhh13nk2u5e8AHQwwrw==" + }, + "Sentry.Extensions.Logging": { + "type": "Transitive", + "resolved": "4.9.0", + "contentHash": "P+9y+rxE5YPHw1sWWMUcSDF5tHfm+vP9yxJiALE85RXMkzT/H7tp7Pstclbnht38KiPsC3sFUtMBdYFdiMvHKg==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.Http": "8.0.0", + "Microsoft.Extensions.Logging.Configuration": "8.0.0", + "Sentry": "4.9.0" + } + }, + "Serilog.Extensions.Hosting": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "db0OcbWeSCvYQkHWu6n0v40N4kKaTAXNjlM3BKvcbwvNzYphQFcBR+36eQ/7hMMwOkJvAyLC2a9/jNdUL5NjtQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Serilog": "3.1.1", + "Serilog.Extensions.Logging": "8.0.0" + } + }, + "Serilog.Extensions.Logging": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==", + "dependencies": { + "Microsoft.Extensions.Logging": "8.0.0", + "Serilog": "3.1.1" + } + }, + "Serilog.Formatting.Compact": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "ob6z3ikzFM3D1xalhFuBIK1IOWf+XrQq+H4KeH4VqBcPpNcmUgZlRQ2h3Q7wvthpdZBBoY86qZOI2LCXNaLlNA==", + "dependencies": { + "Serilog": "3.1.0" + } + }, + "Serilog.Settings.Configuration": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "nR0iL5HwKj5v6ULo3/zpP8NMcq9E2pxYA6XKTSWCbugVs4YqPyvaqaKOY+OMpPivKp7zMEpax2UKHnDodbRB0Q==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "8.0.0", + "Microsoft.Extensions.DependencyModel": "8.0.0", + "Serilog": "3.1.1" + } + }, + "Serilog.Sinks.Debug": { + "type": "Transitive", + "resolved": "2.0.0", + "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Serilog.Sinks.File": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "dependencies": { + "Serilog": "2.10.0" + } + }, + "Swashbuckle.AspNetCore.Swagger": { + "type": "Transitive", + "resolved": "6.6.2", + "contentHash": "ovgPTSYX83UrQUWiS5vzDcJ8TEX1MAxBgDFMK45rC24MorHEPQlZAHlaXj/yth4Zf6xcktpUgTEBvffRQVwDKA==", + "dependencies": { + "Microsoft.OpenApi": "1.6.14" + } + }, + "Swashbuckle.AspNetCore.SwaggerGen": { + "type": "Transitive", + "resolved": "6.6.2", + "contentHash": "zv4ikn4AT1VYuOsDCpktLq4QDq08e7Utzbir86M5/ZkRaLXbCPF11E1/vTmOiDzRTl0zTZINQU2qLKwTcHgfrA==", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "6.6.2" + } + }, + "Swashbuckle.AspNetCore.SwaggerUI": { + "type": "Transitive", + "resolved": "6.6.2", + "contentHash": "mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==" + }, + "System.CodeDom": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "2sCCb7doXEwtYAbqzbF/8UAeDRMNmPaQbU2q50Psg1J9KzumyVVCgKQY8s53WIPTufNT0DpSe9QRvVjOzfDWBA==" + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Composition": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "d7wMuKQtfsxUa7S13tITC8n1cQzewuhD5iDjZtK2prwFfKVzdYtgrTHgjaV03Zq7feGQ5gkP85tJJntXwInsJA==", + "dependencies": { + "System.Composition.AttributedModel": "6.0.0", + "System.Composition.Convention": "6.0.0", + "System.Composition.Hosting": "6.0.0", + "System.Composition.Runtime": "6.0.0", + "System.Composition.TypedParts": "6.0.0" + } + }, + "System.Composition.AttributedModel": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "WK1nSDLByK/4VoC7fkNiFuTVEiperuCN/Hyn+VN30R+W2ijO1d0Z2Qm0ScEl9xkSn1G2MyapJi8xpf4R8WRa/w==" + }, + "System.Composition.Convention": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "XYi4lPRdu5bM4JVJ3/UIHAiG6V6lWWUlkhB9ab4IOq0FrRsp0F4wTyV4Dj+Ds+efoXJ3qbLqlvaUozDO7OLeXA==", + "dependencies": { + "System.Composition.AttributedModel": "6.0.0" + } + }, + "System.Composition.Hosting": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "w/wXjj7kvxuHPLdzZ0PAUt++qJl03t7lENmb2Oev0n3zbxyNULbWBlnd5J5WUMMv15kg5o+/TCZFb6lSwfaUUQ==", + "dependencies": { + "System.Composition.Runtime": "6.0.0" + } + }, + "System.Composition.Runtime": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "qkRH/YBaMPTnzxrS5RDk1juvqed4A6HOD/CwRcDGyPpYps1J27waBddiiq1y93jk2ZZ9wuA/kynM+NO0kb3PKg==" + }, + "System.Composition.TypedParts": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "iUR1eHrL8Cwd82neQCJ00MpwNIBs4NZgXzrPqx8NJf/k4+mwBO0XCRmHYJT4OLSwDDqh5nBLJWkz5cROnrGhRA==", + "dependencies": { + "System.Composition.AttributedModel": "6.0.0", + "System.Composition.Hosting": "6.0.0", + "System.Composition.Runtime": "6.0.0" + } + }, + "System.Data.Common": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lm6E3T5u7BOuEH0u18JpbJHxBfOJPuCyl4Kg1RH10ktYLp5uEEE1xKrHW56/We4SnZpGAuCc9N0MJpSDhTHZGQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.IO.Hashing": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "ne1843evDugl0md7Fjzy6QjJrzsjh46ZKbhf8GwBXb5f/gw97J4bxMs0NQKifDuThh/f0bZ0e62NPl1jzTuRqA==" + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "6.0.3", + "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==" + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Net.NameResolution": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "AFYl08R7MrsrEjqpQWTZWBadqXyTzNDaWpMqyxhb0d6sGhV6xMDKueuBXlLL30gz+DIRY6MpdgnHWlCh5wmq9w==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Principal.Windows": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Security": { + "type": "Transitive", + "resolved": "4.3.2", + "contentHash": "xT2jbYpbBo3ha87rViHoTA6WdvqOAW37drmqyx/6LD8p7HEPT2qgdxoimRzWtPg8Jh4X5G9BV2seeTv4x6FYlA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Claims": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Security.Principal": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.ThreadPool": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Security": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" + } + }, + "System.Reactive": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Metadata": { + "type": "Transitive", + "resolved": "6.0.1", + "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", + "dependencies": { + "System.Collections.Immutable": "6.0.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.CompilerServices.Unsafe": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", + "dependencies": { + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Security.Claims": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "P/+BR/2lnc4PNDHt/TPBAWHVMLMRHsyYZbU1NphW4HIWzCggz8mJbTQQ3MKljFE7LS3WagmVFuBgoLcFzYXlkA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Security.Principal": "4.3.0" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I1tkfQlAoMM2URscUtpcRo/hX0jinXx6a/KUtEQoz3owaYwl3qwsO8cbzYVVnjxrzxjHo3nJC+62uolgeGIS9A==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Security.Principal.Windows": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HVL1rvqYtnRCxFsYag/2le/ZfKLK4yMw79+s6FmKXbSCNN0JeAhrYxnRAHFoWRa0dEojsDcbBSpH3l22QxAVyw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Claims": "4.3.0", + "System.Security.Principal": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.CodePages": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "8.0.0", + "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "8.0.4", + "contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==", + "dependencies": { + "System.Text.Encodings.Web": "8.0.0" + } + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Channels": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.ThreadPool": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "k/+g4b7vjdd4aix83sTgC9VG6oXYKAktSfNIJUNGxPEj7ryEOfzHHhfnmsZvjxawwcD9HyWXKCXmPjX8U4zeSw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + } + } + } +} \ No newline at end of file From cf2f624ae429c93d4fb60e9231ead4c2de49c5db Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Sep 2024 18:07:49 +0200 Subject: [PATCH 046/261] feat: add docker configuration --- .dockerignore | 23 ++++++ DOCKER.md | 8 +++ Dockerfile.backend | 22 ++++++ Foxnouns.Backend/Foxnouns.Backend.csproj | 7 ++ Foxnouns.Frontend/Dockerfile | 12 ++++ .../app/components/ErrorAlert.tsx | 2 +- .../app/components/nav/Navbar.tsx | 4 +- Foxnouns.Frontend/app/env.server.ts | 1 + .../routes/auth.callback.discord/route.tsx | 4 +- .../app/routes/auth.log-in/route.tsx | 6 +- Foxnouns.Frontend/package.json | 1 + Foxnouns.Frontend/yarn.lock | 2 +- docker-compose.yml | 71 +++++++++++++++++++ docker/Caddyfile | 4 ++ docker/config.example.ini | 48 +++++++++++++ docker/frontend.env | 0 docker/proxy-config.example.json | 6 ++ rate/Dockerfile | 16 +++++ go.mod => rate/go.mod | 0 go.sum => rate/go.sum | 0 rate/main.go | 8 +++ 21 files changed, 232 insertions(+), 13 deletions(-) create mode 100644 .dockerignore create mode 100644 DOCKER.md create mode 100644 Dockerfile.backend create mode 100644 Foxnouns.Frontend/Dockerfile create mode 100644 docker-compose.yml create mode 100644 docker/Caddyfile create mode 100644 docker/config.example.ini create mode 100644 docker/frontend.env create mode 100644 docker/proxy-config.example.json create mode 100644 rate/Dockerfile rename go.mod => rate/go.mod (100%) rename go.sum => rate/go.sum (100%) diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f90ce74 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +**/.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 \ No newline at end of file diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..a4f8c3a --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,8 @@ +# Running with Docker + +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. Build with `docker compose build` +4. Run with `docker compose up` + +The Caddy server will listen on `localhost:5004`. diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..b7dd859 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 5000 + +FROM mcr.microsoft.com/dotnet/sdk:8.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/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 8a48573..76bf470 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -4,6 +4,7 @@ enable enable true + Linux @@ -44,4 +45,10 @@ + + + + .dockerignore + + diff --git a/Foxnouns.Frontend/Dockerfile b/Foxnouns.Frontend/Dockerfile new file mode 100644 index 0000000..4150c99 --- /dev/null +++ b/Foxnouns.Frontend/Dockerfile @@ -0,0 +1,12 @@ +FROM docker.io/node:22 + +RUN mkdir -p /app/node_modules && chown -R node:node /app +WORKDIR /app +COPY package.json yarn.lock ./ +USER node +RUN yarn +COPY --chown=node:node . . + +RUN yarn build + +CMD ["yarn", "start"] diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx index cedb69a..d66b6a1 100644 --- a/Foxnouns.Frontend/app/components/ErrorAlert.tsx +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -1,5 +1,5 @@ import { TFunction } from "i18next"; -import Alert from "react-bootstrap/Alert"; +import { Alert } from "react-bootstrap"; import { Trans, useTranslation } from "react-i18next"; import { ApiError, diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx index dbccac7..7e311f2 100644 --- a/Foxnouns.Frontend/app/components/nav/Navbar.tsx +++ b/Foxnouns.Frontend/app/components/nav/Navbar.tsx @@ -3,9 +3,7 @@ import Meta from "~/lib/api/meta"; import { User, UserSettings } from "~/lib/api/user"; import Logo from "./Logo"; -import Nav from "react-bootstrap/Nav"; -import Navbar from "react-bootstrap/Navbar"; -import NavDropdown from "react-bootstrap/NavDropdown"; +import { Nav, Navbar, NavDropdown } from "react-bootstrap"; import { BrightnessHigh, BrightnessHighFill, MoonFill } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts index 882d393..2add747 100644 --- a/Foxnouns.Frontend/app/env.server.ts +++ b/Foxnouns.Frontend/app/env.server.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { env } from "node:process"; export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index 31d4d64..bd01eef 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -10,10 +10,8 @@ import { ShouldRevalidateFunction, } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; -import Form from "react-bootstrap/Form"; -import Button from "react-bootstrap/Button"; +import { Form, Button, Alert } from "react-bootstrap"; import ErrorAlert from "~/components/ErrorAlert"; -import Alert from "react-bootstrap/Alert"; export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { return !actionResult; diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index 1f55fda..adc4ce9 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -6,11 +6,7 @@ import { ActionFunctionArgs, } from "@remix-run/node"; import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react"; -import Form from "react-bootstrap/Form"; -import Button from "react-bootstrap/Button"; -import ButtonGroup from "react-bootstrap/ButtonGroup"; -import ListGroup from "react-bootstrap/ListGroup"; -import { Row, Col } from "react-bootstrap"; +import { Form, Button, ButtonGroup, ListGroup, Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 0b1aff2..cc0c993 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -22,6 +22,7 @@ "compression": "^1.7.4", "cookie": "^0.6.0", "cross-env": "^7.0.3", + "dotenv": "^16.4.5", "express": "^4.19.2", "i18next": "^23.15.1", "i18next-browser-languagedetector": "^8.0.0", diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index f4b109e..1d7c038 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -2554,7 +2554,7 @@ domutils@^3.0.1, domutils@^3.1.0: domelementtype "^2.3.0" domhandler "^5.0.3" -dotenv@^16.0.0: +dotenv@^16.0.0, dotenv@^16.4.5: version "16.4.5" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7176fc2 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,71 @@ +services: + backend: + image: backend + build: + context: . + dockerfile: ./Dockerfile.backend + environment: + - "Database:Url=Host=pgbouncer;Database=postgres;Username=postgres;Password=postgres" + - "Database:EnablePooling=false" + - "Host=0.0.0.0" + - "Port=5000" + restart: unless-stopped + volumes: + - ./docker/config.ini:/app/config.ini + + frontend: + image: frontend + build: ./Foxnouns.Frontend + environment: + - "API_BASE=http://rate:5003/api" + restart: unless-stopped + volumes: + - ./docker/frontend.env:/app/.env + + rate: + image: rate + build: ./rate + environment: + - "PORT=5003" + restart: unless-stopped + volumes: + - ./docker/proxy-config.json:/app/proxy-config.json + + pgbouncer: + image: docker.io/edoburu/pgbouncer:latest + environment: + - "DATABASE_URL=postgres://postgres:postgres@postgres/postgres" + - "AUTH_TYPE=scram-sha-256" + - "MAX_CLIENT_CONN=100" + - "DEFAULT_POOL_SIZE=100" + - "MIN_POOL_SIZE=10" + restart: unless-stopped + + 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 + + caddy: + image: docker.io/caddy:2 + restart: unless-stopped + ports: + - "5004:80" + volumes: + - ./docker/Caddyfile:/etc/caddy/Caddyfile + - caddy_data:/data + - caddy_config:/config + +volumes: + caddy_data: + caddy_config: + postgres_data: diff --git a/docker/Caddyfile b/docker/Caddyfile new file mode 100644 index 0000000..6e12647 --- /dev/null +++ b/docker/Caddyfile @@ -0,0 +1,4 @@ +http:// { + reverse_proxy /api/* http://rate:5003 + reverse_proxy http://frontend:3000 +} \ No newline at end of file diff --git a/docker/config.example.ini b/docker/config.example.ini new file mode 100644 index 0000000..ba32449 --- /dev/null +++ b/docker/config.example.ini @@ -0,0 +1,48 @@ +;; 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.env b/docker/frontend.env new file mode 100644 index 0000000..e69de29 diff --git a/docker/proxy-config.example.json b/docker/proxy-config.example.json new file mode 100644 index 0000000..278249b --- /dev/null +++ b/docker/proxy-config.example.json @@ -0,0 +1,6 @@ +{ + "port": 5003, + "proxy_target": "http://localhost:5000", + "debug": true, + "powered_by": "5 gay rats" +} diff --git a/rate/Dockerfile b/rate/Dockerfile new file mode 100644 index 0000000..b88d028 --- /dev/null +++ b/rate/Dockerfile @@ -0,0 +1,16 @@ +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/go.mod b/rate/go.mod similarity index 100% rename from go.mod rename to rate/go.mod diff --git a/go.sum b/rate/go.sum similarity index 100% rename from go.sum rename to rate/go.sum diff --git a/rate/main.go b/rate/main.go index 33cccfd..6901085 100644 --- a/rate/main.go +++ b/rate/main.go @@ -23,6 +23,14 @@ func main() { 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) From df09a2add8a857987511d8ccb6dfcbbb4c2af5cf Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Sep 2024 18:11:29 +0200 Subject: [PATCH 047/261] fix(config): use correct target in example proxy config --- docker/proxy-config.example.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/proxy-config.example.json b/docker/proxy-config.example.json index 278249b..dbf9482 100644 --- a/docker/proxy-config.example.json +++ b/docker/proxy-config.example.json @@ -1,6 +1,6 @@ { "port": 5003, - "proxy_target": "http://localhost:5000", + "proxy_target": "http://backend:5000", "debug": true, "powered_by": "5 gay rats" } From 6acd9b94f4db453596758e1e53bc043fbab90e76 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Sep 2024 23:24:23 +0200 Subject: [PATCH 048/261] fix(backend): reference System.Text.RegularExpressions directly to avoid CVE --- Foxnouns.Backend/Foxnouns.Backend.csproj | 1 + Foxnouns.Backend/packages.lock.json | 33 ++++++++++++------------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 76bf470..8207394 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -35,6 +35,7 @@ + diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index a704380..90ba53c 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -246,6 +246,15 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" } }, + "System.Text.RegularExpressions": { + "type": "Direct", + "requested": "[4.3.1, )", + "resolved": "4.3.1", + "contentHash": "N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==", + "dependencies": { + "System.Runtime": "4.3.1" + } + }, "CommunityToolkit.HighPerformance": { "type": "Transitive", "resolved": "8.2.2", @@ -587,13 +596,13 @@ }, "Microsoft.NETCore.Platforms": { "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + "resolved": "1.1.1", + "contentHash": "TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==" }, "Microsoft.NETCore.Targets": { "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + "resolved": "1.1.3", + "contentHash": "3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==" }, "Microsoft.OpenApi": { "type": "Transitive", @@ -1203,11 +1212,11 @@ }, "System.Runtime": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "resolved": "4.3.1", + "contentHash": "abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" + "Microsoft.NETCore.Platforms": "1.1.1", + "Microsoft.NETCore.Targets": "1.1.3" } }, "System.Runtime.CompilerServices.Unsafe": { @@ -1486,14 +1495,6 @@ "System.Text.Encodings.Web": "8.0.0" } }, - "System.Text.RegularExpressions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, "System.Threading": { "type": "Transitive", "resolved": "4.3.0", From 0f51f01b34c8a62db2caa4156f4d374299396aaa Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 15 Sep 2024 00:03:15 +0200 Subject: [PATCH 049/261] feat(frontend): start welcome page --- .../routes/auth.callback.discord/route.tsx | 23 +++++++- .../app/routes/auth.log-in/route.tsx | 11 +++- .../app/routes/auth.welcome/route.tsx | 52 +++++++++++++++++++ Foxnouns.Frontend/public/locales/en.json | 12 +++++ Foxnouns.Frontend/vite.config.ts | 4 ++ 5 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 Foxnouns.Frontend/app/routes/auth.welcome/route.tsx diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index bd01eef..b6dde62 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -1,4 +1,10 @@ -import { ActionFunctionArgs, json, redirect, LoaderFunctionArgs } from "@remix-run/node"; +import { + ActionFunctionArgs, + json, + redirect, + LoaderFunctionArgs, + MetaFunction, +} from "@remix-run/node"; import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; import serverRequest, { writeCookie } from "~/lib/request.server"; import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; @@ -12,12 +18,18 @@ import { import { Trans, useTranslation } from "react-i18next"; import { Form, Button, Alert } from "react-bootstrap"; import ErrorAlert from "~/components/ErrorAlert"; +import i18n from "~/i18next.server"; + +export const meta: MetaFunction = ({ data }) => { + return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; +}; export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { return !actionResult; }; export const loader = async ({ request }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); const url = new URL(request.url); const code = url.searchParams.get("code"); @@ -32,7 +44,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { if (resp.has_account) { return json( - { hasAccount: true, user: resp.user!, ticket: null, remoteUser: null }, + { + meta: { title: t("log-in.callback.title.discord-success") }, + hasAccount: true, + user: resp.user!, + ticket: null, + remoteUser: null, + }, { headers: { "Set-Cookie": writeCookie("pronounscc-token", resp.token!), @@ -42,6 +60,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { } return json({ + meta: { title: t("log-in.callback.title.discord-register") }, hasAccount: false, user: null, ticket: resp.ticket!, diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index adc4ce9..226b859 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -5,7 +5,12 @@ import { redirect, ActionFunctionArgs, } from "@remix-run/node"; -import { Form as RemixForm, useActionData, useLoaderData } from "@remix-run/react"; +import { + Form as RemixForm, + ShouldRevalidateFunction, + useActionData, + useLoaderData, +} from "@remix-run/react"; import { Form, Button, ButtonGroup, ListGroup, Row, Col } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; @@ -19,6 +24,10 @@ export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} - pronouns.cc` }]; }; +export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { + return !actionResult; +}; + export const loader = async ({ request }: LoaderFunctionArgs) => { const t = await i18n.getFixedT(request); const token = getToken(request); diff --git a/Foxnouns.Frontend/app/routes/auth.welcome/route.tsx b/Foxnouns.Frontend/app/routes/auth.welcome/route.tsx new file mode 100644 index 0000000..341da15 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/auth.welcome/route.tsx @@ -0,0 +1,52 @@ +import { LoaderFunctionArgs, redirect, json, MetaFunction } from "@remix-run/node"; +import i18n from "~/i18next.server"; +import serverRequest, { getToken } from "~/lib/request.server"; +import { User } from "~/lib/api/user"; +import { useTranslation } from "react-i18next"; +import { Link, useLoaderData } from "@remix-run/react"; +import { Button } from "react-bootstrap"; + +export const meta: MetaFunction = ({ data }) => { + return [{ title: `${data?.meta.title || "Welcome"} - pronouns.cc` }]; +}; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); + const token = getToken(request); + let user: User; + + if (token) { + try { + user = await serverRequest("GET", "/users/@me", { token }); + } catch (e) { + return redirect("/auth/log-in"); + } + } else { + return redirect("/auth/log-in"); + } + + return json({ meta: { title: t("welcome.title") }, user }); +}; + +export default function WelcomePage() { + const { t } = useTranslation(); + const { user } = useLoaderData(); + + return ( +
    +

    {t("welcome.header")}

    +

    {t("welcome.blurb")}

    +

    {t("welcome.customize-profile")}

    +

    {t("welcome.customize-profile-blurb")}

    +

    {t("welcome.create-members")}

    +

    {t("welcome.create-members-blurb")}

    +

    {t("welcome.custom-preferences")}

    +

    {t("welcome.custom-preferences-blurb")}

    + + + +
    + ); +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index f2dfbf2..d3c8d85 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -32,6 +32,10 @@ }, "log-in": { "callback": { + "title": { + "discord-success": "Log in with Discord", + "discord-register": "Register with Discord" + }, "success": "Successfully logged in!", "success-link": "Welcome back, <1>@{{username}}!", "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", @@ -58,5 +62,13 @@ "tumblr": "Log in with Tumblr" }, "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + }, + "welcome": { + "title": "Welcome", + "header": "Welcome to pronouns.cc!", + "customize-profile": "Customize your profile", + "create-members": "Create members", + "custom-preferences": "Customize your preferences", + "profile-button": "Go to your profile" } } diff --git a/Foxnouns.Frontend/vite.config.ts b/Foxnouns.Frontend/vite.config.ts index e609580..a678fb6 100644 --- a/Foxnouns.Frontend/vite.config.ts +++ b/Foxnouns.Frontend/vite.config.ts @@ -20,5 +20,9 @@ export default defineConfig({ changeOrigin: true, }, }, + hmr: { + host: "localhost", + protocol: "ws", + }, }, }); From bb76c24017a18a526ffc67bbc6a1a859203acabe Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 15 Sep 2024 16:48:22 +0200 Subject: [PATCH 050/261] feat(frontend): slightly better error page --- .../app/components/nav/BaseNavbar.tsx | 22 +++++++ .../app/components/nav/Navbar.tsx | 58 ++++++++----------- Foxnouns.Frontend/app/root.tsx | 25 ++++++-- Foxnouns.Frontend/public/locales/en.json | 7 ++- 4 files changed, 72 insertions(+), 40 deletions(-) create mode 100644 Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx diff --git a/Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx b/Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx new file mode 100644 index 0000000..f87b941 --- /dev/null +++ b/Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx @@ -0,0 +1,22 @@ +import { ReactNode } from "react"; +import { Nav, Navbar } from "react-bootstrap"; +import { Link } from "@remix-run/react"; +import Logo from "~/components/nav/Logo"; + +export default function BaseNavbar({ children, theme }: { children?: ReactNode; theme: string }) { + return ( + + + + + {children && ( + <> + + + + + + )} + + ); +} diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx index 7e311f2..3f8beae 100644 --- a/Foxnouns.Frontend/app/components/nav/Navbar.tsx +++ b/Foxnouns.Frontend/app/components/nav/Navbar.tsx @@ -1,11 +1,11 @@ import { Link, useFetcher } from "@remix-run/react"; import Meta from "~/lib/api/meta"; import { User, UserSettings } from "~/lib/api/user"; -import Logo from "./Logo"; -import { Nav, Navbar, NavDropdown } from "react-bootstrap"; +import { Nav, NavDropdown } from "react-bootstrap"; import { BrightnessHigh, BrightnessHighFill, MoonFill } from "react-bootstrap-icons"; import { useTranslation } from "react-i18next"; +import BaseNavbar from "~/components/nav/BaseNavbar"; export default function MainNavbar({ user, @@ -49,36 +49,28 @@ export default function MainNavbar({ const theme = settings.dark_mode ? "dark" : "light"; return ( - - - - - - - - - + + {userMenu} + + + {t("navbar.theme")} + + } + align="end" + > + + {t("navbar.theme-auto")} + + + {t("navbar.theme-dark")} + + + {t("navbar.theme-light")} + + + + ); } diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index a3296b4..e7caee7 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -24,6 +24,8 @@ import getLocalSettings from "./lib/settings.server"; import { LANGUAGE } from "~/env.server"; import { errorCodeDesc } from "./components/ErrorAlert"; import { Container } from "react-bootstrap"; +import { ReactNode } from "react"; +import BaseNavbar from "~/components/nav/BaseNavbar"; export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest("GET", "/meta"); @@ -54,7 +56,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { ); }; -export function Layout({ children }: { children: React.ReactNode }) { +export function Layout({ children }: { children: ReactNode }) { const { locale, settings } = useRouteLoaderData("root") || { meta: { users: { @@ -94,6 +96,8 @@ export function Layout({ children }: { children: React.ReactNode }) { } export function ErrorBoundary() { + const data = useRouteLoaderData("root"); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const error: any = useRouteError(); const { t } = useTranslation(); @@ -110,14 +114,17 @@ export function ErrorBoundary() { return ( - - <>{t("error.title")} - pronouns.cc</> - + {t("error.title")} - {errorElem} + {data?.meUser && data?.settings && data?.meta ? ( + + ) : ( + + )} + {errorElem} @@ -130,8 +137,14 @@ function ApiErrorElem({ error }: { error: ApiError }) { return ( <> -

    {t("error.heading")}

    +

    {t("error.heading")}

    {errorDesc}

    +
    + {t("error.more-info")} +
    +					{JSON.stringify(error, null, "  ")}
    +				
    +
    ); } diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index d3c8d85..07672bc 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -18,7 +18,8 @@ "member-not-found": "Member not found, please check your spelling and try again.", "user-not-found": "User not found, please check your spelling and try again." }, - "title": "Error" + "title": "An error occurred", + "more-info": "Click here for a more detailed error" }, "navbar": { "view-profile": "View profile", @@ -66,9 +67,13 @@ "welcome": { "title": "Welcome", "header": "Welcome to pronouns.cc!", + "blurb": "{welcome.blurb}", "customize-profile": "Customize your profile", + "customize-profile-blurb": "{welcome.customize-profile-blurb}", "create-members": "Create members", + "create-members-blurb": "{welcome.create-members-blurb}", "custom-preferences": "Customize your preferences", + "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", "profile-button": "Go to your profile" } } From 6388e3127d0e2371adc7c2a80017e9ea61738fb7 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 17 Sep 2024 20:58:31 +0200 Subject: [PATCH 051/261] add dev command to repository root --- .gitignore | 1 + Foxnouns.Backend/Foxnouns.Backend.csproj | 2 +- .../Middleware/AuthenticationMiddleware.cs | 1 - package.json | 8 + yarn.lock | 176 ++++++++++++++++++ 5 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.gitignore b/.gitignore index 2d0d096..50799e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ bin/ obj/ +node_modules/ .version config.ini *.DotSettings.user diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 8207394..987ebbd 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -44,7 +44,7 @@
    - + diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index 5c69b79..9190fac 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -31,7 +31,6 @@ public class AuthenticationMiddleware(DatabaseContext db) : IMiddleware } ctx.SetToken(oauthToken); - await next(ctx); } } diff --git a/package.json b/package.json new file mode 100644 index 0000000..da7f5dc --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "devDependencies": { + "concurrently": "^9.0.1" + }, + "scripts": { + "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'cd Foxnouns.Backend && dotnet watch --no-hot-reload' 'cd Foxnouns.Frontend && yarn dev' 'cd rate && go run -v .'" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..2e0d845 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,176 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concurrently@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.0.1.tgz#01e171bf6c7af0c022eb85daef95bff04d8185aa" + integrity sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg== + 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: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +escalade@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +rxjs@^7.8.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +tslib@^2.1.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.7.2: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" From 41e620ad03dd4fa36ab373b3a29d60a900ef712c Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 17 Sep 2024 22:12:12 +0200 Subject: [PATCH 052/261] feat: add go users exporter --- Foxnouns.Frontend/app/lib/api/user.ts | 42 ++- .../app/routes/$username/route.tsx | 13 +- migrators/go-exporter/.gitignore | 1 + migrators/go-exporter/go.mod | 82 +++++ migrators/go-exporter/go.sum | 286 ++++++++++++++++++ migrators/go-exporter/main.go | 101 +++++++ migrators/go-exporter/user.go | 111 +++++++ 7 files changed, 629 insertions(+), 7 deletions(-) create mode 100644 migrators/go-exporter/.gitignore create mode 100644 migrators/go-exporter/go.mod create mode 100644 migrators/go-exporter/go.sum create mode 100644 migrators/go-exporter/main.go create mode 100644 migrators/go-exporter/user.go diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts index babae31..270a4af 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -1,13 +1,49 @@ -export type User = { +export type PartialUser = { id: string; username: string; - display_name: string | null; + display_name?: string | null; + avatar_url?: string | null; +}; + +export type User = PartialUser & { bio: string | null; member_title: string | null; - avatar_url: string | null; links: string[]; + names: FieldEntry[]; + pronouns: Pronoun[]; + fields: Field[]; +}; + +export type UserWithMembers = User & { members: PartialMember[] }; + +export type UserWithHiddenFields = User & { + auth_methods?: unknown[]; + member_list_hidden: boolean; + last_active: string; }; export type UserSettings = { dark_mode: boolean | null; }; + +export type PartialMember = { + id: string; + name: string; + display_name: string | null; + bio: string | null; + avatar_url: string | null; + names: FieldEntry[]; + pronouns: Pronoun[]; +}; + +export type FieldEntry = { + value: string; + status: string; +}; + +export type Pronoun = FieldEntry & { display_text: string | null }; + +export type Field = { + name: string; + entries: FieldEntry[]; +}; diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index 418b3e8..f6f2559 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -1,6 +1,6 @@ import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { redirect, useLoaderData } from "@remix-run/react"; -import { User } from "~/lib/api/user"; +import { UserWithMembers } from "~/lib/api/user"; import serverRequest from "~/lib/request.server"; export const meta: MetaFunction = ({ data }) => { @@ -9,14 +9,19 @@ export const meta: MetaFunction = ({ data }) => { return [{ title: `@${user.username} - pronouns.cc` }]; }; -export const loader = async ({ params }: LoaderFunctionArgs) => { +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const url = new URL(request.url); + const memberPage = parseInt(url.searchParams.get("page") ?? "0", 10); + let username = params.username!; if (!username.startsWith("@")) throw redirect(`/@${username}`); username = username.substring("@".length); - const user = await serverRequest("GET", `/users/${username}`); + const user = await serverRequest("GET", `/users/${username}`); + let members = user.members.slice(memberPage * 20, (memberPage + 1) * 20); + if (members.length === 0) members = user.members.slice(0, 20); - return json({ user }); + return json({ user, members }); }; export default function UserPage() { diff --git a/migrators/go-exporter/.gitignore b/migrators/go-exporter/.gitignore new file mode 100644 index 0000000..94a2dd1 --- /dev/null +++ b/migrators/go-exporter/.gitignore @@ -0,0 +1 @@ +*.json \ No newline at end of file diff --git a/migrators/go-exporter/go.mod b/migrators/go-exporter/go.mod new file mode 100644 index 0000000..aaadec0 --- /dev/null +++ b/migrators/go-exporter/go.mod @@ -0,0 +1,82 @@ +module code.vulpine.solutions/sam/Foxnouns.NET/go-exporter + +go 1.22.6 + +require ( + cloud.google.com/go/compute v1.23.2 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + codeberg.org/pronounscc/pronouns.cc v0.6.5-0.20240213162950-5fcd87a94a07 // indirect + emperror.dev/errors v0.8.1 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect + github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec // indirect + github.com/ajg/form v1.5.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bwmarrin/discordgo v0.27.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/davidbyttow/govips/v2 v2.13.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/georgysavva/scany/v2 v2.0.0 // indirect + github.com/getsentry/sentry-go v0.25.0 // indirect + github.com/go-chi/chi/v5 v5.0.10 // indirect + github.com/go-chi/cors v1.2.1 // indirect + github.com/go-chi/httprate v0.7.4 // indirect + github.com/go-chi/render v1.0.3 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.4.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/joho/godotenv v1.5.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mediocregopher/radix/v4 v4.1.4 // indirect + github.com/minio/md5-simd v1.1.2 // indirect + github.com/minio/minio-go/v7 v7.0.63 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/rs/xid v1.5.0 // indirect + github.com/rubenv/sql-migrate v1.5.2 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tilinna/clock v1.1.0 // indirect + github.com/toshi0607/chi-prometheus v0.1.4 // indirect + github.com/urfave/cli/v2 v2.25.7 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect + go.opencensus.io v0.24.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.26.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/image v0.13.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sync v0.4.0 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect + google.golang.org/api v0.148.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/migrators/go-exporter/go.sum b/migrators/go-exporter/go.sum new file mode 100644 index 0000000..933780c --- /dev/null +++ b/migrators/go-exporter/go.sum @@ -0,0 +1,286 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go/compute v1.23.2 h1:nWEMDhgbBkBJjfpVySqU4jgWdc22PLR0o4vEexZHers= +cloud.google.com/go/compute v1.23.2/go.mod h1:JJ0atRC0J/oWYiiVBmsSsrRnh92DhZPG4hFDcR04Rns= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +codeberg.org/pronounscc/pronouns.cc v0.6.5-0.20240213162950-5fcd87a94a07 h1:Vsf1xjT8msCuNj3KbDsqs7QR614WJgYFYc6+cMsx8zM= +codeberg.org/pronounscc/pronouns.cc v0.6.5-0.20240213162950-5fcd87a94a07/go.mod h1:Y4HFkHFeIGoK+CAkig0bUxRTUm5Eprr7oP3mpVizUi0= +emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= +emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE= +github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k= +github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec h1:2NFk5fe52cHyRcUnXSs4CSEAqm+rL/hr3AdflBE3VPU= +github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= +github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davidbyttow/govips/v2 v2.13.0 h1:5MK9ZcXZC5GzUR9Ca8fJwOYqMgll/H096ec0PJP59QM= +github.com/davidbyttow/govips/v2 v2.13.0/go.mod h1:LPTrwWtNa5n4yl9UC52YBOEGdZcY5hDTP4Ms2QWasTw= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU= +github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38= +github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI= +github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= +github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= +github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= +github.com/go-chi/httprate v0.7.4 h1:a2GIjv8he9LRf3712zxxnRdckQCm7I8y8yQhkJ84V6M= +github.com/go-chi/httprate v0.7.4/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= +github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= +github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= +github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= +github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +github.com/mediocregopher/radix/v4 v4.1.4 h1:Uze6DEbEAvL+VHXUEu/EDBTkUk5CLct5h3nVSGpc6Ts= +github.com/mediocregopher/radix/v4 v4.1.4/go.mod h1:ajchozX/6ELmydxWeWM6xCFHVpZ4+67LXHOTOVR0nCE= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ= +github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= +github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= +github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= +github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao= +github.com/tilinna/clock v1.1.0 h1:6IQQQCo6KoBxVudv6gwtY8o4eDfhHo8ojA5dP0MfhSs= +github.com/tilinna/clock v1.1.0/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao= +github.com/toshi0607/chi-prometheus v0.1.4 h1:5KpqJrmdvMvbfU0JiL9ghOTbe8S9sgHDCCQvXgnyoJo= +github.com/toshi0607/chi-prometheus v0.1.4/go.mod h1:E++tBjqpDsvGWjLYdcFd5rvqJ7HG8wwBux+M6gyIL/Q= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= +golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= +golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs= +google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a h1:a2MQQVoTo96JC9PMGtGBymLp7+/RzpFc2yX/9WfFg1c= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/migrators/go-exporter/main.go b/migrators/go-exporter/main.go new file mode 100644 index 0000000..ac749ac --- /dev/null +++ b/migrators/go-exporter/main.go @@ -0,0 +1,101 @@ +package main + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "time" + + "codeberg.org/pronounscc/pronouns.cc/backend/db" + "github.com/Masterminds/squirrel" + "github.com/jackc/pgx/v5/pgxpool" +) + +var exportWhat = flag.String("export", "", "What to export") +var exportLimit = flag.Int64("limit", 0, "Number of items to export for testing") + +var sq = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) +var oldDB *db.DB + +type Output struct { + Output any `json:"output"` // The output array + Skipped []string `json:"skipped"` // IDs of skipped items +} + +func main() { + flag.Parse() + + dbUrl := os.Getenv("DATABASE") + + pool, err := pgxpool.New(context.Background(), dbUrl) + if err != nil { + log.Fatalf("creating database pool: %v\n", err) + } + + oldDB = &db.DB{ + Pool: pool, + } + + switch *exportWhat { + case "users": + exportUsers() + default: + fmt.Printf("invalid export type %q\nvalid export types are: users\n", *exportWhat) + } +} + +func exportUsers() { + filename := fmt.Sprintf("users-output-%v.json", time.Now().Unix()) + f, err := os.Create(filename) + if err != nil { + log.Fatalf("error opening output file %q: %v\n", filename, err) + } + defer f.Close() + + users, err := getUsers() + if err != nil { + log.Fatalf("getting users from database: %v\n", err) + } + + log.Println("converting users") + + start := time.Now() + + var ( + newUsers []NewUser + skipped []string + ) + for i, u := range users { + newUser, err := userToNewUser(u) + if err != nil { + log.Printf("error converting user %v: %v\n", u.SnowflakeID, err) + skipped = append(skipped, u.SnowflakeID.String()) + continue + } + + newUsers = append(newUsers, newUser) + log.Printf("converted user %7d (%v)\n", i+1, u.SnowflakeID) + } + + log.Printf("converted users in %v (skipped: %v)\n", time.Since(start).Round(time.Millisecond), len(skipped)) + + b, err := json.MarshalIndent(Output{Output: newUsers, Skipped: skipped}, "", " ") + if err != nil { + log.Fatalf("error marshaling json: %v\n", err) + } + + _, err = f.Write(b) + if err != nil { + log.Fatalf("writing file: %v\n", err) + } + + err = f.Sync() + if err != nil { + log.Fatalf("syncing file: %v\n", err) + } + + fmt.Printf("\n\nexported %v users! filename: %q\n", len(newUsers), filename) +} diff --git a/migrators/go-exporter/user.go b/migrators/go-exporter/user.go new file mode 100644 index 0000000..216c665 --- /dev/null +++ b/migrators/go-exporter/user.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "time" + + "codeberg.org/pronounscc/pronouns.cc/backend/db" + "emperror.dev/errors" + "github.com/georgysavva/scany/v2/pgxscan" +) + +func getUsers() (u []db.User, err error) { + q := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance"). + From("users").OrderBy("snowflake_id ASC") + + if *exportLimit != 0 { + q = q.Limit(uint64(*exportLimit)) + } + + sql, args, err := q.ToSql() + if err != nil { + return u, errors.Wrap(err, "building sql") + } + + err = pgxscan.Select(context.Background(), oldDB, &u, sql, args...) + if err != nil { + return u, errors.Wrap(err, "getting users from db") + } + + return u, nil +} + +func userToNewUser(u db.User) (NewUser, error) { + fields, err := oldDB.UserFields(context.Background(), u.ID) + if err != nil { + return NewUser{}, errors.Wrap(err, "getting fields") + } + + new := NewUser{ + ID: u.SnowflakeID.String(), + SID: u.SID, + Username: u.Username, + DisplayName: u.DisplayName, + Bio: u.Bio, + MemberTitle: u.MemberTitle, + LastActive: u.LastActive, + Avatar: u.Avatar, + Links: u.Links, + Names: u.Names, + + MemberListHidden: u.ListPrivate, + Timezone: u.Timezone, + Role: "USER", + + Deleted: u.DeletedAt != nil, + DeletedAt: u.DeletedAt, + DeleteReason: u.DeleteReason, + + CustomPreferences: u.CustomPreferences, + } + + if u.IsAdmin { + new.Role = "ADMIN" + } + + for _, p := range u.Pronouns { + new.Pronouns = append(new.Pronouns, NewPronoun{Value: p.Pronouns, Status: string(p.Status), DisplayText: p.DisplayText}) + } + + for _, f := range fields { + new.Fields = append(new.Fields, NewField{Name: f.Name, Entries: f.Entries}) + } + + return new, nil +} + +type NewUser struct { + ID string `json:"id"` + SID string `json:"sid"` + Username string `json:"username"` + DisplayName *string `json:"display_name"` + Bio *string `json:"bio"` + MemberTitle *string `json:"member_title"` + LastActive time.Time `json:"last_active"` + Avatar *string `json:"avatar_hash"` + Links []string `json:"links"` + Names []db.FieldEntry `json:"names"` + Pronouns []NewPronoun `json:"pronouns"` + Fields []NewField `json:"fields"` + + MemberListHidden bool `json:"member_list_hidden"` + Timezone *string `json:"timezone"` + Role string `json:"role"` // one of USER or ADMIN + + Deleted bool `json:"deleted"` + DeletedAt *time.Time `json:"deleted_at"` + DeleteReason *string `json:"delete_reason"` // TODO: this should be imported as a warning + + CustomPreferences db.CustomPreferences `json:"custom_preferences"` +} + +type NewPronoun struct { + Value string `json:"value"` + Status string `json:"status"` + DisplayText *string `json:"display_text"` +} + +type NewField struct { + Name string `json:"name"` + Entries []db.FieldEntry `json:"entries"` +} From 412d720abc580bdb1815c91c88df3e41fc1a1d0d Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 18 Sep 2024 21:44:47 +0200 Subject: [PATCH 053/261] feat: add .net user importer --- .../.idea.Foxnouns.NET/.idea/indexLayout.xml | 1 + Foxnouns.Backend/Database/Snowflake.cs | 1 + .../Extensions/WebApplicationExtensions.cs | 1 - Foxnouns.Frontend/package.json | 2 +- Foxnouns.Frontend/yarn.lock | 8 +- Foxnouns.NET.sln | 6 + migrators/NetImporter/ImportUser.cs | 174 ++++++++++++++++++ migrators/NetImporter/NetImporter.cs | 83 +++++++++ migrators/NetImporter/NetImporter.csproj | 25 +++ migrators/go-exporter/user.go | 23 +++ 10 files changed, 318 insertions(+), 6 deletions(-) create mode 100644 migrators/NetImporter/ImportUser.cs create mode 100644 migrators/NetImporter/NetImporter.cs create mode 100644 migrators/NetImporter/NetImporter.csproj diff --git a/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml b/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml index a797372..ea4646c 100644 --- a/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml +++ b/.idea/.idea.Foxnouns.NET/.idea/indexLayout.xml @@ -3,6 +3,7 @@ Foxnouns.Frontend + migrators/go-exporter diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index 78efee6..5c1c4bb 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -4,6 +4,7 @@ using System.Globalization; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; using NodaTime; +using JsonSerializer = Newtonsoft.Json.JsonSerializer; namespace Foxnouns.Backend.Database; diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 77a8a7e..132b4d0 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -34,7 +34,6 @@ public static class WebApplicationExtensions .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) - .MinimumLevel.Override("Hangfire", LogEventLevel.Information) .WriteTo.Console(); if (config.Logging.SeqLogUrl != null) diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index cc0c993..839ad14 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -56,7 +56,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "i18next-parser": "^9.0.2", "prettier": "^3.3.3", - "sass": "^1.78.0", + "sass": "1.77.6", "typescript": "^5.1.6", "vite": "^5.1.0", "vite-tsconfig-paths": "^4.2.1" diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index 1d7c038..c55dc2e 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -6097,10 +6097,10 @@ safe-regex-test@^1.0.3: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass@^1.78.0: - version "1.78.0" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.78.0.tgz#cef369b2f9dc21ea1d2cf22c979f52365da60841" - integrity sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ== +sass@1.77.6: + version "1.77.6" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4" + integrity sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q== dependencies: chokidar ">=3.0.0 <4.0.0" immutable "^4.0.0" diff --git a/Foxnouns.NET.sln b/Foxnouns.NET.sln index 3b119c5..aec8ae7 100644 --- a/Foxnouns.NET.sln +++ b/Foxnouns.NET.sln @@ -5,6 +5,8 @@ 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}") = "NetImporter", "migrators\NetImporter\NetImporter.csproj", "{FBCF80EE-624F-43AF-8122-230B5447940C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ 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 + {FBCF80EE-624F-43AF-8122-230B5447940C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBCF80EE-624F-43AF-8122-230B5447940C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBCF80EE-624F-43AF-8122-230B5447940C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBCF80EE-624F-43AF-8122-230B5447940C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/migrators/NetImporter/ImportUser.cs b/migrators/NetImporter/ImportUser.cs new file mode 100644 index 0000000..3524fad --- /dev/null +++ b/migrators/NetImporter/ImportUser.cs @@ -0,0 +1,174 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using NodaTime.Extensions; +using Serilog; + +namespace NetImporter; + +public static class Users +{ + public static async Task ImportUsers(string filename) + { + await using var db = await NetImporter.GetContextAsync(); + await db.Database.ExecuteSqlRawAsync("SELECT 1"); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + var users = NetImporter.ReadFromFile(filename).Output.Select(ConvertUser).ToList(); + db.AddRange(users); + await db.SaveChangesAsync(); + + stopwatch.Stop(); + Log.Information("Imported {Count} users in {Duration}", users.Count, stopwatch.ElapsedDuration()); + } + + private static User ConvertUser(ImportUser oldUser) + { + var user = new User + { + Id = oldUser.Id, + Username = oldUser.Username, + DisplayName = oldUser.DisplayName, + Bio = oldUser.Bio, + MemberTitle = oldUser.MemberTitle, + LastActive = oldUser.LastActive.ToInstant(), + Avatar = oldUser.AvatarHash, + Links = oldUser.Links ?? [], + + Role = oldUser.ParseRole(), + Deleted = oldUser.Deleted, + DeletedAt = oldUser.DeletedAt?.ToInstant(), + DeletedBy = null + }; + + if (oldUser is { DiscordId: not null, DiscordUsername: not null }) + { + user.AuthMethods.Add(new AuthMethod + { + Id = SnowflakeGenerator.Instance.GenerateSnowflake(), + AuthType = AuthType.Discord, + RemoteId = oldUser.DiscordId, + RemoteUsername = oldUser.DiscordUsername + }); + } + + if (oldUser is { TumblrId: not null, TumblrUsername: not null }) + { + user.AuthMethods.Add(new AuthMethod + { + Id = SnowflakeGenerator.Instance.GenerateSnowflake(), + AuthType = AuthType.Tumblr, + RemoteId = oldUser.TumblrId, + RemoteUsername = oldUser.TumblrUsername + }); + } + + if (oldUser is { GoogleId: not null, GoogleUsername: not null }) + { + user.AuthMethods.Add(new AuthMethod + { + Id = SnowflakeGenerator.Instance.GenerateSnowflake(), + AuthType = AuthType.Google, + RemoteId = oldUser.GoogleId, + RemoteUsername = oldUser.GoogleUsername + }); + } + + // Convert all custom preference UUIDs to snowflakes + var prefMapping = new Dictionary(); + foreach (var (key, value) in oldUser.CustomPreferences) + { + var newKey = SnowflakeGenerator.Instance.GenerateSnowflake(); + prefMapping[key] = newKey; + user.CustomPreferences[newKey] = value; + } + + foreach (var name in oldUser.Names ?? []) + { + user.Names.Add(new FieldEntry + { + Value = name.Value, + Status = prefMapping.TryGetValue(name.Status, out var newStatus) ? newStatus.ToString() : name.Status, + }); + } + + foreach (var pronoun in oldUser.Pronouns ?? []) + { + user.Pronouns.Add(new Pronoun + { + Value = pronoun.Value, + DisplayText = pronoun.DisplayText, + Status = prefMapping.TryGetValue(pronoun.Status, out var newStatus) + ? newStatus.ToString() + : pronoun.Status, + }); + } + + foreach (var field in oldUser.Fields ?? []) + { + var entries = field.Entries.Select(entry => new FieldEntry + { + Value = entry.Value, + Status = prefMapping.TryGetValue(entry.Status, out var newStatus) + ? newStatus.ToString() + : entry.Status, + }) + .ToList(); + + user.Fields.Add(new Field + { + Name = field.Name, + Entries = entries.ToArray() + }); + } + + Log.Debug("Converted user {UserId}", oldUser.Id); + + return user; + } + + [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + private record ImportUser( + Snowflake Id, + string Sid, + string Username, + string? DisplayName, + string? Bio, + string? MemberTitle, + OffsetDateTime LastActive, + string? AvatarHash, + string[]? Links, + FieldEntry[]? Names, + Pronoun[]? Pronouns, + Field[]? Fields, + string? DiscordId, + string? DiscordUsername, + string? FediverseId, + string? FediverseUsername, + long? FediverseAppId, + string? TumblrId, + string? TumblrUsername, + string? GoogleId, + string? GoogleUsername, + bool MemberListHidden, + string? Timezone, + string Role, + bool Deleted, + OffsetDateTime? DeletedAt, + string? DeleteReason, + Dictionary CustomPreferences) + { + public UserRole ParseRole() => Role switch + { + "USER" => UserRole.User, + "MODERATOR" => UserRole.Moderator, + "ADMIN" => UserRole.Admin, + _ => UserRole.User + }; + } +} \ No newline at end of file diff --git a/migrators/NetImporter/NetImporter.cs b/migrators/NetImporter/NetImporter.cs new file mode 100644 index 0000000..05cc42d --- /dev/null +++ b/migrators/NetImporter/NetImporter.cs @@ -0,0 +1,83 @@ +using Foxnouns.Backend; +using Foxnouns.Backend.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using NodaTime; +using NodaTime.Serialization.JsonNet; +using Serilog; +using Serilog.Events; + +namespace NetImporter; + +internal static class NetImporter +{ + public static async Task Main(string[] args) + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Debug() + .MinimumLevel.Override("Microsoft", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Information) + .WriteTo.Console() + .CreateLogger(); + + switch (args.Length) + { + case < 2: + Console.WriteLine("Not enough arguments. Usage: "); + return; + case > 2: + Console.WriteLine("Too many arguments. Usage: "); + return; + } + + switch (args[0].ToLowerInvariant()) + { + case "users": + await Users.ImportUsers(args[1]); + break; + default: + Console.WriteLine("Invalid type. Valid types are: users"); + break; + } + } + + internal static async Task GetContextAsync() + { + var connString = Environment.GetEnvironmentVariable("DATABASE"); + if (connString == null) throw new Exception("$DATABASE not set, must be an ADO.NET connection string"); + + var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); + var config = new Config + { + Database = new Config.DatabaseConfig + { + Url = connString + } + }; + + var db = new DatabaseContext(config, loggerFactory); + + if ((await db.Database.GetPendingMigrationsAsync()).Any()) + { + Log.Fatal("Database needs to be migrated first"); + } + + return db; + } + + private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings + { + ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() } + }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); + + internal static Input ReadFromFile(string path) + { + var data = File.ReadAllText(path); + return JsonConvert.DeserializeObject>(data, Settings) ?? throw new Exception("Invalid input file"); + } +} + +internal record Input(List Output, List Skipped); \ No newline at end of file diff --git a/migrators/NetImporter/NetImporter.csproj b/migrators/NetImporter/NetImporter.csproj new file mode 100644 index 0000000..e62f921 --- /dev/null +++ b/migrators/NetImporter/NetImporter.csproj @@ -0,0 +1,25 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/migrators/go-exporter/user.go b/migrators/go-exporter/user.go index 216c665..1af246e 100644 --- a/migrators/go-exporter/user.go +++ b/migrators/go-exporter/user.go @@ -48,6 +48,16 @@ func userToNewUser(u db.User) (NewUser, error) { Links: u.Links, Names: u.Names, + Discord: u.Discord, + DiscordUsername: u.DiscordUsername, + Fediverse: u.Fediverse, + FediverseUsername: u.FediverseUsername, + FediverseAppID: u.FediverseAppID, + Tumblr: u.Tumblr, + TumblrUsername: u.TumblrUsername, + Google: u.Google, + GoogleUsername: u.GoogleUsername, + MemberListHidden: u.ListPrivate, Timezone: u.Timezone, Role: "USER", @@ -88,6 +98,19 @@ type NewUser struct { Pronouns []NewPronoun `json:"pronouns"` Fields []NewField `json:"fields"` + Discord *string `json:"discord_id"` + DiscordUsername *string `json:"discord_username"` + + Fediverse *string `json:"fediverse_id"` + FediverseUsername *string `json:"fediverse_username"` + FediverseAppID *int64 `json:"fediverse_app_id"` + + Tumblr *string `json:"tumblr_id"` + TumblrUsername *string `json:"tumblr_username"` + + Google *string `json:"google_id"` + GoogleUsername *string `json:"google_username"` + MemberListHidden bool `json:"member_list_hidden"` Timezone *string `json:"timezone"` Role string `json:"role"` // one of USER or ADMIN From 862a64840edba0bc9563f3b904b2f1453d2dca3f Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 24 Sep 2024 20:56:10 +0200 Subject: [PATCH 054/261] feat: add avatar/bio/links/names/pronouns to user page --- .gitignore | 1 + .../inspectionProfiles/Project_Default.xml | 31 ++++ .../Controllers/MembersController.cs | 3 +- .../Controllers/UsersController.cs | 22 +++ Foxnouns.Backend/Utils/ValidationUtils.cs | 67 +++++++- .../app/components/KeyedIcon.tsx | 16 ++ .../app/components/ProfileLink.tsx | 27 +++ .../app/components/PronounLink.tsx | 32 ++++ .../app/components/StatusIcon.tsx | 34 ++++ .../app/components/StatusLine.tsx | 36 ++++ Foxnouns.Frontend/app/lib/api/user.ts | 60 +++++++ Foxnouns.Frontend/app/lib/markdown.ts | 22 +++ .../app/routes/$username/route.tsx | 87 +++++++++- Foxnouns.Frontend/package.json | 7 +- Foxnouns.Frontend/public/locales/en.json | 162 +++++++++--------- Foxnouns.Frontend/yarn.lock | 133 +++++++++++++- 16 files changed, 650 insertions(+), 90 deletions(-) create mode 100644 Foxnouns.Frontend/app/components/KeyedIcon.tsx create mode 100644 Foxnouns.Frontend/app/components/ProfileLink.tsx create mode 100644 Foxnouns.Frontend/app/components/PronounLink.tsx create mode 100644 Foxnouns.Frontend/app/components/StatusIcon.tsx create mode 100644 Foxnouns.Frontend/app/components/StatusLine.tsx create mode 100644 Foxnouns.Frontend/app/lib/markdown.ts diff --git a/.gitignore b/.gitignore index 50799e6..d9b0dfd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ node_modules/ config.ini *.DotSettings.user proxy-config.json +.DS_Store diff --git a/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml b/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml index ec33848..c9504c5 100644 --- a/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/.idea.Foxnouns.NET/.idea/inspectionProfiles/Project_Default.xml @@ -2,6 +2,37 @@
    {user.names.length > 0 && ( -
    -

    {t("user.heading.names")}

    -
      - {user.names.map((n, i) => ( - - {n.value} - - ))} -
    -
    + )} {user.pronouns.length > 0 && ( -
    -

    {t("user.heading.pronouns")}

    - {user.pronouns.map((p, i) => ( - - - - ))} -
    + )} + {user.fields.map((f, i) => ( + + ))}
    From bb649d1d72fe0662ae6693924cfe770506ee57cd Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Sep 2024 19:15:03 +0200 Subject: [PATCH 058/261] fix: actually commit the favicon --- Foxnouns.Frontend/public/favicon.svg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Foxnouns.Frontend/public/favicon.svg diff --git a/Foxnouns.Frontend/public/favicon.svg b/Foxnouns.Frontend/public/favicon.svg new file mode 100644 index 0000000..11e664f --- /dev/null +++ b/Foxnouns.Frontend/public/favicon.svg @@ -0,0 +1,2 @@ + +image/svg+xml \ No newline at end of file From f81ae97821980d3273ca603d40773c0553c3dd62 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Sep 2024 19:48:05 +0200 Subject: [PATCH 059/261] feat(backend): return unlisted status in partial member for authenticated users --- .../Controllers/InternalController.cs | 3 +- .../Controllers/MembersController.cs | 3 +- .../Controllers/UsersController.cs | 10 ++-- .../Extensions/AvatarObjectExtensions.cs | 6 +- .../Extensions/WebApplicationExtensions.cs | 4 +- Foxnouns.Backend/Foxnouns.Backend.csproj | 56 +++++++++---------- .../Mailables/AccountCreationMailable.cs | 3 +- .../Services/MemberRendererService.cs | 14 +++-- .../Services/UserRendererService.cs | 7 ++- Foxnouns.Backend/Utils/AuthUtils.cs | 2 +- Foxnouns.Backend/Utils/ValidationUtils.cs | 9 ++- .../Views/Mail/_ViewImports.cshtml | 2 +- Foxnouns.Backend/Views/Mail/_ViewStart.cshtml | 2 +- Foxnouns.Frontend/app/lib/utils.ts | 0 .../app/routes/$username/MemberCard.tsx | 0 15 files changed, 68 insertions(+), 53 deletions(-) create mode 100644 Foxnouns.Frontend/app/lib/utils.ts create mode 100644 Foxnouns.Frontend/app/routes/$username/MemberCard.tsx diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index e63b579..b79de1c 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -60,7 +60,8 @@ public partial class InternalController(DatabaseContext db) : ControllerBase { if (endpoint.RoutePattern.RawText == null) continue; - var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), new RouteValueDictionary()); + var templateMatcher = new TemplateMatcher(TemplateParser.Parse(endpoint.RoutePattern.RawText), + new RouteValueDictionary()); if (!templateMatcher.TryMatch(url, new())) continue; var httpMethodAttribute = endpoint.Metadata.GetMetadata(); if (httpMethodAttribute != null && diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 113d95b..1ffc928 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -42,7 +42,8 @@ public class MembersController( [HttpPost("/api/v2/users/@me/members")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize("member.create")] - public async Task CreateMemberAsync([FromBody] CreateMemberRequest req, CancellationToken ct = default) + public async Task CreateMemberAsync([FromBody] CreateMemberRequest req, + CancellationToken ct = default) { ValidationUtils.Validate([ ("name", ValidationUtils.ValidateMemberName(req.Name)), diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 98e4f9c..bb3417c 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -104,7 +104,8 @@ public class UsersController( [HttpPatch("@me/custom-preferences")] [Authorize("user.update")] [ProducesResponseType>(StatusCodes.Status200OK)] - public async Task UpdateCustomPreferencesAsync([FromBody] List req, CancellationToken ct = default) + public async Task UpdateCustomPreferencesAsync([FromBody] List req, + CancellationToken ct = default) { ValidationUtils.Validate(ValidateCustomPreferences(req)); @@ -180,8 +181,8 @@ public class UsersController( public Pronoun[]? Pronouns { get; init; } public Field[]? Fields { get; init; } } - - + + [HttpGet("@me/settings")] [Authorize("user.read_hidden")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] @@ -194,7 +195,8 @@ public class UsersController( [HttpPatch("@me/settings")] [Authorize("user.read_hidden", "user.update")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task UpdateUserSettingsAsync([FromBody] UpdateUserSettingsRequest req, CancellationToken ct = default) + public async Task UpdateUserSettingsAsync([FromBody] UpdateUserSettingsRequest req, + CancellationToken ct = default) { var user = await db.Users.FirstAsync(u => u.Id == CurrentUser!.Id, ct); diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index cb70adf..7c39aa4 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -14,11 +14,13 @@ public static class AvatarObjectExtensions 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) => + DeleteMemberAvatarAsync(this ObjectStorageService objectStorageService, Snowflake id, string hash, + CancellationToken ct = default) => await objectStorageService.RemoveObjectAsync(MemberAvatarUpdateInvocable.Path(id, hash), ct); public static async Task - DeleteUserAvatarAsync(this ObjectStorageService objectStorageService, Snowflake id, string hash, CancellationToken ct = default) => + DeleteUserAvatarAsync(this ObjectStorageService objectStorageService, Snowflake id, string hash, + CancellationToken ct = default) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); public static async Task ConvertBase64UriToAvatar(this string uri) diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 132b4d0..a5b2af6 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -100,11 +100,11 @@ public static class WebApplicationExtensions // Transient jobs .AddTransient() .AddTransient(); - + if (!config.Logging.EnableMetrics) services.AddHostedService(); }); - + return builder.Services; } diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 987ebbd..a9e7b74 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -8,34 +8,34 @@ - - - - - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + @@ -44,12 +44,12 @@ - + - - .dockerignore - + + .dockerignore + diff --git a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs index 3fc1ff4..cec17cf 100644 --- a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs +++ b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs @@ -2,7 +2,8 @@ using Coravel.Mailer.Mail; namespace Foxnouns.Backend.Mailables; -public class AccountCreationMailable(Config config, AccountCreationMailableView view) : Mailable +public class AccountCreationMailable(Config config, AccountCreationMailableView view) + : Mailable { public override void Build() { diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 962712f..ef7b923 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -11,6 +11,7 @@ public class MemberRendererService(DatabaseContext db, Config config) public async Task> RenderUserMembersAsync(User user, Token? token) { var canReadHiddenMembers = token != null && token.UserId == user.Id && token.HasScope("member.read"); + var renderUnlisted = token != null && token.UserId == user.Id && token.HasScope("user.read_hidden"); var canReadMemberList = !user.ListHidden || canReadHiddenMembers; IEnumerable members = canReadMemberList @@ -20,7 +21,7 @@ public class MemberRendererService(DatabaseContext db, Config config) .ToListAsync() : []; if (!canReadHiddenMembers) members = members.Where(m => !m.Unlisted); - return members.Select(RenderPartialMember); + return members.Select(m => RenderPartialMember(m, renderUnlisted)); } public MemberResponse RenderMember(Member member, Token? token) @@ -34,10 +35,11 @@ public class MemberRendererService(DatabaseContext db, Config config) } private UserRendererService.PartialUser RenderPartialUser(User user) => - new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user)); + new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user), user.CustomPreferences); - public PartialMember RenderPartialMember(Member member) => new(member.Id, member.Name, - member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns); + public PartialMember RenderPartialMember(Member member, bool renderUnlisted = false) => new(member.Id, member.Name, + member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns, + renderUnlisted ? member.Unlisted : null); private string? AvatarUrlFor(Member member) => member.Avatar != null ? $"{config.MediaBaseUrl}/members/{member.Id}/avatars/{member.Avatar}.webp" : null; @@ -52,7 +54,9 @@ public class MemberRendererService(DatabaseContext db, Config config) string? Bio, string? AvatarUrl, IEnumerable Names, - IEnumerable Pronouns); + IEnumerable Pronouns, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + bool? Unlisted); public record MemberResponse( Snowflake Id, diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 07bdb8b..8251611 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -39,7 +39,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe return new UserResponse( user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, - renderMembers ? members.Select(memberRenderer.RenderPartialMember) : null, + renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderAuthMethods ? authMethods.Select(a => new AuthenticationMethodResponse( a.Id, a.AuthType, a.RemoteId, @@ -52,7 +52,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe } public PartialUser RenderPartialUser(User user) => - new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user)); + new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user), user.CustomPreferences); private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; @@ -94,6 +94,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe Snowflake Id, string Username, string? DisplayName, - string? AvatarUrl + string? AvatarUrl, + Dictionary CustomPreferences ); } \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 26965e2..c767198 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -79,7 +79,7 @@ public static class AuthUtils return false; } } - + public static bool TryParseToken(string? input, out byte[] rawToken) { rawToken = []; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 9020c0f..f29dd24 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -156,7 +156,8 @@ public static class ValidationUtils break; } - errors = errors.Concat(ValidateFieldEntries(field.Entries, customPreferences, $"fields.{index}.entries")).ToList(); + errors = errors.Concat(ValidateFieldEntries(field.Entries, customPreferences, $"fields.{index}.entries")) + .ToList(); } return errors; @@ -238,12 +239,14 @@ public static class ValidationUtils { case > Limits.FieldEntryTextLimit: errors.Add(($"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError("Pronoun display text is too long", 1, Limits.FieldEntryTextLimit, + ValidationError.LengthError("Pronoun display text is too long", 1, + Limits.FieldEntryTextLimit, entry.Value.Length))); break; case < 1: errors.Add(($"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError("Pronoun display text is too short", 1, Limits.FieldEntryTextLimit, + ValidationError.LengthError("Pronoun display text is too short", 1, + Limits.FieldEntryTextLimit, entry.Value.Length))); break; } diff --git a/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml b/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml index 6ececef..f13b1c3 100644 --- a/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml +++ b/Foxnouns.Backend/Views/Mail/_ViewImports.cshtml @@ -1,2 +1,2 @@ @using Foxnouns.Backend -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@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 index b74bab7..4080127 100644 --- a/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml +++ b/Foxnouns.Backend/Views/Mail/_ViewStart.cshtml @@ -1,3 +1,3 @@ @{ Layout = "~/Views/Mail/Layout.cshtml"; -} +} \ No newline at end of file diff --git a/Foxnouns.Frontend/app/lib/utils.ts b/Foxnouns.Frontend/app/lib/utils.ts new file mode 100644 index 0000000..e69de29 diff --git a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx new file mode 100644 index 0000000..e69de29 From 6f79d35f11c86d938dc3b3c6b42cc4cc3775f61a Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Sep 2024 19:48:28 +0200 Subject: [PATCH 060/261] feat(frontend): add members to user page --- .../app/components/StatusIcon.tsx | 4 +- .../app/components/StatusLine.tsx | 4 +- Foxnouns.Frontend/app/lib/api/user.ts | 7 +- Foxnouns.Frontend/app/lib/utils.ts | 1 + .../app/routes/$username/MemberCard.tsx | 74 ++++++++ .../app/routes/$username/route.tsx | 45 ++++- Foxnouns.Frontend/public/locales/en.json | 171 +++++++++--------- 7 files changed, 212 insertions(+), 94 deletions(-) diff --git a/Foxnouns.Frontend/app/components/StatusIcon.tsx b/Foxnouns.Frontend/app/components/StatusIcon.tsx index d7a068e..9f2fa89 100644 --- a/Foxnouns.Frontend/app/components/StatusIcon.tsx +++ b/Foxnouns.Frontend/app/components/StatusIcon.tsx @@ -1,4 +1,4 @@ -import { CustomPreference, defaultPreferences } from "~/lib/api/user"; +import { CustomPreference, defaultPreferences, mergePreferences } from "~/lib/api/user"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; import Icon from "~/components/KeyedIcon"; @@ -9,7 +9,7 @@ export default function StatusIcon({ preferences: Record; status: string; }) { - const mergedPrefs = Object.assign({}, defaultPreferences, preferences); + const mergedPrefs = mergePreferences(preferences); const currentPref = status in mergedPrefs ? mergedPrefs[status] : defaultPreferences.missing; const id = crypto.randomUUID(); diff --git a/Foxnouns.Frontend/app/components/StatusLine.tsx b/Foxnouns.Frontend/app/components/StatusLine.tsx index b39222f..704df75 100644 --- a/Foxnouns.Frontend/app/components/StatusLine.tsx +++ b/Foxnouns.Frontend/app/components/StatusLine.tsx @@ -2,11 +2,11 @@ import { CustomPreference, defaultPreferences, FieldEntry, + mergePreferences, PreferenceSize, Pronoun, } from "~/lib/api/user"; import classNames from "classnames"; -import { ReactNode } from "react"; import StatusIcon from "~/components/StatusIcon"; import PronounLink from "~/components/PronounLink"; @@ -17,7 +17,7 @@ export default function StatusLine({ entry: FieldEntry | Pronoun; preferences: Record; }) { - const mergedPrefs = Object.assign({}, defaultPreferences, preferences); + const mergedPrefs = mergePreferences(preferences); const currentPref = entry.status in mergedPrefs ? mergedPrefs[entry.status] : defaultPreferences.missing; diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts index 0487ea6..5dc969c 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -3,6 +3,7 @@ export type PartialUser = { username: string; display_name?: string | null; avatar_url?: string | null; + custom_preferences: Record; }; export type User = PartialUser & { @@ -12,7 +13,6 @@ export type User = PartialUser & { names: FieldEntry[]; pronouns: Pronoun[]; fields: Field[]; - custom_preferences: Record; }; export type UserWithMembers = User & { members: PartialMember[] }; @@ -35,6 +35,7 @@ export type PartialMember = { avatar_url: string | null; names: FieldEntry[]; pronouns: Pronoun[]; + unlisted: boolean | null; }; export type FieldEntry = { @@ -63,6 +64,10 @@ export enum PreferenceSize { Small = "SMALL", } +export function mergePreferences(prefs: Record) { + return Object.assign({}, defaultPreferences, prefs); +} + export const defaultPreferences = Object.freeze({ favourite: { icon: "heart-fill", diff --git a/Foxnouns.Frontend/app/lib/utils.ts b/Foxnouns.Frontend/app/lib/utils.ts index e69de29..9a8d8b5 100644 --- a/Foxnouns.Frontend/app/lib/utils.ts +++ b/Foxnouns.Frontend/app/lib/utils.ts @@ -0,0 +1 @@ +export const defaultAvatarUrl = "https://pronouns.cc/default/512.webp"; diff --git a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx index e69de29..893782e 100644 --- a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx +++ b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx @@ -0,0 +1,74 @@ +import { + defaultPreferences, + mergePreferences, + PartialMember, + PartialUser, + Pronoun, +} from "~/lib/api/user"; +import { Link } from "@remix-run/react"; +import { defaultAvatarUrl } from "~/lib/utils"; +import { useTranslation } from "react-i18next"; +import { OverlayTrigger, Tooltip } from "react-bootstrap"; +import { Lock } from "react-bootstrap-icons"; + +export default function MemberCard({ user, member }: { user: PartialUser; member: PartialMember }) { + const { t } = useTranslation(); + + const mergedPrefs = mergePreferences(user.custom_preferences); + const pronouns: Pronoun[] = []; + for (const pronoun of member.pronouns) { + const pref = + pronoun.status in mergedPrefs ? mergedPrefs[pronoun.status] : defaultPreferences.missing; + if (pref.favourite) pronouns.push(pronoun); + } + + const displayedPronouns = pronouns + .map((pronoun) => { + if (pronoun.display_text) { + return pronoun.display_text; + } else { + const split = pronoun.value.split("/"); + if (split.length === 5) return split.splice(0, 2).join("/"); + return pronoun.value; + } + }) + .join(", "); + + return ( +
    + + {t("user.member-avatar-alt", + +

    + + {member.display_name ?? member.name} + {member.unlisted === true && ( + <> + + {t("user.member-hidden")} + + } + > + + + + + + )} + + {displayedPronouns && <>{displayedPronouns}} +

    +
    + ); +} diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index 6a40dad..b2d6406 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -3,11 +3,14 @@ import { Link, redirect, useLoaderData, useRouteLoaderData } from "@remix-run/re import { UserWithMembers } from "~/lib/api/user"; import serverRequest from "~/lib/request.server"; import { loader as rootLoader } from "~/root"; -import { Alert } from "react-bootstrap"; +import { Alert, Button } from "react-bootstrap"; import { Trans, useTranslation } from "react-i18next"; import { renderMarkdown } from "~/lib/markdown"; import ProfileLink from "~/components/ProfileLink"; import ProfileField from "~/components/ProfileField"; +import { PersonPlusFill } from "react-bootstrap-icons"; +import { defaultAvatarUrl } from "~/lib/utils"; +import MemberCard from "~/routes/$username/MemberCard"; export const meta: MetaFunction = ({ data }) => { const { user } = data!; @@ -39,16 +42,17 @@ export default function UserPage() { const { user } = useLoaderData(); const { meUser } = useRouteLoaderData("root") || { meUser: undefined }; + const isMeUser = meUser && meUser.id === user.id; const bio = renderMarkdown(user.bio); return ( <> - {meUser && meUser.id === user.id && ( + {isMeUser && ( You are currently viewing your public profile.
    - Edit your profile + Edit your profile
    )} @@ -56,7 +60,7 @@ export default function UserPage() {
    {t("user.avatar-alt",
    + {user.members.length > 0 || + (isMeUser && ( + <> +
    +

    + {user.member_title || t("user.heading.members")}{" "} + {/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */} + +

    +
    + {user.members.length === 0 ? ( +
    + + You don't have any members yet. +
    + Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms. +
    + You can create a new member with the "Create member" button above.{" "} + (only you can see this) +
    +
    + ) : ( +
    + {user.members.map((member, i) => ( + + ))} +
    + )} +
    + + ))} ); } diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index afebe1a..20a1fa0 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,87 +1,88 @@ { - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "An error occurred", - "more-info": "Click here for a more detailed error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up", - "theme": "Theme", - "theme-auto": "Automatic", - "theme-dark": "Dark", - "theme-light": "Light" - }, - "user": { - "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", - "avatar-alt": "Avatar for @{{username}}", - "heading": { - "names": "Names", - "pronouns": "Pronouns" - } - }, - "log-in": { - "callback": { - "title": { - "discord-success": "Log in with Discord", - "discord-register": "Register with Discord" - }, - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - }, - "welcome": { - "title": "Welcome", - "header": "Welcome to pronouns.cc!", - "blurb": "{welcome.blurb}", - "customize-profile": "Customize your profile", - "customize-profile-blurb": "{welcome.customize-profile-blurb}", - "create-members": "Create members", - "create-members-blurb": "{welcome.create-members-blurb}", - "custom-preferences": "Customize your preferences", - "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", - "profile-button": "Go to your profile" - } + "error": { + "heading": "An error occurred", + "validation": { + "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", + "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", + "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", + "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", + "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" + }, + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "An error occurred", + "more-info": "Click here for a more detailed error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up" + }, + "user": { + "member-avatar-alt": "Avatar for {{name}}", + "member-hidden": "This member is unlisted, and not shown in your public member list.", + "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", + "avatar-alt": "Avatar for @{{username}}", + "heading": { + "names": "Names", + "pronouns": "Pronouns", + "members": "Members" + }, + "create-member-button": "Create member", + "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" + }, + "log-in": { + "callback": { + "title": { + "discord-success": "Log in with Discord", + "discord-register": "Register with Discord" + }, + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up", + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else." + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + }, + "welcome": { + "title": "Welcome", + "header": "Welcome to pronouns.cc!", + "blurb": "{welcome.blurb}", + "customize-profile": "Customize your profile", + "customize-profile-blurb": "{welcome.customize-profile-blurb}", + "create-members": "Create members", + "create-members-blurb": "{welcome.create-members-blurb}", + "custom-preferences": "Customize your preferences", + "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", + "profile-button": "Go to your profile" + } } From 6c7a26c73a134ebbeb6ede3f1c4d20626e5dd5d0 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 25 Sep 2024 19:48:54 +0200 Subject: [PATCH 061/261] chore: add some names to ignored spell check words --- Foxnouns.NET.sln.DotSettings | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Foxnouns.NET.sln.DotSettings diff --git a/Foxnouns.NET.sln.DotSettings b/Foxnouns.NET.sln.DotSettings new file mode 100644 index 0000000..69e6273 --- /dev/null +++ b/Foxnouns.NET.sln.DotSettings @@ -0,0 +1,3 @@ + + True + True \ No newline at end of file From a4a62fa6b6f1e80c4e739f9702c59c3e13090dfc Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 02:14:58 +0200 Subject: [PATCH 062/261] fix(backend): invert unlisted member filter in RenderUserAsync --- Foxnouns.Backend/Services/UserRendererService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 8251611..dd266fa 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -27,7 +27,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe IEnumerable members = renderMembers ? await db.Members.Where(m => m.UserId == user.Id).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); + if (!(isSelfUser && tokenCanReadHiddenMembers)) members = members.Where(m => !m.Unlisted); var authMethods = renderAuthMethods ? await db.AuthMethods From 6ea8861da237d19c23a7babf03f29cef1ecefc69 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 02:15:54 +0200 Subject: [PATCH 063/261] feat(frontend): add member pagination --- .../app/routes/$username/route.tsx | 96 ++++++---- Foxnouns.Frontend/public/locales/en.json | 172 +++++++++--------- 2 files changed, 150 insertions(+), 118 deletions(-) diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index b2d6406..3333800 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -3,7 +3,7 @@ import { Link, redirect, useLoaderData, useRouteLoaderData } from "@remix-run/re import { UserWithMembers } from "~/lib/api/user"; import serverRequest from "~/lib/request.server"; import { loader as rootLoader } from "~/root"; -import { Alert, Button } from "react-bootstrap"; +import { Alert, Button, Pagination } from "react-bootstrap"; import { Trans, useTranslation } from "react-i18next"; import { renderMarkdown } from "~/lib/markdown"; import ProfileLink from "~/components/ProfileLink"; @@ -11,6 +11,7 @@ import ProfileField from "~/components/ProfileField"; import { PersonPlusFill } from "react-bootstrap-icons"; import { defaultAvatarUrl } from "~/lib/utils"; import MemberCard from "~/routes/$username/MemberCard"; +import { ReactNode } from "react"; export const meta: MetaFunction = ({ data }) => { const { user } = data!; @@ -34,17 +35,44 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { memberPage = 0; } + console.log(JSON.stringify(members)); + return json({ user, members, currentPage: memberPage, pageCount }); }; export default function UserPage() { const { t } = useTranslation(); - const { user } = useLoaderData(); + const { user, members, currentPage, pageCount } = useLoaderData(); const { meUser } = useRouteLoaderData("root") || { meUser: undefined }; const isMeUser = meUser && meUser.id === user.id; const bio = renderMarkdown(user.bio); + const paginationItems: ReactNode[] = []; + for (let i = 0; i < pageCount; i++) { + paginationItems.push( + + {i + 1} + , + ); + } + + const pagination = ( + + + {paginationItems} + + + ); + return ( <> {isMeUser && ( @@ -120,39 +148,43 @@ export default function UserPage() { ))}
    - {user.members.length > 0 || - (isMeUser && ( - <> -
    -

    - {user.member_title || t("user.heading.members")}{" "} - {/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */} + {(members.length > 0 || isMeUser) && ( + <> +
    +

    + {user.member_title || t("user.heading.members")}{" "} + {isMeUser && ( + // @ts-expect-error using as=Link causes an error here, even though it runs completely fine -

    -
    - {user.members.length === 0 ? ( -
    - - You don't have any members yet. -
    - Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms. -
    - You can create a new member with the "Create member" button above.{" "} - (only you can see this) -
    -
    - ) : ( -
    - {user.members.map((member, i) => ( - - ))} -
    - )} -
    - - ))} + )} + + {pageCount > 1 && pagination} +
    + {members.length === 0 ? ( +
    + + You don't have any members yet. +
    + Members are sub-profiles that can have their own avatar, names, pronouns, and + preferred terms. +
    + You can create a new member with the "Create member" button above.{" "} + (only you can see this) +
    +
    + ) : ( +
    + {members.map((member, i) => ( + + ))} +
    + )} +
    + {pageCount > 1 && pagination} + + )} ); } diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 20a1fa0..2a414ba 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,88 +1,88 @@ { - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "An error occurred", - "more-info": "Click here for a more detailed error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up" - }, - "user": { - "member-avatar-alt": "Avatar for {{name}}", - "member-hidden": "This member is unlisted, and not shown in your public member list.", - "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", - "avatar-alt": "Avatar for @{{username}}", - "heading": { - "names": "Names", - "pronouns": "Pronouns", - "members": "Members" - }, - "create-member-button": "Create member", - "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" - }, - "log-in": { - "callback": { - "title": { - "discord-success": "Log in with Discord", - "discord-register": "Register with Discord" - }, - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - }, - "welcome": { - "title": "Welcome", - "header": "Welcome to pronouns.cc!", - "blurb": "{welcome.blurb}", - "customize-profile": "Customize your profile", - "customize-profile-blurb": "{welcome.customize-profile-blurb}", - "create-members": "Create members", - "create-members-blurb": "{welcome.create-members-blurb}", - "custom-preferences": "Customize your preferences", - "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", - "profile-button": "Go to your profile" - } + "error": { + "heading": "An error occurred", + "validation": { + "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", + "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", + "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", + "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", + "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" + }, + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "An error occurred", + "more-info": "Click here for a more detailed error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up" + }, + "user": { + "member-avatar-alt": "Avatar for {{name}}", + "member-hidden": "This member is unlisted, and not shown in your public member list.", + "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", + "avatar-alt": "Avatar for @{{username}}", + "heading": { + "names": "Names", + "pronouns": "Pronouns", + "members": "Members" + }, + "create-member-button": "Create member", + "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" + }, + "log-in": { + "callback": { + "title": { + "discord-success": "Log in with Discord", + "discord-register": "Register with Discord" + }, + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up", + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else." + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + }, + "welcome": { + "title": "Welcome", + "header": "Welcome to pronouns.cc!", + "blurb": "{welcome.blurb}", + "customize-profile": "Customize your profile", + "customize-profile-blurb": "{welcome.customize-profile-blurb}", + "create-members": "Create members", + "create-members-blurb": "{welcome.create-members-blurb}", + "custom-preferences": "Customize your preferences", + "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", + "profile-button": "Go to your profile" + } } From df93f28273177674e75a2a5a90d85745aa59f14b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 15:08:08 +0200 Subject: [PATCH 064/261] feat(backend): add short IDs to models --- .../Controllers/MembersController.cs | 6 +- Foxnouns.Backend/Database/DatabaseContext.cs | 4 + .../20240926124950_AddSids.Designer.cs | 452 +++++++++++++++++ .../Migrations/20240926124950_AddSids.cs | 98 ++++ ...20240926130208_NonNullableSids.Designer.cs | 458 ++++++++++++++++++ .../20240926130208_NonNullableSids.cs | 57 +++ .../DatabaseContextModelSnapshot.cs | 26 + Foxnouns.Backend/Database/Models/Member.cs | 1 + Foxnouns.Backend/Database/Models/User.cs | 2 + 9 files changed, 1101 insertions(+), 3 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.Designer.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 1ffc928..46fe51e 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -50,9 +50,9 @@ public class MembersController( ("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName)), ("bio", ValidationUtils.ValidateBio(req.Bio)), ("avatar", ValidationUtils.ValidateAvatar(req.Avatar)), - ..ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), - ..ValidationUtils.ValidateFieldEntries(req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names"), - ..ValidationUtils.ValidatePronouns(req.Pronouns?.ToArray(), CurrentUser!.CustomPreferences) + .. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), + .. ValidationUtils.ValidateFieldEntries(req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names"), + .. ValidationUtils.ValidatePronouns(req.Pronouns?.ToArray(), CurrentUser!.CustomPreferences) ]); var member = new Member diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 70477f2..b46079b 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -60,15 +60,19 @@ public class DatabaseContext : DbContext 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(k => k.Key).IsUnique(); + 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"); diff --git a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs new file mode 100644 index 0000000..144b244 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs @@ -0,0 +1,452 @@ +// +using System; +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("20240926124950_AddSids")] + partial class AddSids + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .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>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + + b.Property("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") + .HasColumnType("text") + .HasColumnName("sid"); + + b.Property("Unlisted") + .HasColumnType("boolean") + .HasColumnName("unlisted"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("pk_members"); + + 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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (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>("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("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") + .HasColumnType("text") + .HasColumnName("sid"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + 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.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.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.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs new file mode 100644 index 0000000..e0af60f --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs @@ -0,0 +1,98 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + 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.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.Designer.cs new file mode 100644 index 0000000..8c0c656 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.Designer.cs @@ -0,0 +1,458 @@ +// +using System; +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("20240926130208_NonNullableSids")] + partial class NonNullableSids + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .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>("Fields") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("fields"); + + b.Property("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("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.TemporaryKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Expires") + .HasColumnType("timestamp with time zone") + .HasColumnName("expires"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text") + .HasColumnName("key"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text") + .HasColumnName("value"); + + b.HasKey("Id") + .HasName("pk_temporary_keys"); + + b.HasIndex("Key") + .IsUnique() + .HasDatabaseName("ix_temporary_keys_key"); + + b.ToTable("temporary_keys", (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>("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("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("Username") + .IsRequired() + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_users"); + + 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.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.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.Navigation("AuthMethods"); + + b.Navigation("Members"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs new file mode 100644 index 0000000..4f01d1d --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using NodaTime; + +#nullable disable + +namespace Foxnouns.Backend.Database.Migrations +{ + /// + 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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 36ca955..dd457cf 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -175,6 +175,13 @@ namespace Foxnouns.Backend.Database.Migrations .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"); @@ -186,6 +193,10 @@ namespace Foxnouns.Backend.Database.Migrations b.HasKey("Id") .HasName("pk_members"); + b.HasIndex("Sid") + .IsUnique() + .HasDatabaseName("ix_members_sid"); + b.HasIndex("UserId", "Name") .IsUnique() .HasDatabaseName("ix_members_user_id_name"); @@ -314,6 +325,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("last_active"); + b.Property("LastSidReroll") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_sid_reroll"); + b.Property("Links") .IsRequired() .HasColumnType("text[]") @@ -350,6 +365,13 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("jsonb") .HasColumnName("settings"); + b.Property("Sid") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("text") + .HasColumnName("sid") + .HasDefaultValueSql("find_free_user_sid()"); + b.Property("Username") .IsRequired() .HasColumnType("text") @@ -358,6 +380,10 @@ namespace Foxnouns.Backend.Database.Migrations b.HasKey("Id") .HasName("pk_users"); + b.HasIndex("Sid") + .IsUnique() + .HasDatabaseName("ix_users_sid"); + b.HasIndex("Username") .IsUnique() .HasDatabaseName("ix_users_username"); diff --git a/Foxnouns.Backend/Database/Models/Member.cs b/Foxnouns.Backend/Database/Models/Member.cs index cd0d9cd..d20d374 100644 --- a/Foxnouns.Backend/Database/Models/Member.cs +++ b/Foxnouns.Backend/Database/Models/Member.cs @@ -3,6 +3,7 @@ namespace Foxnouns.Backend.Database.Models; public class Member : BaseModel { public required string Name { get; set; } + public string Sid { get; set; } = string.Empty; public string? DisplayName { get; set; } public string? Bio { get; set; } public string? Avatar { get; set; } diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index de48944..112e560 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -8,6 +8,7 @@ namespace Foxnouns.Backend.Database.Models; public class User : BaseModel { public required string Username { get; set; } + public string Sid { get; set; } = string.Empty; public string? DisplayName { get; set; } public string? Bio { get; set; } public string? MemberTitle { get; set; } @@ -28,6 +29,7 @@ public class User : BaseModel 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; } From e7e493708257f469525bf4a6054e7fa7d769ceed Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 15:26:37 +0200 Subject: [PATCH 065/261] fix(frontend): remove debug console.logs --- Foxnouns.Frontend/app/root.tsx | 2 -- Foxnouns.Frontend/app/routes/$username/route.tsx | 2 -- Foxnouns.Frontend/app/routes/auth.log-in/route.tsx | 2 -- 3 files changed, 6 deletions(-) diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index eadd87c..b622f3f 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -99,8 +99,6 @@ export function ErrorBoundary() { const error: any = useRouteError(); const { t } = useTranslation(); - console.log(error); - const errorElem = "code" in error && "message" in error ? ( diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index 3333800..c19b5e7 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -35,8 +35,6 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { memberPage = 0; } - console.log(JSON.stringify(members)); - return json({ user, members, currentPage: memberPage, pageCount }); }; diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index a974de9..09a6675 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -53,8 +53,6 @@ export const action = async ({ request }: ActionFunctionArgs) => { const email = body.get("email") as string | null; const password = body.get("password") as string | null; - console.log(email, password); - try { const resp = await serverRequest("POST", "/auth/email/login", { body: { email, password }, From e76c634738afe40b8ac830f77907e53e894c59e2 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 15:26:52 +0200 Subject: [PATCH 066/261] feat(backend): return short IDs --- Foxnouns.Backend/Services/MemberRendererService.cs | 8 +++++--- Foxnouns.Backend/Services/UserRendererService.cs | 6 ++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index ef7b923..625e8b8 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -29,15 +29,15 @@ public class MemberRendererService(DatabaseContext db, Config config) var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); return new MemberResponse( - member.Id, member.Name, member.DisplayName, member.Bio, + member.Id, member.Sid, member.Name, member.DisplayName, member.Bio, AvatarUrlFor(member), member.Links, member.Names, member.Pronouns, member.Fields, RenderPartialUser(member.User), renderUnlisted ? member.Unlisted : null); } private UserRendererService.PartialUser RenderPartialUser(User user) => - new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user), user.CustomPreferences); + 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.Name, + public PartialMember RenderPartialMember(Member member, bool renderUnlisted = false) => new(member.Id, member.Sid, member.Name, member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns, renderUnlisted ? member.Unlisted : null); @@ -49,6 +49,7 @@ public class MemberRendererService(DatabaseContext db, Config config) public record PartialMember( Snowflake Id, + string Sid, string Name, string? DisplayName, string? Bio, @@ -60,6 +61,7 @@ public class MemberRendererService(DatabaseContext db, Config config) public record MemberResponse( Snowflake Id, + string Sid, string Name, string? DisplayName, string? Bio, diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index dd266fa..5a62a1a 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -37,7 +37,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe : []; return new UserResponse( - user.Id, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, + user.Id, user.Sid, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderAuthMethods @@ -52,13 +52,14 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe } public PartialUser RenderPartialUser(User user) => - new(user.Id, user.Username, user.DisplayName, AvatarUrlFor(user), user.CustomPreferences); + new(user.Id, user.Sid, user.Username, user.DisplayName, AvatarUrlFor(user), user.CustomPreferences); private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; public record UserResponse( Snowflake Id, + string Sid, string Username, string? DisplayName, string? Bio, @@ -92,6 +93,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe public record PartialUser( Snowflake Id, + string Sid, string Username, string? DisplayName, string? AvatarUrl, From b5f9ef9bd6c4e47a2c8151f0b4ea473efab6ed9e Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 16:38:43 +0200 Subject: [PATCH 067/261] feat(backend): add short ID reroll endpoints --- .../Controllers/MembersController.cs | 25 ++++++++++++++++++- .../Controllers/UsersController.cs | 22 +++++++++++++++- Foxnouns.Backend/Database/DatabaseContext.cs | 16 ++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 46fe51e..59bec37 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -9,6 +9,7 @@ using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Foxnouns.Backend.Controllers; @@ -19,7 +20,8 @@ public class MembersController( MemberRendererService memberRenderer, ISnowflakeGenerator snowflakeGenerator, ObjectStorageService objectStorageService, - IQueue queue) : ApiControllerBase + IQueue queue, + IClock clock) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); @@ -114,4 +116,25 @@ public class MembersController( List? Names, List? Pronouns, List? Fields); + + [HttpPost("/api/v2/users/@me/members/{memberRef}/reroll-sid")] + [Authorize("member.update")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task RerollSidAsync(string memberRef) + { + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + + var minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); + if (CurrentUser!.LastSidReroll > minTimeAgo) + throw new ApiError.BadRequest("Cannot reroll short ID yet"); + + await db.Members.Where(m => m.Id == member.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(m => m.Sid, _ => db.FindFreeMemberSid())); + + // Re-fetch member so we can be sure the sid is correct + var updatedMember = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + + return Ok(memberRenderer.RenderMember(updatedMember, CurrentToken)); + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index bb3417c..7d1177e 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -8,6 +8,7 @@ using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Foxnouns.Backend.Controllers; @@ -16,7 +17,8 @@ public class UsersController( DatabaseContext db, UserRendererService userRenderer, ISnowflakeGenerator snowflakeGenerator, - IQueue queue) : ApiControllerBase + IQueue queue, + IClock clock) : ApiControllerBase { [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] @@ -213,4 +215,22 @@ public class UsersController( { public bool? DarkMode { get; init; } } + + [HttpPost("@me/reroll-sid")] + [Authorize("user.update")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task RerollSidAsync() + { + var minTimeAgo = clock.GetCurrentInstant() - Duration.FromHours(1); + if (CurrentUser!.LastSidReroll > minTimeAgo) + throw new ApiError.BadRequest("Cannot reroll short ID yet"); + + await db.Users.Where(u => u.Id == CurrentUser.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(u => u.Sid, _ => db.FindFreeUserSid()) + .SetProperty(u => u.LastSidReroll, _ => clock.GetCurrentInstant())); + + var user = await db.ResolveUserAsync(CurrentUser.Id); + return Ok(await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, renderMembers: false)); + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index b46079b..e907abd 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -76,7 +76,23 @@ public class DatabaseContext : DbContext 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.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeUserSid))!) + .HasName("find_free_user_sid"); + + modelBuilder.HasDbFunction(typeof(DatabaseContext).GetMethod(nameof(FindFreeMemberSid))!) + .HasName("find_free_member_sid"); } + + /// + /// 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")] From e83895255ef5bf1376b6e9cfd7551ac21023e009 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 17:09:27 +0200 Subject: [PATCH 068/261] fix(backend): return last_sid_reroll in API, update last sid reroll + last active correctly --- Foxnouns.Backend/Controllers/MembersController.cs | 9 +++++++-- Foxnouns.Backend/Controllers/UsersController.cs | 4 +++- Foxnouns.Backend/Services/UserRendererService.cs | 10 +++++++--- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 59bec37..44bb9ce 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -128,13 +128,18 @@ public class MembersController( 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())); - // Re-fetch member so we can be sure the sid is correct - var updatedMember = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + await db.Users.Where(u => u.Id == CurrentUser.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(u => u.LastSidReroll, clock.GetCurrentInstant()) + .SetProperty(u => u.LastActive, clock.GetCurrentInstant())); + // Re-fetch member to fetch the new sid + var updatedMember = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); return Ok(memberRenderer.RenderMember(updatedMember, CurrentToken)); } } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 7d1177e..fddd798 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -225,10 +225,12 @@ public class UsersController( 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.LastSidReroll, clock.GetCurrentInstant()) + .SetProperty(u => u.LastActive, clock.GetCurrentInstant())); var user = await db.ResolveUserAsync(CurrentUser.Id); return Ok(await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, renderMembers: false)); diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 5a62a1a..0f8fe2e 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -37,7 +37,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe : []; return new UserResponse( - user.Id, user.Sid, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, + user.Id, user.Sid, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), + user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderAuthMethods @@ -47,7 +48,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe )) : null, tokenHidden ? user.ListHidden : null, - tokenHidden ? user.LastActive : null + tokenHidden ? user.LastActive : null, + tokenHidden ? user.LastSidReroll : null ); } @@ -77,7 +79,9 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? MemberListHidden, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - Instant? LastActive + Instant? LastActive, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + Instant? LastSidReroll ); public record AuthenticationMethodResponse( From 39b091758521ef16c236e81d3dfbf9be2f6f5731 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 17:11:52 +0200 Subject: [PATCH 069/261] add script to prune designer files from migrations, add README with acknowledgements --- .../20240527132444_Init.Designer.cs | 412 -------------- .../Migrations/20240527132444_Init.cs | 3 + ...20240528125310_AddApplications.Designer.cs | 470 --------------- .../20240528125310_AddApplications.cs | 3 + .../20240528145744_AddListHidden.Designer.cs | 474 ---------------- .../20240528145744_AddListHidden.cs | 3 + .../20240604142522_AddPassword.Designer.cs | 478 ---------------- .../Migrations/20240604142522_AddPassword.cs | 3 + ...611225328_AddTemporaryKeyCache.Designer.cs | 511 ----------------- .../20240611225328_AddTemporaryKeyCache.cs | 3 + ...240712233806_AddUserLastActive.Designer.cs | 515 ----------------- .../20240712233806_AddUserLastActive.cs | 3 + .../20240713000719_AddDeleted.Designer.cs | 528 ----------------- .../Migrations/20240713000719_AddDeleted.cs | 3 + ...821210355_AddCustomPreferences.Designer.cs | 535 ------------------ .../20240821210355_AddCustomPreferences.cs | 3 + ...20240905191709_AddUserSettings.Designer.cs | 432 -------------- .../20240905191709_AddUserSettings.cs | 3 + .../20240926124950_AddSids.Designer.cs | 452 --------------- .../Migrations/20240926124950_AddSids.cs | 3 + ...20240926130208_NonNullableSids.Designer.cs | 458 --------------- .../20240926130208_NonNullableSids.cs | 3 + .../Database/prune-designer-cs-files.sh | 30 + README.md | 35 ++ 24 files changed, 98 insertions(+), 5265 deletions(-) delete mode 100644 Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs delete mode 100644 Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.Designer.cs create mode 100644 Foxnouns.Backend/Database/prune-designer-cs-files.sh create mode 100644 README.md diff --git a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs deleted file mode 100644 index 5274ef0..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.Designer.cs +++ /dev/null @@ -1,412 +0,0 @@ -// -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 4a876cf..f501fe0 100644 --- a/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs +++ b/Foxnouns.Backend/Database/Migrations/20240527132444_Init.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; using NodaTime; #nullable disable @@ -6,6 +7,8 @@ using NodaTime; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240527132444_Init")] public partial class Init : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.Designer.cs deleted file mode 100644 index 2b660a0..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.Designer.cs +++ /dev/null @@ -1,470 +0,0 @@ -// -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 d6694cd..d8486f4 100644 --- a/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs +++ b/Foxnouns.Backend/Database/Migrations/20240528125310_AddApplications.cs @@ -1,10 +1,13 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; #nullable disable namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240528125310_AddApplications")] public partial class AddApplications : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs deleted file mode 100644 index 07d8181..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.Designer.cs +++ /dev/null @@ -1,474 +0,0 @@ -// -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 1b40552..aa9483c 100644 --- a/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.cs +++ b/Foxnouns.Backend/Database/Migrations/20240528145744_AddListHidden.cs @@ -1,10 +1,13 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; #nullable disable namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240528145744_AddListHidden")] public partial class AddListHidden : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs deleted file mode 100644 index 2c92566..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.Designer.cs +++ /dev/null @@ -1,478 +0,0 @@ -// -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 23671a8..02add03 100644 --- a/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs +++ b/Foxnouns.Backend/Database/Migrations/20240604142522_AddPassword.cs @@ -1,10 +1,13 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; #nullable disable namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240604142522_AddPassword")] public partial class AddPassword : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs deleted file mode 100644 index af4f52a..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.Designer.cs +++ /dev/null @@ -1,511 +0,0 @@ -// -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("20240611225328_AddTemporaryKeyCache")] - partial class AddTemporaryKeyCache - { - /// - 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.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (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/20240611225328_AddTemporaryKeyCache.cs b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs index ccf736b..5976175 100644 --- a/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs +++ b/Foxnouns.Backend/Database/Migrations/20240611225328_AddTemporaryKeyCache.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; using NodaTime; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; @@ -7,6 +8,8 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240611225328_AddTemporaryKeyCache")] public partial class AddTemporaryKeyCache : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs deleted file mode 100644 index 0430058..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.Designer.cs +++ /dev/null @@ -1,515 +0,0 @@ -// -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("20240712233806_AddUserLastActive")] - partial class AddUserLastActive - { - /// - 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.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (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("LastActive") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_active"); - - 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/20240712233806_AddUserLastActive.cs b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs index 8d1392c..ab928be 100644 --- a/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs +++ b/Foxnouns.Backend/Database/Migrations/20240712233806_AddUserLastActive.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; using NodaTime; #nullable disable @@ -6,6 +7,8 @@ using NodaTime; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240712233806_AddUserLastActive")] public partial class AddUserLastActive : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs deleted file mode 100644 index 8c2dcdc..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.Designer.cs +++ /dev/null @@ -1,528 +0,0 @@ -// -using System; -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("20240713000719_AddDeleted")] - partial class AddDeleted - { - /// - 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.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (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("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("LastActive") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_active"); - - 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/20240713000719_AddDeleted.cs b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs index a14ad5c..08336cc 100644 --- a/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs +++ b/Foxnouns.Backend/Database/Migrations/20240713000719_AddDeleted.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; using NodaTime; #nullable disable @@ -6,6 +7,8 @@ using NodaTime; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240713000719_AddDeleted")] public partial class AddDeleted : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs deleted file mode 100644 index c08b9fb..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.Designer.cs +++ /dev/null @@ -1,535 +0,0 @@ -// -using System; -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("20240821210355_AddCustomPreferences")] - partial class AddCustomPreferences - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") - .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.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (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>("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("LastActive") - .HasColumnType("timestamp with time zone") - .HasColumnName("last_active"); - - 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/20240821210355_AddCustomPreferences.cs b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs index 7d68ad7..96bf5c7 100644 --- a/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs +++ b/Foxnouns.Backend/Database/Migrations/20240821210355_AddCustomPreferences.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.EntityFrameworkCore.Infrastructure; using System.Collections.Generic; using Foxnouns.Backend.Database.Models; using Microsoft.EntityFrameworkCore.Migrations; @@ -8,6 +9,8 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240821210355_AddCustomPreferences")] public partial class AddCustomPreferences : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs deleted file mode 100644 index 45b3a53..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.Designer.cs +++ /dev/null @@ -1,432 +0,0 @@ -// -using System; -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("20240905191709_AddUserSettings")] - partial class AddUserSettings - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") - .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>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("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("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.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (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>("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("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("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.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.Navigation("AuthMethods"); - - b.Navigation("Members"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs index 49c5bd1..ffd8fae 100644 --- a/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs +++ b/Foxnouns.Backend/Database/Migrations/20240905191709_AddUserSettings.cs @@ -1,4 +1,5 @@ using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -6,6 +7,8 @@ using Microsoft.EntityFrameworkCore.Migrations; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240905191709_AddUserSettings")] public partial class AddUserSettings : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs deleted file mode 100644 index 144b244..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.Designer.cs +++ /dev/null @@ -1,452 +0,0 @@ -// -using System; -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("20240926124950_AddSids")] - partial class AddSids - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") - .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>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("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") - .HasColumnType("text") - .HasColumnName("sid"); - - b.Property("Unlisted") - .HasColumnType("boolean") - .HasColumnName("unlisted"); - - b.Property("UserId") - .HasColumnType("bigint") - .HasColumnName("user_id"); - - b.HasKey("Id") - .HasName("pk_members"); - - 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.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (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>("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("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") - .HasColumnType("text") - .HasColumnName("sid"); - - b.Property("Username") - .IsRequired() - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("Id") - .HasName("pk_users"); - - 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.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.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.Navigation("AuthMethods"); - - b.Navigation("Members"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs index e0af60f..6d8c22e 100644 --- a/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs +++ b/Foxnouns.Backend/Database/Migrations/20240926124950_AddSids.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; using NodaTime; #nullable disable @@ -6,6 +7,8 @@ using NodaTime; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240926124950_AddSids")] public partial class AddSids : Migration { /// diff --git a/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.Designer.cs b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.Designer.cs deleted file mode 100644 index 8c0c656..0000000 --- a/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.Designer.cs +++ /dev/null @@ -1,458 +0,0 @@ -// -using System; -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("20240926130208_NonNullableSids")] - partial class NonNullableSids - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") - .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>("Fields") - .IsRequired() - .HasColumnType("jsonb") - .HasColumnName("fields"); - - b.Property("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("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.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (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>("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("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("Username") - .IsRequired() - .HasColumnType("text") - .HasColumnName("username"); - - b.HasKey("Id") - .HasName("pk_users"); - - 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.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.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.Navigation("AuthMethods"); - - b.Navigation("Members"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs index 4f01d1d..ed06ec5 100644 --- a/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs +++ b/Foxnouns.Backend/Database/Migrations/20240926130208_NonNullableSids.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; using NodaTime; #nullable disable @@ -6,6 +7,8 @@ using NodaTime; namespace Foxnouns.Backend.Database.Migrations { /// + [DbContext(typeof(DatabaseContext))] + [Migration("20240926130208_NonNullableSids")] public partial class NonNullableSids : Migration { /// diff --git a/Foxnouns.Backend/Database/prune-designer-cs-files.sh b/Foxnouns.Backend/Database/prune-designer-cs-files.sh new file mode 100644 index 0000000..41b96cc --- /dev/null +++ b/Foxnouns.Backend/Database/prune-designer-cs-files.sh @@ -0,0 +1,30 @@ +#!/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/README.md b/README.md new file mode 100644 index 0000000..de5b9b8 --- /dev/null +++ b/README.md @@ -0,0 +1,35 @@ +# Foxnouns.NET + +Rewrite of pronouns.cc's codebase in C#, using Remix 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) From a70078995b100aef502405860e48e97816437fc8 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 20:15:04 +0200 Subject: [PATCH 070/261] feat(backend): add pride flag models --- .../Controllers/FlagsController.cs | 28 ++++ Foxnouns.Backend/Database/DatabaseContext.cs | 7 + .../Migrations/20240926180037_AddFlags.cs | 129 +++++++++++++++ .../DatabaseContextModelSnapshot.cs | 148 ++++++++++++++++++ Foxnouns.Backend/Database/Models/Member.cs | 2 + Foxnouns.Backend/Database/Models/PrideFlag.cs | 25 +++ Foxnouns.Backend/Database/Models/User.cs | 3 + .../Services/MemberRendererService.cs | 2 + .../Services/UserRendererService.cs | 2 + 9 files changed, 346 insertions(+) create mode 100644 Foxnouns.Backend/Controllers/FlagsController.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs create mode 100644 Foxnouns.Backend/Database/Models/PrideFlag.cs diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs new file mode 100644 index 0000000..b08dcef --- /dev/null +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -0,0 +1,28 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Middleware; +using Foxnouns.Backend.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace Foxnouns.Backend.Controllers; + +[Route("/api/v2/users/@me/flags")] +public class FlagsController(DatabaseContext db, UserRendererService userRenderer) : ApiControllerBase +{ + [HttpGet] + [Authorize("identify")] + [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] + public async Task GetFlagsAsync(CancellationToken ct = default) + { + var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); + + return Ok(flags.Select(f => new PrideFlagResponse( + f.Id, userRenderer.ImageUrlFor(f), f.Name, f.Description))); + } + + private record PrideFlagResponse( + Snowflake Id, + string ImageUrl, + string Name, + string? Description); +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index e907abd..81866fe 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -21,6 +21,10 @@ public class DatabaseContext : DbContext public DbSet Tokens { get; set; } public DbSet Applications { get; set; } public DbSet TemporaryKeys { get; set; } + + public DbSet PrideFlags { get; set; } + public DbSet UserFlags { get; set; } + public DbSet MemberFlags { get; set; } public DatabaseContext(Config config, ILoggerFactory? loggerFactory) { @@ -77,6 +81,9 @@ public class DatabaseContext : DbContext 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"); diff --git a/Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs b/Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs new file mode 100644 index 0000000..ae59b0b --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20240926180037_AddFlags.cs @@ -0,0 +1,129 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Infrastructure; +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index dd457cf..e1e05c2 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -204,6 +204,68 @@ 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.PrideFlag", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("Hash") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hash"); + + 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("UserId") + .HasDatabaseName("ix_pride_flags_user_id"); + + b.ToTable("pride_flags", (string)null); + }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b => { b.Property("Id") @@ -391,6 +453,35 @@ 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.AuthMethod", b => { b.HasOne("Foxnouns.Backend.Database.Models.FediverseApplication", "FediverseApplication") @@ -422,6 +513,35 @@ namespace Foxnouns.Backend.Database.Migrations 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.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.Token", b => { b.HasOne("Foxnouns.Backend.Database.Models.Application", "Application") @@ -443,11 +563,39 @@ namespace Foxnouns.Backend.Database.Migrations 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.User", b => { b.Navigation("AuthMethods"); + b.Navigation("Flags"); + b.Navigation("Members"); + + b.Navigation("ProfileFlags"); }); #pragma warning restore 612, 618 } diff --git a/Foxnouns.Backend/Database/Models/Member.cs b/Foxnouns.Backend/Database/Models/Member.cs index d20d374..ceaf84a 100644 --- a/Foxnouns.Backend/Database/Models/Member.cs +++ b/Foxnouns.Backend/Database/Models/Member.cs @@ -14,6 +14,8 @@ public class Member : BaseModel public List Pronouns { get; set; } = []; public List Fields { get; set; } = []; + 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/PrideFlag.cs b/Foxnouns.Backend/Database/Models/PrideFlag.cs new file mode 100644 index 0000000..b7f91cf --- /dev/null +++ b/Foxnouns.Backend/Database/Models/PrideFlag.cs @@ -0,0 +1,25 @@ +namespace Foxnouns.Backend.Database.Models; + +public class PrideFlag : BaseModel +{ + public required Snowflake UserId { get; init; } + public required string Hash { get; init; } + 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!; +} \ No newline at end of file diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index 112e560..2328fbe 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -21,6 +21,9 @@ public class User : BaseModel 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 diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 625e8b8..11416f0 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -47,6 +47,8 @@ public class MemberRendererService(DatabaseContext db, Config config) private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; + private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; + public record PartialMember( Snowflake Id, string Sid, diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 0f8fe2e..688214d 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -58,6 +58,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; + + public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; public record UserResponse( Snowflake Id, From ff2ba1fb1bd77153fb14c6559bd3430e748d7f30 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 22:25:47 +0200 Subject: [PATCH 071/261] fix(backend): correctly hash images --- Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs | 9 +++++++-- Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs | 2 +- Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index 7c39aa4..c3c3c04 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -23,7 +23,7 @@ public static class AvatarObjectExtensions CancellationToken ct = default) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); - public static async Task ConvertBase64UriToAvatar(this string uri) + public static async Task ConvertBase64UriToImage(this string uri, int size, bool crop) { if (!uri.StartsWith("data:image/")) throw new ArgumentException("Not a data URI", nameof(uri)); @@ -40,7 +40,12 @@ public static class AvatarObjectExtensions var image = Image.Load(rawImage); var processor = new ResizeProcessor( - new ResizeOptions { Size = new Size(512), Mode = ResizeMode.Crop, Position = AnchorPositionMode.Center }, + new ResizeOptions + { + Size = new Size(size), + Mode = crop ? ResizeMode.Crop : ResizeMode.Max, + Position = AnchorPositionMode.Center + }, image.Size ); diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs index 3beff48..7ed801a 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -31,7 +31,7 @@ public class MemberAvatarUpdateInvocable(DatabaseContext db, ObjectStorageServic try { - var image = await newAvatar.ConvertBase64UriToAvatar(); + var image = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); image.Seek(0, SeekOrigin.Begin); var prevHash = member.Avatar; diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index d1abd42..44dd312 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -31,7 +31,7 @@ public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService try { - var image = await newAvatar.ConvertBase64UriToAvatar(); + var image = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); image.Seek(0, SeekOrigin.Begin); var prevHash = user.Avatar; From 14e6e35cb7aa4b7ebc1754582ee1036d51438a5b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 22:26:40 +0200 Subject: [PATCH 072/261] feat(backend): add create flag endpoint and job --- .../Controllers/FlagsController.cs | 32 +++++++++-- .../Extensions/WebApplicationExtensions.cs | 3 +- Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 53 +++++++++++++++++++ Foxnouns.Backend/Jobs/Payloads.cs | 4 +- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 Foxnouns.Backend/Jobs/CreateFlagInvocable.cs diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index b08dcef..7bf20e5 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -1,4 +1,7 @@ +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 Microsoft.AspNetCore.Mvc; @@ -7,7 +10,11 @@ using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users/@me/flags")] -public class FlagsController(DatabaseContext db, UserRendererService userRenderer) : ApiControllerBase +public class FlagsController( + DatabaseContext db, + UserRendererService userRenderer, + ISnowflakeGenerator snowflakeGenerator, + IQueue queue) : ApiControllerBase { [HttpGet] [Authorize("identify")] @@ -16,10 +23,29 @@ public class FlagsController(DatabaseContext db, UserRendererService userRendere { var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); - return Ok(flags.Select(f => new PrideFlagResponse( - f.Id, userRenderer.ImageUrlFor(f), f.Name, f.Description))); + return Ok(flags.Select(ToResponse)); } + [HttpPost] + [Authorize("user.update")] + [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] + public IActionResult CreateFlag([FromBody] CreateFlagRequest req) + { + var id = snowflakeGenerator.GenerateSnowflake(); + + queue.QueueInvocableWithPayload( + new CreateFlagPayload(id, CurrentUser!.Id, req.Name, req.Image, req.Description)); + + return Accepted(new CreateFlagResponse(id, req.Name, req.Description)); + } + + public record CreateFlagRequest(string Name, string Image, string? Description); + + public record CreateFlagResponse(Snowflake Id, string Name, string? Description); + + private PrideFlagResponse ToResponse(PrideFlag flag) => + new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description); + private record PrideFlagResponse( Snowflake Id, string ImageUrl, diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index a5b2af6..e249fd7 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -99,7 +99,8 @@ public static class WebApplicationExtensions .AddHostedService() // Transient jobs .AddTransient() - .AddTransient(); + .AddTransient() + .AddTransient(); if (!config.Logging.EnableMetrics) services.AddHostedService(); diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs new file mode 100644 index 0000000..6f69794 --- /dev/null +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -0,0 +1,53 @@ +using System.Security.Cryptography; +using Coravel.Invocable; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; +using Foxnouns.Backend.Services; + +namespace Foxnouns.Backend.Jobs; + +public class CreateFlagInvocable(DatabaseContext db, ObjectStorageService objectStorageService, ILogger logger) + : IInvocable, IInvocableWithPayload +{ + private readonly ILogger _logger = logger.ForContext(); + public required CreateFlagPayload Payload { get; set; } + + public async Task Invoke() + { + _logger.Information("Creating flag {FlagId} for user {UserId} with image data length {DataLength}", Payload.Id, + Payload.UserId, Payload.ImageData.Length); + + try + { + var image = await Payload.ImageData.ConvertBase64UriToImage(size: 256, crop: false); + image.Seek(0, SeekOrigin.Begin); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); + image.Seek(0, SeekOrigin.Begin); + + await objectStorageService.PutObjectAsync(Path(hash), image, "image/webp"); + + var flag = new PrideFlag + { + Id = Payload.Id, + UserId = Payload.UserId, + Hash = hash, + Name = Payload.Name, + Description = Payload.Description + }; + db.Add(flag); + + await db.SaveChangesAsync(); + + _logger.Information("Uploaded flag {FlagId} with hash {Hash}", flag.Id, flag.Hash); + } + catch (ArgumentException ae) + { + _logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", Payload.Id, ae.Message); + } + + throw new NotImplementedException(); + } + + private static string Path(string hash) => $"flags/{hash}.webp"; +} \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/Payloads.cs b/Foxnouns.Backend/Jobs/Payloads.cs index f28254a..672f6d6 100644 --- a/Foxnouns.Backend/Jobs/Payloads.cs +++ b/Foxnouns.Backend/Jobs/Payloads.cs @@ -2,4 +2,6 @@ using Foxnouns.Backend.Database; namespace Foxnouns.Backend.Jobs; -public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); \ No newline at end of file +public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); + +public record CreateFlagPayload(Snowflake Id, Snowflake UserId, string Name, string ImageData, string? Description); \ No newline at end of file From e20a7d34659e4389267dc12efeb0a6dd482e2266 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 22:30:24 +0200 Subject: [PATCH 073/261] fix(backend): *actually* correctly hash images --- Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs | 10 ++++++++-- Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 6 +----- Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs | 4 +--- Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs | 3 +-- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index c3c3c04..063f835 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using Foxnouns.Backend.Database; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Services; @@ -23,7 +24,7 @@ public static class AvatarObjectExtensions CancellationToken ct = default) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); - public static async Task ConvertBase64UriToImage(this string uri, int size, bool crop) + public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(this string uri, int size, bool crop) { if (!uri.StartsWith("data:image/")) throw new ArgumentException("Not a data URI", nameof(uri)); @@ -53,6 +54,11 @@ public static class AvatarObjectExtensions var stream = new MemoryStream(64 * 1024); await image.SaveAsync(stream, new WebpEncoder { Quality = 95, NearLossless = false }); - return stream; + + stream.Seek(0, SeekOrigin.Begin); + var hash = Convert.ToHexString(await SHA256.HashDataAsync(stream)).ToLower(); + stream.Seek(0, SeekOrigin.Begin); + + return (hash, stream); } } \ No newline at end of file diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 6f69794..1acf054 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -20,11 +20,7 @@ public class CreateFlagInvocable(DatabaseContext db, ObjectStorageService object try { - var image = await Payload.ImageData.ConvertBase64UriToImage(size: 256, crop: false); - image.Seek(0, SeekOrigin.Begin); - var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); - image.Seek(0, SeekOrigin.Begin); - + var (hash, image) = await Payload.ImageData.ConvertBase64UriToImage(size: 256, crop: false); await objectStorageService.PutObjectAsync(Path(hash), image, "image/webp"); var flag = new PrideFlag diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs index 7ed801a..baab54a 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -31,9 +31,7 @@ public class MemberAvatarUpdateInvocable(DatabaseContext db, ObjectStorageServic try { - var image = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); - var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); - image.Seek(0, SeekOrigin.Begin); + var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); var prevHash = member.Avatar; await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index 44dd312..aab0240 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -31,8 +31,7 @@ public class UserAvatarUpdateInvocable(DatabaseContext db, ObjectStorageService try { - var image = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); - var hash = Convert.ToHexString(await SHA256.HashDataAsync(image)).ToLower(); + var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); image.Seek(0, SeekOrigin.Begin); var prevHash = user.Avatar; From 758ab9ec5b4b475c9a5a6dc1907cd865be65fc9b Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Sep 2024 23:03:50 +0200 Subject: [PATCH 074/261] feat(backend): delete flag endpoint --- .../Controllers/FlagsController.cs | 39 ++++++++++++++++++- .../Extensions/AvatarObjectExtensions.cs | 4 ++ Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 3 +- 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 7bf20e5..31f3400 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -1,6 +1,7 @@ using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; @@ -11,11 +12,15 @@ namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users/@me/flags")] public class FlagsController( + ILogger logger, DatabaseContext db, UserRendererService userRenderer, + ObjectStorageService objectStorageService, ISnowflakeGenerator snowflakeGenerator, IQueue queue) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpGet] [Authorize("identify")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] @@ -40,8 +45,40 @@ public class FlagsController( } public record CreateFlagRequest(string Name, string Image, string? Description); + private record CreateFlagResponse(Snowflake Id, string Name, string? Description); - public record CreateFlagResponse(Snowflake Id, string Name, string? Description); + [HttpDelete("{id}")] + [Authorize("user.update")] + public async Task DeleteFlagAsync(Snowflake id) + { + await using var tx = await db.Database.BeginTransactionAsync(); + + var 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."); + + var hash = flag.Hash; + + db.PrideFlags.Remove(flag); + await db.SaveChangesAsync(); + + var flagCount = await db.PrideFlags.CountAsync(f => f.Hash == flag.Hash); + if (flagCount == 0) + { + try + { + _logger.Information("Deleting flag file {Hash} as it is no longer used by any flags", hash); + await objectStorageService.DeleteFlagAsync(hash); + } + catch (Exception e) + { + _logger.Error(e, "Error deleting flag file {Hash}", hash); + } + } else _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); + + await tx.CommitAsync(); + + return NoContent(); + } private PrideFlagResponse ToResponse(PrideFlag flag) => new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description); diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs index 063f835..6aa1626 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs @@ -24,6 +24,10 @@ public static class AvatarObjectExtensions CancellationToken ct = default) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateInvocable.Path(id, hash), ct); + public static async Task DeleteFlagAsync(this ObjectStorageService objectStorageService, string hash, + CancellationToken ct = default) => + await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct); + public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage(this string uri, int size, bool crop) { if (!uri.StartsWith("data:image/")) diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 1acf054..0d99244 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -1,4 +1,3 @@ -using System.Security.Cryptography; using Coravel.Invocable; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -45,5 +44,5 @@ public class CreateFlagInvocable(DatabaseContext db, ObjectStorageService object throw new NotImplementedException(); } - private static string Path(string hash) => $"flags/{hash}.webp"; + public static string Path(string hash) => $"flags/{hash}.webp"; } \ No newline at end of file From 6a4aa8064a97da54be4791fab404e361821fca71 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 27 Sep 2024 00:38:34 +0200 Subject: [PATCH 075/261] feat(backend): update flag endpoint --- .../Controllers/FlagsController.cs | 87 ++++++++++++++++++- 1 file changed, 83 insertions(+), 4 deletions(-) diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 31f3400..5021d8e 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -5,6 +5,7 @@ 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; @@ -20,7 +21,7 @@ public class FlagsController( IQueue queue) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); - + [HttpGet] [Authorize("identify")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] @@ -36,6 +37,8 @@ public class FlagsController( [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] public IActionResult CreateFlag([FromBody] CreateFlagRequest req) { + ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); + var id = snowflakeGenerator.GenerateSnowflake(); queue.QueueInvocableWithPayload( @@ -45,14 +48,41 @@ public class FlagsController( } public record CreateFlagRequest(string Name, string Image, string? Description); + private record CreateFlagResponse(Snowflake Id, string Name, string? Description); + [HttpPatch("{id}")] + [Authorize("user.update")] + public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) + { + ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); + + var 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(ToResponse(flag)); + } + + public class UpdateFlagRequest : PatchRequest + { + public string? Name { get; init; } + public string? Description { get; init; } + } + [HttpDelete("{id}")] [Authorize("user.update")] public async Task DeleteFlagAsync(Snowflake id) { await using var tx = await db.Database.BeginTransactionAsync(); - + var 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."); @@ -73,8 +103,9 @@ public class FlagsController( { _logger.Error(e, "Error deleting flag file {Hash}", hash); } - } else _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); - + } + else _logger.Debug("Flag file {Hash} is used by other flags, not deleting", hash); + await tx.CommitAsync(); return NoContent(); @@ -88,4 +119,52 @@ public class FlagsController( string ImageUrl, string Name, string? Description); + + 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; + } } \ No newline at end of file From a3cbdc1a080a65982583897c0eac2ffc3afcc92b Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 27 Sep 2024 14:48:09 +0200 Subject: [PATCH 076/261] feat(backend): ability to set profile flags, return profile flags in get user endpoint --- .../Controllers/FlagsController.cs | 17 +++------ .../Controllers/UsersController.cs | 7 ++++ .../Database/FlagQueryExtensions.cs | 36 +++++++++++++++++++ .../Services/UserRendererService.cs | 17 +++++++-- 4 files changed, 62 insertions(+), 15 deletions(-) create mode 100644 Foxnouns.Backend/Database/FlagQueryExtensions.cs diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 5021d8e..efae036 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -24,17 +24,17 @@ public class FlagsController( [HttpGet] [Authorize("identify")] - [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] + [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] public async Task GetFlagsAsync(CancellationToken ct = default) { var flags = await db.PrideFlags.Where(f => f.UserId == CurrentUser!.Id).ToListAsync(ct); - return Ok(flags.Select(ToResponse)); + return Ok(flags.Select(userRenderer.RenderPrideFlag)); } [HttpPost] [Authorize("user.update")] - [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] + [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] public IActionResult CreateFlag([FromBody] CreateFlagRequest req) { ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, req.Image)); @@ -68,7 +68,7 @@ public class FlagsController( db.Update(flag); await db.SaveChangesAsync(); - return Ok(ToResponse(flag)); + return Ok(userRenderer.RenderPrideFlag(flag)); } public class UpdateFlagRequest : PatchRequest @@ -111,15 +111,6 @@ public class FlagsController( return NoContent(); } - private PrideFlagResponse ToResponse(PrideFlag flag) => - new(flag.Id, userRenderer.ImageUrlFor(flag), flag.Name, flag.Description); - - private record PrideFlagResponse( - Snowflake Id, - string ImageUrl, - string Name, - string? Description); - private static List<(string, ValidationError?)> ValidateFlag(string? name, string? description, string? imageData) { var errors = new List<(string, ValidationError?)>(); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index fddd798..05bccdd 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -86,6 +86,12 @@ public class UsersController( user.Fields = req.Fields.ToList(); } + if (req.Flags != null) + { + var flagError = await db.SetUserFlagsAsync(CurrentUser!.Id, req.Flags); + if (flagError != null) errors.Add(("flags", flagError)); + } + if (req.HasProperty(nameof(req.Avatar))) errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); @@ -182,6 +188,7 @@ public class UsersController( public FieldEntry[]? Names { get; init; } public Pronoun[]? Pronouns { get; init; } public Field[]? Fields { get; init; } + public Snowflake[]? Flags { get; init; } } diff --git a/Foxnouns.Backend/Database/FlagQueryExtensions.cs b/Foxnouns.Backend/Database/FlagQueryExtensions.cs new file mode 100644 index 0000000..39272af --- /dev/null +++ b/Foxnouns.Backend/Database/FlagQueryExtensions.cs @@ -0,0 +1,36 @@ +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) + { + var currentFlags = await db.UserFlags.Where(f => f.UserId == userId).ToListAsync(); + foreach (var 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); + + var flags = await db.GetFlagsAsync(userId); + var unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); + if (unknownFlagIds.Length != 0) + return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds); + + var userFlags = flagIds.Select(id => new UserFlag { PrideFlagId = id, UserId = userId }); + db.UserFlags.AddRange(userFlags); + + return null; + } +} \ No newline at end of file diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 688214d..95d40d3 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -25,10 +25,12 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe renderAuthMethods = renderAuthMethods && tokenPrivileged; IEnumerable members = - renderMembers ? await db.Members.Where(m => m.UserId == user.Id).ToListAsync(ct) : []; + 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); + var flags = await db.UserFlags.Where(f => f.UserId == user.Id).OrderBy(f => f.Id).ToListAsync(ct); + var authMethods = renderAuthMethods ? await db.AuthMethods .Where(a => a.UserId == user.Id) @@ -40,6 +42,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe user.Id, user.Sid, user.Username, user.DisplayName, user.Bio, user.MemberTitle, AvatarUrlFor(user), user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, + flags.Select(f => RenderPrideFlag(f.PrideFlag)), renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderAuthMethods ? authMethods.Select(a => new AuthenticationMethodResponse( @@ -58,7 +61,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe private string? AvatarUrlFor(User user) => user.Avatar != null ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - + public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; public record UserResponse( @@ -74,6 +77,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe IEnumerable Pronouns, IEnumerable Fields, Dictionary CustomPreferences, + IEnumerable Flags, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] @@ -105,4 +109,13 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe string? AvatarUrl, Dictionary CustomPreferences ); + + public PrideFlagResponse RenderPrideFlag(PrideFlag flag) => + new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); + + public record PrideFlagResponse( + Snowflake Id, + string ImageUrl, + string Name, + string? Description); } \ No newline at end of file From 8fe8755183f82fe234bd2f3bb6f4818b3687e8fa Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 27 Sep 2024 15:29:33 +0200 Subject: [PATCH 077/261] feat(backend): validate links, allow setting links in POST /users/@me/members --- .../Controllers/MembersController.cs | 5 +++- .../Controllers/UsersController.cs | 4 +-- Foxnouns.Backend/Database/DatabaseContext.cs | 2 +- .../Services/MemberRendererService.cs | 3 +- Foxnouns.Backend/Utils/ValidationUtils.cs | 28 +++++++++++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 44bb9ce..8438161 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -54,7 +54,8 @@ public class MembersController( ("avatar", ValidationUtils.ValidateAvatar(req.Avatar)), .. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), .. ValidationUtils.ValidateFieldEntries(req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names"), - .. ValidationUtils.ValidatePronouns(req.Pronouns?.ToArray(), CurrentUser!.CustomPreferences) + .. ValidationUtils.ValidatePronouns(req.Pronouns?.ToArray(), CurrentUser!.CustomPreferences), + .. ValidationUtils.ValidateLinks(req.Links) ]); var member = new Member @@ -64,6 +65,7 @@ public class MembersController( Name = req.Name, DisplayName = req.DisplayName, Bio = req.Bio, + Links = req.Links ?? [], Fields = req.Fields ?? [], Names = req.Names ?? [], Pronouns = req.Pronouns ?? [], @@ -113,6 +115,7 @@ public class MembersController( string? Bio, string? Avatar, bool? Unlisted, + string[]? Links, List? Names, List? Pronouns, List? Fields); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 05bccdd..c976183 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -64,7 +64,7 @@ public class UsersController( if (req.HasProperty(nameof(req.Links))) { - // TODO: validate link length + errors.AddRange(ValidationUtils.ValidateLinks(req.Links)); user.Links = req.Links ?? []; } @@ -238,7 +238,7 @@ public class UsersController( .SetProperty(u => u.Sid, _ => db.FindFreeUserSid()) .SetProperty(u => u.LastSidReroll, clock.GetCurrentInstant()) .SetProperty(u => u.LastActive, clock.GetCurrentInstant())); - + var user = await db.ResolveUserAsync(CurrentUser.Id); return Ok(await userRenderer.RenderUserAsync(user, CurrentUser, CurrentToken, renderMembers: false)); } diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 81866fe..dbf38c5 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -21,7 +21,7 @@ public class DatabaseContext : DbContext public DbSet Tokens { get; set; } public DbSet Applications { get; set; } public DbSet TemporaryKeys { get; set; } - + public DbSet PrideFlags { get; set; } public DbSet UserFlags { get; set; } public DbSet MemberFlags { get; set; } diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 11416f0..0fdaa53 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -37,7 +37,8 @@ public class MemberRendererService(DatabaseContext db, Config config) private UserRendererService.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, + public PartialMember RenderPartialMember(Member member, bool renderUnlisted = false) => new(member.Id, member.Sid, + member.Name, member.DisplayName, member.Bio, AvatarUrlFor(member), member.Names, member.Pronouns, renderUnlisted ? member.Unlisted : null); diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index f29dd24..1650860 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -99,6 +99,34 @@ public static class ValidationUtils }; } + private const int MaxLinks = 25; + private const int MaxLinkLength = 256; + + public static IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) + { + if (links == null) return []; + if (links.Length > MaxLinks) + return [("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length))]; + + var errors = new List<(string, ValidationError?)>(); + foreach (var (link, idx) in links.Select((l, i) => (l, i))) + { + switch (link.Length) + { + case 0: + errors.Add(($"links.{idx}", + ValidationError.LengthError("Link cannot be empty", 1, 256, 0))); + break; + case > MaxLinkLength: + errors.Add(($"links.{idx}", + ValidationError.LengthError("Link is too long", 1, MaxLinkLength, link.Length))); + break; + } + } + + return errors; + } + public static ValidationError? ValidateBio(string? bio) { return bio?.Length switch From e11e60e16bb43344a85419050d1012dc5acc2f31 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 28 Sep 2024 22:28:59 +0200 Subject: [PATCH 078/261] feat(backend): add update member endpoint --- .../Controllers/MembersController.cs | 94 +++++++++++++++++++ .../Controllers/UsersController.cs | 16 +++- .../Database/FlagQueryExtensions.cs | 21 +++++ Foxnouns.Backend/Utils/PatchRequest.cs | 3 + 4 files changed, 133 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 8438161..1c4a783 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -92,6 +92,100 @@ public class MembersController( return Ok(memberRenderer.RenderMember(member, CurrentToken)); } + [HttpPatch("/api/v2/users/@me/members/{memberRef}")] + [Authorize("member.update")] + public async Task UpdateMemberAsync(string memberRef, [FromBody] UpdateMemberRequest req) + { + await using var tx = await db.Database.BeginTransactionAsync(); + var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); + var errors = new List<(string, ValidationError?)>(); + + if (req.Name != null) + { + errors.Add(("name", ValidationUtils.ValidateMemberName(req.Name))); + member.Name = req.Name; + } + + if (req.HasProperty(nameof(req.DisplayName))) + { + errors.Add(("display_name", ValidationUtils.ValidateDisplayName(req.DisplayName))); + member.DisplayName = req.DisplayName; + } + + if (req.HasProperty(nameof(req.Bio))) + { + errors.Add(("bio", ValidationUtils.ValidateBio(req.Bio))); + member.Bio = req.Bio; + } + + if (req.HasProperty(nameof(req.Links))) + { + errors.AddRange(ValidationUtils.ValidateLinks(req.Links)); + member.Links = req.Links ?? []; + } + + if (req.Names != null) + { + errors.AddRange(ValidationUtils.ValidateFieldEntries(req.Names, CurrentUser!.CustomPreferences, "names")); + member.Names = req.Names.ToList(); + } + + if (req.Pronouns != null) + { + errors.AddRange(ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences)); + member.Pronouns = req.Pronouns.ToList(); + } + + if (req.Fields != null) + { + errors.AddRange(ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences)); + member.Fields = req.Fields.ToList(); + } + + if (req.Flags != null) + { + var 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", ValidationUtils.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))) + queue.QueueInvocableWithPayload( + new AvatarUpdatePayload(member.Id, req.Avatar)); + 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)); + } + + 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; } + } + [HttpDelete("/api/v2/users/@me/members/{memberRef}")] [Authorize("member.update")] public async Task DeleteMemberAsync(string memberRef) diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index c976183..e292fc3 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using Coravel.Queuing.Interfaces; +using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Jobs; @@ -15,11 +16,14 @@ namespace Foxnouns.Backend.Controllers; [Route("/api/v2/users")] public class UsersController( DatabaseContext db, + ILogger logger, UserRendererService userRenderer, ISnowflakeGenerator snowflakeGenerator, IQueue queue, IClock clock) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetUserAsync(string userRef, CancellationToken ct = default) @@ -103,7 +107,17 @@ public class UsersController( queue.QueueInvocableWithPayload( new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); - await db.SaveChangesAsync(ct); + 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)); diff --git a/Foxnouns.Backend/Database/FlagQueryExtensions.cs b/Foxnouns.Backend/Database/FlagQueryExtensions.cs index 39272af..cbdd710 100644 --- a/Foxnouns.Backend/Database/FlagQueryExtensions.cs +++ b/Foxnouns.Backend/Database/FlagQueryExtensions.cs @@ -33,4 +33,25 @@ public static class FlagQueryExtensions return null; } + + public static async Task SetMemberFlagsAsync(this DatabaseContext db, Snowflake userId, + Snowflake memberId, Snowflake[] flagIds) + { + var currentFlags = await db.MemberFlags.Where(f => f.MemberId == memberId).ToListAsync(); + foreach (var 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); + + var flags = await db.GetFlagsAsync(userId); + var unknownFlagIds = flagIds.Where(id => flags.All(f => f.Id != id)).ToArray(); + if (unknownFlagIds.Length != 0) + return ValidationError.GenericValidationError("Unknown flag IDs", unknownFlagIds); + + var memberFlags = flagIds.Select(id => new MemberFlag { PrideFlagId = id, MemberId = memberId }); + db.MemberFlags.AddRange(memberFlags); + + return null; + } } \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/PatchRequest.cs b/Foxnouns.Backend/Utils/PatchRequest.cs index da98615..4e3ac1c 100644 --- a/Foxnouns.Backend/Utils/PatchRequest.cs +++ b/Foxnouns.Backend/Utils/PatchRequest.cs @@ -6,6 +6,9 @@ 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 { From f539902711184a882e56fac80da103b56b816400 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 29 Sep 2024 19:52:22 +0200 Subject: [PATCH 079/261] feat(backend): render flags in member response --- Foxnouns.Backend/Database/DatabaseQueryExtensions.cs | 2 ++ Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs | 1 + Foxnouns.Backend/Services/MemberRendererService.cs | 7 ++++++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 60d4499..d5df79d 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -71,6 +71,7 @@ public static class DatabaseQueryExtensions { member = await context.Members .Include(m => m.User) + .Include(m => m.ProfileFlags) .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Id == snowflake && m.UserId == userId, ct); if (member != null) return member; @@ -78,6 +79,7 @@ public static class DatabaseQueryExtensions member = await context.Members .Include(m => m.User) + .Include(m => m.ProfileFlags) .Where(m => !m.User.Deleted) .FirstOrDefaultAsync(m => m.Name == memberRef && m.UserId == userId, ct); if (member != null) return member; diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index 6b6da6d..c28b58e 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,5 +1,6 @@ using System.Net; using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; namespace Foxnouns.Backend.Middleware; diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 0fdaa53..9ebea49 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -24,13 +24,14 @@ public class MemberRendererService(DatabaseContext db, Config config) return members.Select(m => RenderPartialMember(m, renderUnlisted)); } - public MemberResponse RenderMember(Member member, Token? token) + public MemberResponse RenderMember(Member member, Token? token = null) { var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); return new MemberResponse( member.Id, member.Sid, member.Name, member.DisplayName, 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); } @@ -50,6 +51,9 @@ public class MemberRendererService(DatabaseContext db, Config config) private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; + private UserRendererService.PrideFlagResponse RenderPrideFlag(PrideFlag flag) => + new(flag.Id, ImageUrlFor(flag), flag.Name, flag.Description); + public record PartialMember( Snowflake Id, string Sid, @@ -73,6 +77,7 @@ public class MemberRendererService(DatabaseContext db, Config config) IEnumerable Names, IEnumerable Pronouns, IEnumerable Fields, + IEnumerable Flags, UserRendererService.PartialUser User, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Unlisted); From dc18ab60d2f7fcf224989f02bfba94f3449070da Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 29 Sep 2024 20:24:47 +0200 Subject: [PATCH 080/261] feat(frontend): add flags to user page --- Foxnouns.Frontend/app/app.scss | 6 ++++ .../components/{ => profile}/ProfileField.tsx | 2 +- .../app/components/profile/ProfileFlag.tsx | 28 +++++++++++++++++++ .../components/{ => profile}/ProfileLink.tsx | 0 .../components/{ => profile}/PronounLink.tsx | 0 .../components/{ => profile}/StatusIcon.tsx | 0 .../components/{ => profile}/StatusLine.tsx | 4 +-- Foxnouns.Frontend/app/lib/api/member.ts | 5 ++++ Foxnouns.Frontend/app/lib/api/user.ts | 8 ++++++ .../app/routes/$username/route.tsx | 20 +++++++++++-- 10 files changed, 68 insertions(+), 5 deletions(-) rename Foxnouns.Frontend/app/components/{ => profile}/ProfileField.tsx (89%) create mode 100644 Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx rename Foxnouns.Frontend/app/components/{ => profile}/ProfileLink.tsx (100%) rename Foxnouns.Frontend/app/components/{ => profile}/PronounLink.tsx (100%) rename Foxnouns.Frontend/app/components/{ => profile}/StatusIcon.tsx (100%) rename Foxnouns.Frontend/app/components/{ => profile}/StatusLine.tsx (89%) create mode 100644 Foxnouns.Frontend/app/lib/api/member.ts diff --git a/Foxnouns.Frontend/app/app.scss b/Foxnouns.Frontend/app/app.scss index 45a8ee5..0d30b1e 100644 --- a/Foxnouns.Frontend/app/app.scss +++ b/Foxnouns.Frontend/app/app.scss @@ -21,3 +21,9 @@ @import "@fontsource/firago/400.css"; @import "@fontsource/firago/400-italic.css"; @import "@fontsource/firago/700.css"; + +.pride-flag { + height: 1.5rem; + max-width: 200px; + border-radius: 3px; +} diff --git a/Foxnouns.Frontend/app/components/ProfileField.tsx b/Foxnouns.Frontend/app/components/profile/ProfileField.tsx similarity index 89% rename from Foxnouns.Frontend/app/components/ProfileField.tsx rename to Foxnouns.Frontend/app/components/profile/ProfileField.tsx index ed5577d..92d8a46 100644 --- a/Foxnouns.Frontend/app/components/ProfileField.tsx +++ b/Foxnouns.Frontend/app/components/profile/ProfileField.tsx @@ -1,5 +1,5 @@ import { CustomPreference, FieldEntry, Pronoun } from "~/lib/api/user"; -import StatusLine from "~/components/StatusLine"; +import StatusLine from "~/components/profile/StatusLine"; export default function ProfileField({ name, diff --git a/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx b/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx new file mode 100644 index 0000000..756783d --- /dev/null +++ b/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx @@ -0,0 +1,28 @@ +import type { PrideFlag } from "~/lib/api/user"; +import { OverlayTrigger, Tooltip } from "react-bootstrap"; + +export default function ProfileFlag({ flag }: { flag: PrideFlag }) { + return ( + + + {flag.description ?? flag.name} + + } + > + + {flag.description + + {" "} + {flag.name} + + ); +} diff --git a/Foxnouns.Frontend/app/components/ProfileLink.tsx b/Foxnouns.Frontend/app/components/profile/ProfileLink.tsx similarity index 100% rename from Foxnouns.Frontend/app/components/ProfileLink.tsx rename to Foxnouns.Frontend/app/components/profile/ProfileLink.tsx diff --git a/Foxnouns.Frontend/app/components/PronounLink.tsx b/Foxnouns.Frontend/app/components/profile/PronounLink.tsx similarity index 100% rename from Foxnouns.Frontend/app/components/PronounLink.tsx rename to Foxnouns.Frontend/app/components/profile/PronounLink.tsx diff --git a/Foxnouns.Frontend/app/components/StatusIcon.tsx b/Foxnouns.Frontend/app/components/profile/StatusIcon.tsx similarity index 100% rename from Foxnouns.Frontend/app/components/StatusIcon.tsx rename to Foxnouns.Frontend/app/components/profile/StatusIcon.tsx diff --git a/Foxnouns.Frontend/app/components/StatusLine.tsx b/Foxnouns.Frontend/app/components/profile/StatusLine.tsx similarity index 89% rename from Foxnouns.Frontend/app/components/StatusLine.tsx rename to Foxnouns.Frontend/app/components/profile/StatusLine.tsx index 704df75..3729e8a 100644 --- a/Foxnouns.Frontend/app/components/StatusLine.tsx +++ b/Foxnouns.Frontend/app/components/profile/StatusLine.tsx @@ -7,8 +7,8 @@ import { Pronoun, } from "~/lib/api/user"; import classNames from "classnames"; -import StatusIcon from "~/components/StatusIcon"; -import PronounLink from "~/components/PronounLink"; +import StatusIcon from "~/components/profile/StatusIcon"; +import PronounLink from "~/components/profile/PronounLink"; export default function StatusLine({ entry, diff --git a/Foxnouns.Frontend/app/lib/api/member.ts b/Foxnouns.Frontend/app/lib/api/member.ts new file mode 100644 index 0000000..478e7af --- /dev/null +++ b/Foxnouns.Frontend/app/lib/api/member.ts @@ -0,0 +1,5 @@ +import { PartialMember, PrideFlag } from "~/lib/api/user"; + +export type Member = PartialMember & { + flags: PrideFlag[]; +}; diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts index 5dc969c..0b6f375 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -13,6 +13,7 @@ export type User = PartialUser & { names: FieldEntry[]; pronouns: Pronoun[]; fields: Field[]; + flags: PrideFlag[]; }; export type UserWithMembers = User & { members: PartialMember[] }; @@ -50,6 +51,13 @@ export type Field = { entries: FieldEntry[]; }; +export type PrideFlag = { + id: string; + image_url: string; + name: string; + description: string | null; +}; + export type CustomPreference = { icon: string; tooltip: string; diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index c19b5e7..f5db6b7 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -6,12 +6,13 @@ import { loader as rootLoader } from "~/root"; import { Alert, Button, Pagination } from "react-bootstrap"; import { Trans, useTranslation } from "react-i18next"; import { renderMarkdown } from "~/lib/markdown"; -import ProfileLink from "~/components/ProfileLink"; -import ProfileField from "~/components/ProfileField"; +import ProfileLink from "~/components/profile/ProfileLink"; +import ProfileField from "~/components/profile/ProfileField"; import { PersonPlusFill } from "react-bootstrap-icons"; import { defaultAvatarUrl } from "~/lib/utils"; import MemberCard from "~/routes/$username/MemberCard"; import { ReactNode } from "react"; +import ProfileFlag from "~/components/profile/ProfileFlag"; export const meta: MetaFunction = ({ data }) => { const { user } = data!; @@ -92,6 +93,13 @@ export default function UserPage() { height={200} className="rounded-circle img-fluid" /> + {user.flags && user.bio && ( +
    + {user.flags.map((f, i) => ( + + ))} +
    + )}
    {user.display_name ? ( @@ -146,6 +154,14 @@ export default function UserPage() { ))}
    + {/* If a user or member has no bio, flags are displayed in a row below the other profile info, rather than just below the avatar */} + {user.flags && !user.bio && ( +
    + {user.flags.map((f, i) => ( + + ))} +
    + )} {(members.length > 0 || isMeUser) && ( <>
    From 45142164055a993e212c7a60264de54179211984 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 29 Sep 2024 20:32:54 +0200 Subject: [PATCH 081/261] refactor(frontend): extract profile view to component shared between users and members --- .../app/components/profile/BaseProfile.tsx | 111 ++++++++++++++++++ Foxnouns.Frontend/app/lib/api/member.ts | 4 +- .../app/routes/$username/route.tsx | 91 ++------------ 3 files changed, 121 insertions(+), 85 deletions(-) create mode 100644 Foxnouns.Frontend/app/components/profile/BaseProfile.tsx diff --git a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx new file mode 100644 index 0000000..4017877 --- /dev/null +++ b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx @@ -0,0 +1,111 @@ +import { CustomPreference, User } from "~/lib/api/user"; +import { Member } from "~/lib/api/member"; +import { defaultAvatarUrl } from "~/lib/utils"; +import ProfileFlag from "~/components/profile/ProfileFlag"; +import ProfileLink from "~/components/profile/ProfileLink"; +import ProfileField from "~/components/profile/ProfileField"; +import { useTranslation } from "react-i18next"; +import { renderMarkdown } from "~/lib/markdown"; + +export type Props = { + name: string; + fullName?: string; + avatarI18nKey: string; + profile: User | Member; + customPreferences: Record; +}; + +export default function BaseProfile({ + name, + avatarI18nKey, + fullName, + profile, + customPreferences, +}: Props) { + const { t } = useTranslation(); + const bio = renderMarkdown(profile.bio); + + return ( + <> +
    +
    +
    + {t(avatarI18nKey, + {profile.flags && profile.bio && ( +
    + {profile.flags.map((f, i) => ( + + ))} +
    + )} +
    +
    + {profile.display_name ? ( + <> +

    {profile.display_name}

    +

    {fullName || `@${name}`}

    + + ) : ( + <> +

    {fullName || `@${name}`}

    + + )} + {bio && ( + <> +
    +

    + + )} +
    + {profile.links.length > 0 && ( +
    +
      + {profile.links.map((l, i) => ( + + ))} +
    +
    + )} +
    +
    + {profile.names.length > 0 && ( + + )} + {profile.pronouns.length > 0 && ( + + )} + {profile.fields.map((f, i) => ( + + ))} +
    +
    + {/* If a user or member has no bio, flags are displayed in a row below the other profile info, rather than just below the avatar */} + {profile.flags && !profile.bio && ( +
    + {profile.flags.map((f, i) => ( + + ))} +
    + )} + + ); +} diff --git a/Foxnouns.Frontend/app/lib/api/member.ts b/Foxnouns.Frontend/app/lib/api/member.ts index 478e7af..7827f0a 100644 --- a/Foxnouns.Frontend/app/lib/api/member.ts +++ b/Foxnouns.Frontend/app/lib/api/member.ts @@ -1,5 +1,7 @@ -import { PartialMember, PrideFlag } from "~/lib/api/user"; +import { Field, PartialMember, PrideFlag } from "~/lib/api/user"; export type Member = PartialMember & { + fields: Field[]; flags: PrideFlag[]; + links: string[]; }; diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index f5db6b7..48aee01 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -6,13 +6,10 @@ import { loader as rootLoader } from "~/root"; import { Alert, Button, Pagination } from "react-bootstrap"; import { Trans, useTranslation } from "react-i18next"; import { renderMarkdown } from "~/lib/markdown"; -import ProfileLink from "~/components/profile/ProfileLink"; -import ProfileField from "~/components/profile/ProfileField"; import { PersonPlusFill } from "react-bootstrap-icons"; -import { defaultAvatarUrl } from "~/lib/utils"; import MemberCard from "~/routes/$username/MemberCard"; import { ReactNode } from "react"; -import ProfileFlag from "~/components/profile/ProfileFlag"; +import BaseProfile from "~/components/profile/BaseProfile"; export const meta: MetaFunction = ({ data }) => { const { user } = data!; @@ -45,7 +42,6 @@ export default function UserPage() { const { meUser } = useRouteLoaderData("root") || { meUser: undefined }; const isMeUser = meUser && meUser.id === user.id; - const bio = renderMarkdown(user.bio); const paginationItems: ReactNode[] = []; for (let i = 0; i < pageCount; i++) { @@ -83,85 +79,12 @@ export default function UserPage() { )} -
    -
    -
    - {t("user.avatar-alt", - {user.flags && user.bio && ( -
    - {user.flags.map((f, i) => ( - - ))} -
    - )} -
    -
    - {user.display_name ? ( - <> -

    {user.display_name}

    -

    @{user.username}

    - - ) : ( - <> -

    @{user.username}

    - - )} - {bio && ( - <> -
    -

    - - )} -
    - {user.links.length > 0 && ( -
    -
      - {user.links.map((l, i) => ( - - ))} -
    -
    - )} -
    -
    - {user.names.length > 0 && ( - - )} - {user.pronouns.length > 0 && ( - - )} - {user.fields.map((f, i) => ( - - ))} -
    -
    - {/* If a user or member has no bio, flags are displayed in a row below the other profile info, rather than just below the avatar */} - {user.flags && !user.bio && ( -
    - {user.flags.map((f, i) => ( - - ))} -
    - )} + {(members.length > 0 || isMeUser) && ( <>
    From 3f0a94af3d3fa0014ebdbd6e07b1b4f720cf6883 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 29 Sep 2024 20:37:30 +0200 Subject: [PATCH 082/261] fix(frontend): reset colour and change size of member card links --- Foxnouns.Frontend/app/routes/$username/MemberCard.tsx | 2 +- Foxnouns.Frontend/app/routes/$username/route.tsx | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx index 893782e..bc6e516 100644 --- a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx +++ b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx @@ -47,7 +47,7 @@ export default function MemberCard({ user, member }: { user: PartialUser; member />

    - + {member.display_name ?? member.name} {member.unlisted === true && ( <> diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index 48aee01..af300d8 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -56,12 +56,14 @@ export default function UserPage() { {paginationItems} From 0bdd0148d2307e5aff89ccdac7d73863ffc393b9 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 29 Sep 2024 21:10:11 +0200 Subject: [PATCH 083/261] feat(frontend): member page --- .../app/components/profile/BaseProfile.tsx | 32 ++-- Foxnouns.Frontend/app/lib/api/member.ts | 4 +- .../app/routes/$username/route.tsx | 5 +- .../app/routes/$username_.$member/route.tsx | 64 +++++++ Foxnouns.Frontend/public/locales/en.json | 177 +++++++++--------- 5 files changed, 181 insertions(+), 101 deletions(-) create mode 100644 Foxnouns.Frontend/app/routes/$username_.$member/route.tsx diff --git a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx index 4017877..a058755 100644 --- a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx +++ b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx @@ -10,14 +10,14 @@ import { renderMarkdown } from "~/lib/markdown"; export type Props = { name: string; fullName?: string; - avatarI18nKey: string; + userI18nKeys: boolean; profile: User | Member; customPreferences: Record; }; export default function BaseProfile({ name, - avatarI18nKey, + userI18nKeys, fullName, profile, customPreferences, @@ -30,13 +30,23 @@ export default function BaseProfile({

    - {t(avatarI18nKey, + {userI18nKeys ? ( + {t("user.avatar-alt", + ) : ( + {t("member.avatar-alt", + )} {profile.flags && profile.bio && (
    {profile.flags.map((f, i) => ( @@ -46,9 +56,9 @@ export default function BaseProfile({ )}
    - {profile.display_name ? ( + {profile.display_name || fullName ? ( <> -

    {profile.display_name}

    +

    {profile.display_name || name}

    {fullName || `@${name}`}

    ) : ( diff --git a/Foxnouns.Frontend/app/lib/api/member.ts b/Foxnouns.Frontend/app/lib/api/member.ts index 7827f0a..1719f04 100644 --- a/Foxnouns.Frontend/app/lib/api/member.ts +++ b/Foxnouns.Frontend/app/lib/api/member.ts @@ -1,7 +1,9 @@ -import { Field, PartialMember, PrideFlag } from "~/lib/api/user"; +import { Field, PartialMember, PartialUser, PrideFlag } from "~/lib/api/user"; export type Member = PartialMember & { fields: Field[]; flags: PrideFlag[]; links: string[]; + + user: PartialUser; }; diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index af300d8..77f0f58 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -5,7 +5,6 @@ import serverRequest from "~/lib/request.server"; import { loader as rootLoader } from "~/root"; import { Alert, Button, Pagination } from "react-bootstrap"; import { Trans, useTranslation } from "react-i18next"; -import { renderMarkdown } from "~/lib/markdown"; import { PersonPlusFill } from "react-bootstrap-icons"; import MemberCard from "~/routes/$username/MemberCard"; import { ReactNode } from "react"; @@ -77,13 +76,13 @@ export default function UserPage() { You are currently viewing your public profile.
    - Edit your profile + Edit your profile
    )} diff --git a/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx b/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx new file mode 100644 index 0000000..d463fbf --- /dev/null +++ b/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx @@ -0,0 +1,64 @@ +import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; +import { Link, redirect, useLoaderData, useRouteLoaderData } from "@remix-run/react"; +import serverRequest from "~/lib/request.server"; +import { Member } from "~/lib/api/member"; +import BaseProfile from "~/components/profile/BaseProfile"; +import { loader as rootLoader } from "~/root"; +import { Alert, Button } from "react-bootstrap"; +import { Trans, useTranslation } from "react-i18next"; +import { ArrowLeft } from "react-bootstrap-icons"; + +export const meta: MetaFunction = ({ data }) => { + const { member } = data!; + + return [ + { title: `${member.display_name ?? member.name} • @${member.user.username} • pronouns.cc` }, + ]; +}; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + let username = params.username!; + const memberName = params.member!; + if (!username.startsWith("@")) throw redirect(`/@${username}/${memberName}`); + username = username.substring("@".length); + + const member = await serverRequest("GET", `/users/${username}/members/${memberName}`); + return json({ member }); +}; + +export default function MemberPage() { + const { t } = useTranslation(); + const { member } = useLoaderData(); + const { meUser } = useRouteLoaderData("root") || { meUser: undefined }; + const isMeUser = meUser && meUser.id === member.user.id; + + const memberName = member.name; + + return ( + <> + {isMeUser && ( + + + You are currently viewing the public profile of {{ memberName }}. +
    + Edit your profile +
    +
    + )} +
    + {/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */} + +
    + + + ); +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 2a414ba..52464b5 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,88 +1,93 @@ { - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "An error occurred", - "more-info": "Click here for a more detailed error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up" - }, - "user": { - "member-avatar-alt": "Avatar for {{name}}", - "member-hidden": "This member is unlisted, and not shown in your public member list.", - "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", - "avatar-alt": "Avatar for @{{username}}", - "heading": { - "names": "Names", - "pronouns": "Pronouns", - "members": "Members" - }, - "create-member-button": "Create member", - "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" - }, - "log-in": { - "callback": { - "title": { - "discord-success": "Log in with Discord", - "discord-register": "Register with Discord" - }, - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - }, - "welcome": { - "title": "Welcome", - "header": "Welcome to pronouns.cc!", - "blurb": "{welcome.blurb}", - "customize-profile": "Customize your profile", - "customize-profile-blurb": "{welcome.customize-profile-blurb}", - "create-members": "Create members", - "create-members-blurb": "{welcome.create-members-blurb}", - "custom-preferences": "Customize your preferences", - "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", - "profile-button": "Go to your profile" - } + "error": { + "heading": "An error occurred", + "validation": { + "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", + "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", + "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", + "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", + "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" + }, + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "An error occurred", + "more-info": "Click here for a more detailed error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up" + }, + "user": { + "avatar-alt": "Avatar for @{{username}}", + "heading": { + "names": "Names", + "pronouns": "Pronouns", + "members": "Members" + }, + "member-avatar-alt": "Avatar for {{name}}", + "member-hidden": "This member is unlisted, and not shown in your public member list.", + "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", + "create-member-button": "Create member", + "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" + }, + "member": { + "avatar-alt": "Avatar for {{name}}", + "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit your profile", + "back": "Back to {{name}}" + }, + "log-in": { + "callback": { + "title": { + "discord-success": "Log in with Discord", + "discord-register": "Register with Discord" + }, + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up", + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else." + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + }, + "welcome": { + "title": "Welcome", + "header": "Welcome to pronouns.cc!", + "blurb": "{welcome.blurb}", + "customize-profile": "Customize your profile", + "customize-profile-blurb": "{welcome.customize-profile-blurb}", + "create-members": "Create members", + "create-members-blurb": "{welcome.create-members-blurb}", + "custom-preferences": "Customize your preferences", + "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", + "profile-button": "Go to your profile" + } } From 19bfee62032f2ac94689dbc5bea4f0a5258f29a8 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 29 Sep 2024 21:32:09 +0200 Subject: [PATCH 084/261] fix(frontend): correct wording in own member alert --- Foxnouns.Frontend/app/routes/$username_.$member/route.tsx | 2 +- Foxnouns.Frontend/public/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx b/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx index d463fbf..2cb2a41 100644 --- a/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx @@ -41,7 +41,7 @@ export default function MemberPage() { You are currently viewing the public profile of {{ memberName }}.
    - Edit your profile + Edit profile
    )} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 52464b5..18a9cf7 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -42,7 +42,7 @@ }, "member": { "avatar-alt": "Avatar for {{name}}", - "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit your profile", + "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", "back": "Back to {{name}}" }, "log-in": { From 646c2694e1814b3480d2308e57e6df62b1b0fc86 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 30 Sep 2024 13:02:10 +0200 Subject: [PATCH 085/261] add .noai file --- .noai | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .noai diff --git a/.noai b/.noai new file mode 100644 index 0000000..e69de29 From 2b8e4c3e8d3df9a6684bcd2d3a6a0e293039861d Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 30 Sep 2024 20:14:03 +0200 Subject: [PATCH 086/261] feat(frontend): use __Host prefix for token cookie --- Foxnouns.Frontend/app/lib/request.server.ts | 4 +- Foxnouns.Frontend/app/lib/utils.ts | 1 + Foxnouns.Frontend/app/root.tsx | 7 +- .../routes/auth.callback.discord/route.tsx | 5 +- .../app/routes/auth.log-in/route.tsx | 3 +- .../app/routes/auth.log-out/route.tsx | 3 +- .../app/routes/dark-mode/route.tsx | 4 +- Foxnouns.Frontend/public/locales/en.json | 182 +++++++++--------- 8 files changed, 108 insertions(+), 101 deletions(-) diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index d6192ae..4648d5f 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -1,6 +1,7 @@ import { parse as parseCookie, serialize as serializeCookie } from "cookie"; import { API_BASE } from "~/env.server"; import { ApiError, ErrorCode } from "./api/error"; +import { tokenCookieName } from "~/lib/utils"; export type RequestParams = { token?: string; @@ -39,7 +40,7 @@ export default async function serverRequest( return (await resp.json()) as T; } -export const getToken = (req: Request) => getCookie(req, "pronounscc-token"); +export const getToken = (req: Request) => getCookie(req, tokenCookieName); export function getCookie(req: Request, cookieName: string): string | undefined { const header = req.headers.get("Cookie"); @@ -57,4 +58,5 @@ export const writeCookie = (cookieName: string, value: string, maxAge: number | path: "/", sameSite: "lax", httpOnly: true, + secure: true, }); diff --git a/Foxnouns.Frontend/app/lib/utils.ts b/Foxnouns.Frontend/app/lib/utils.ts index 9a8d8b5..89b9f0f 100644 --- a/Foxnouns.Frontend/app/lib/utils.ts +++ b/Foxnouns.Frontend/app/lib/utils.ts @@ -1 +1,2 @@ export const defaultAvatarUrl = "https://pronouns.cc/default/512.webp"; +export const tokenCookieName = "__Host-pronounscc-token"; diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx index b622f3f..3fc3c67 100644 --- a/Foxnouns.Frontend/app/root.tsx +++ b/Foxnouns.Frontend/app/root.tsx @@ -13,7 +13,7 @@ import { LoaderFunctionArgs } from "@remix-run/node"; import { useChangeLanguage } from "remix-i18next/react"; import { useTranslation } from "react-i18next"; -import serverRequest, { getCookie, writeCookie } from "./lib/request.server"; +import serverRequest, { getToken, writeCookie } from "./lib/request.server"; import Meta from "./lib/api/meta"; import Navbar from "./components/nav/Navbar"; import { User, UserSettings } from "./lib/api/user"; @@ -26,11 +26,12 @@ import { errorCodeDesc } from "./components/ErrorAlert"; import { Container } from "react-bootstrap"; import { ReactNode } from "react"; import BaseNavbar from "~/components/nav/BaseNavbar"; +import { tokenCookieName } from "~/lib/utils"; export const loader = async ({ request }: LoaderFunctionArgs) => { const meta = await serverRequest("GET", "/meta"); - const token = getCookie(request, "pronounscc-token"); + const token = getToken(request); let setCookie = ""; let meUser: User | undefined; @@ -43,7 +44,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { } catch (e) { // If we get an unauthorized error, clear the token, as it's not valid anymore. if ((e as ApiError).code === ErrorCode.AuthenticationRequired) { - setCookie = writeCookie("pronounscc-token", token, 0); + setCookie = writeCookie(tokenCookieName, token, 0); } } } diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index 75cdc5e..d27d7d5 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -19,6 +19,7 @@ import { Trans, useTranslation } from "react-i18next"; import { Form, Button, Alert } from "react-bootstrap"; import ErrorAlert from "~/components/ErrorAlert"; import i18n from "~/i18next.server"; +import { tokenCookieName } from "~/lib/utils"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; @@ -53,7 +54,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { }, { headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token!), + "Set-Cookie": writeCookie(tokenCookieName, resp.token!), }, }, ); @@ -90,7 +91,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { return redirect("/auth/welcome", { headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token), + "Set-Cookie": writeCookie(tokenCookieName, resp.token), }, status: 303, }); diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index 09a6675..fc25d75 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -19,6 +19,7 @@ import { AuthResponse, AuthUrls } from "~/lib/api/auth"; import { ApiError, ErrorCode } from "~/lib/api/error"; import ErrorAlert from "~/components/ErrorAlert"; import { User } from "~/lib/api/user"; +import { tokenCookieName } from "~/lib/utils"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; @@ -61,7 +62,7 @@ export const action = async ({ request }: ActionFunctionArgs) => { return redirect("/", { status: 303, headers: { - "Set-Cookie": writeCookie("pronounscc-token", resp.token), + "Set-Cookie": writeCookie(tokenCookieName, resp.token), }, }); } catch (e) { diff --git a/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx index 1b146e2..8b89d8d 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx @@ -1,10 +1,11 @@ import { ActionFunction } from "@remix-run/node"; import { writeCookie } from "~/lib/request.server"; +import { tokenCookieName } from "~/lib/utils"; export const action: ActionFunction = async () => { return new Response(null, { headers: { - "Set-Cookie": writeCookie("pronounscc-token", "token", 0), + "Set-Cookie": writeCookie(tokenCookieName, "token", 0), }, status: 204, }); diff --git a/Foxnouns.Frontend/app/routes/dark-mode/route.tsx b/Foxnouns.Frontend/app/routes/dark-mode/route.tsx index a3f82e1..c3c2b24 100644 --- a/Foxnouns.Frontend/app/routes/dark-mode/route.tsx +++ b/Foxnouns.Frontend/app/routes/dark-mode/route.tsx @@ -1,6 +1,6 @@ import { ActionFunction } from "@remix-run/node"; import { UserSettings } from "~/lib/api/user"; -import serverRequest, { getCookie, writeCookie } from "~/lib/request.server"; +import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; // Handles theme switching // Remix itself handles redirecting back to the original page after the setting is set @@ -15,7 +15,7 @@ export const action: ActionFunction = async ({ request }) => { const body = await request.formData(); const theme = (body.get("theme") as string | null) || "auto"; - const token = getCookie(request, "pronounscc-token"); + const token = getToken(request); if (token) { await serverRequest("PATCH", "/users/@me/settings", { token, diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 18a9cf7..bf974b6 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,93 +1,93 @@ { - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "An error occurred", - "more-info": "Click here for a more detailed error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up" - }, - "user": { - "avatar-alt": "Avatar for @{{username}}", - "heading": { - "names": "Names", - "pronouns": "Pronouns", - "members": "Members" - }, - "member-avatar-alt": "Avatar for {{name}}", - "member-hidden": "This member is unlisted, and not shown in your public member list.", - "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", - "create-member-button": "Create member", - "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" - }, - "member": { - "avatar-alt": "Avatar for {{name}}", - "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", - "back": "Back to {{name}}" - }, - "log-in": { - "callback": { - "title": { - "discord-success": "Log in with Discord", - "discord-register": "Register with Discord" - }, - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - }, - "welcome": { - "title": "Welcome", - "header": "Welcome to pronouns.cc!", - "blurb": "{welcome.blurb}", - "customize-profile": "Customize your profile", - "customize-profile-blurb": "{welcome.customize-profile-blurb}", - "create-members": "Create members", - "create-members-blurb": "{welcome.create-members-blurb}", - "custom-preferences": "Customize your preferences", - "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", - "profile-button": "Go to your profile" - } + "error": { + "heading": "An error occurred", + "validation": { + "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", + "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", + "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", + "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", + "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" + }, + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "An error occurred", + "more-info": "Click here for a more detailed error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up" + }, + "user": { + "avatar-alt": "Avatar for @{{username}}", + "heading": { + "names": "Names", + "pronouns": "Pronouns", + "members": "Members" + }, + "member-avatar-alt": "Avatar for {{name}}", + "member-hidden": "This member is unlisted, and not shown in your public member list.", + "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", + "create-member-button": "Create member", + "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" + }, + "member": { + "avatar-alt": "Avatar for {{name}}", + "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", + "back": "Back to {{name}}" + }, + "log-in": { + "callback": { + "title": { + "discord-success": "Log in with Discord", + "discord-register": "Register with Discord" + }, + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up", + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else." + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + }, + "welcome": { + "title": "Welcome", + "header": "Welcome to pronouns.cc!", + "blurb": "{welcome.blurb}", + "customize-profile": "Customize your profile", + "customize-profile-blurb": "{welcome.customize-profile-blurb}", + "create-members": "Create members", + "create-members-blurb": "{welcome.create-members-blurb}", + "custom-preferences": "Customize your preferences", + "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", + "profile-button": "Go to your profile" + } } From 8f3478d57a9624d1719889b1e2a2c311a0a78e9a Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 30 Sep 2024 20:14:16 +0200 Subject: [PATCH 087/261] fix(backend): only validate member name if it's changed --- Foxnouns.Backend/Controllers/MembersController.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 1c4a783..76eaa20 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -100,7 +100,9 @@ public class MembersController( var member = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); var errors = new List<(string, ValidationError?)>(); - if (req.Name != null) + // 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", ValidationUtils.ValidateMemberName(req.Name))); member.Name = req.Name; From 80ac16694ce2a882094907fd95cf24adfc5af4a4 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 30 Sep 2024 21:40:28 +0200 Subject: [PATCH 088/261] feat(frontend): start settings pages --- .../app/routes/$username/route.tsx | 2 +- .../app/routes/$username_.$member/route.tsx | 2 +- .../routes/auth.callback.discord/route.tsx | 13 ++ .../app/routes/settings._index/route.tsx | 18 ++ .../app/routes/settings/route.tsx | 65 ++++++ Foxnouns.Frontend/public/locales/en.json | 195 ++++++++++-------- 6 files changed, 202 insertions(+), 93 deletions(-) create mode 100644 Foxnouns.Frontend/app/routes/settings._index/route.tsx create mode 100644 Foxnouns.Frontend/app/routes/settings/route.tsx diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx index 77f0f58..c558ac5 100644 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username/route.tsx @@ -93,7 +93,7 @@ export default function UserPage() { {user.member_title || t("user.heading.members")}{" "} {isMeUser && ( // @ts-expect-error using as=Link causes an error here, even though it runs completely fine - )} diff --git a/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx b/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx index 2cb2a41..da72aed 100644 --- a/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx +++ b/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx @@ -41,7 +41,7 @@ export default function MemberPage() { You are currently viewing the public profile of {{ memberName }}.
    - Edit profile + Edit profile
    )} diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index d27d7d5..f1e8fb7 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -14,12 +14,15 @@ import { useActionData, useLoaderData, ShouldRevalidateFunction, + useNavigate, + Navigate, } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; import { Form, Button, Alert } from "react-bootstrap"; import ErrorAlert from "~/components/ErrorAlert"; import i18n from "~/i18next.server"; import { tokenCookieName } from "~/lib/utils"; +import { useEffect } from "react"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; @@ -106,6 +109,16 @@ export default function DiscordCallbackPage() { const { t } = useTranslation(); const data = useLoaderData(); const actionData = useActionData(); + const navigate = useNavigate(); + + useEffect(() => { + setTimeout(() => { + if (data.hasAccount) { + navigate(`/@${data.user!.username}`); + } + }, 2000); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); if (data.hasAccount) { const username = data.user!.username; diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx new file mode 100644 index 0000000..38ccd19 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -0,0 +1,18 @@ +import { Table } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; +import { useRouteLoaderData } from "@remix-run/react"; +import { loader as settingsLoader } from "../settings/route"; + +export default function SettingsIndex() { + const { user } = useRouteLoaderData("routes/settings")!; + const { t } = useTranslation(); + + return <> + + + + + +
    {t("settings.general.id")}{user.id}
    + +} diff --git a/Foxnouns.Frontend/app/routes/settings/route.tsx b/Foxnouns.Frontend/app/routes/settings/route.tsx new file mode 100644 index 0000000..74123e9 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/settings/route.tsx @@ -0,0 +1,65 @@ +import { LoaderFunctionArgs, json, redirect, MetaFunction } from "@remix-run/node"; +import i18n from "~/i18next.server"; +import serverRequest, { getToken } from "~/lib/request.server"; +import { User } from "~/lib/api/user"; +import { Link, Outlet, useLocation } from "@remix-run/react"; +import { Nav } from "react-bootstrap"; +import { useTranslation } from "react-i18next"; + +export const meta: MetaFunction = ({ data }) => { + return [{ title: `${data?.meta.title || "Settings"} • pronouns.cc` }]; +}; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); + const token = getToken(request); + + if (token) { + try { + const user = await serverRequest("GET", "/users/@me", { token }); + return json({ user, meta: { title: t("settings.title") } }); + } catch (e) { + return redirect("/auth/log-in"); + } + } + + return redirect("/auth/log-in"); +}; + +export default function SettingsLayout() { + const { t } = useTranslation(); + const { pathname } = useLocation(); + + return ( + <> + +
    + +
    + + ); +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index bf974b6..da882a8 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,93 +1,106 @@ { - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "An error occurred", - "more-info": "Click here for a more detailed error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up" - }, - "user": { - "avatar-alt": "Avatar for @{{username}}", - "heading": { - "names": "Names", - "pronouns": "Pronouns", - "members": "Members" - }, - "member-avatar-alt": "Avatar for {{name}}", - "member-hidden": "This member is unlisted, and not shown in your public member list.", - "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", - "create-member-button": "Create member", - "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" - }, - "member": { - "avatar-alt": "Avatar for {{name}}", - "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", - "back": "Back to {{name}}" - }, - "log-in": { - "callback": { - "title": { - "discord-success": "Log in with Discord", - "discord-register": "Register with Discord" - }, - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - }, - "welcome": { - "title": "Welcome", - "header": "Welcome to pronouns.cc!", - "blurb": "{welcome.blurb}", - "customize-profile": "Customize your profile", - "customize-profile-blurb": "{welcome.customize-profile-blurb}", - "create-members": "Create members", - "create-members-blurb": "{welcome.create-members-blurb}", - "custom-preferences": "Customize your preferences", - "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", - "profile-button": "Go to your profile" - } + "error": { + "heading": "An error occurred", + "validation": { + "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", + "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", + "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", + "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", + "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" + }, + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "An error occurred", + "more-info": "Click here for a more detailed error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up" + }, + "user": { + "avatar-alt": "Avatar for @{{username}}", + "heading": { + "names": "Names", + "pronouns": "Pronouns", + "members": "Members" + }, + "member-avatar-alt": "Avatar for {{name}}", + "member-hidden": "This member is unlisted, and not shown in your public member list.", + "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", + "create-member-button": "Create member", + "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" + }, + "member": { + "avatar-alt": "Avatar for {{name}}", + "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", + "back": "Back to {{name}}" + }, + "log-in": { + "callback": { + "title": { + "discord-success": "Log in with Discord", + "discord-register": "Register with Discord" + }, + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up", + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else." + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + }, + "welcome": { + "title": "Welcome", + "header": "Welcome to pronouns.cc!", + "blurb": "{welcome.blurb}", + "customize-profile": "Customize your profile", + "customize-profile-blurb": "{welcome.customize-profile-blurb}", + "create-members": "Create members", + "create-members-blurb": "{welcome.create-members-blurb}", + "custom-preferences": "Customize your preferences", + "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", + "profile-button": "Go to your profile" + }, + "settings": { + "general": { + "id": "Your user ID" + }, + "title": "Settings", + "nav": { + "general-information": "General information", + "profile": "Base profile", + "members": "Members", + "authentication": "Authentication", + "export": "Export your data" + } + } } From 400289332336082f62ef895531577b8837648136 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 30 Sep 2024 21:44:41 +0200 Subject: [PATCH 089/261] feat(backend): limit total members per user --- Foxnouns.Backend/Controllers/MembersController.cs | 6 ++++++ Foxnouns.Backend/Controllers/MetaController.cs | 13 +++++++++++-- Foxnouns.Backend/Utils/ValidationUtils.cs | 6 ++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 76eaa20..3564090 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -41,6 +41,8 @@ public class MembersController( return Ok(memberRenderer.RenderMember(member, CurrentToken)); } + public const int MaxMemberCount = 500; + [HttpPost("/api/v2/users/@me/members")] [ProducesResponseType(StatusCodes.Status200OK)] [Authorize("member.create")] @@ -58,6 +60,10 @@ public class MembersController( .. ValidationUtils.ValidateLinks(req.Links) ]); + var memberCount = await db.Members.CountAsync(m => m.UserId == CurrentUser.Id, ct); + if (memberCount >= MaxMemberCount) + throw new ApiError.BadRequest("Maximum number of members reached"); + var member = new Member { Id = snowflakeGenerator.GenerateSnowflake(), diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 31c505e..ffd4fe6 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -1,3 +1,4 @@ +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; namespace Foxnouns.Backend.Controllers; @@ -18,14 +19,22 @@ public class MetaController : ApiControllerBase (int)FoxnounsMetrics.UsersActiveMonthCount.Value, (int)FoxnounsMetrics.UsersActiveWeekCount.Value, (int)FoxnounsMetrics.UsersActiveDayCount.Value - )) + ), + new Limits( + MemberCount: MembersController.MaxMemberCount, + BioLength: ValidationUtils.MaxBioLength)) ); } [HttpGet("/api/v2/coffee")] public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); - private record MetaResponse(string Repository, string Version, string Hash, int Members, UserInfo Users); + private record MetaResponse(string Repository, string Version, string Hash, int Members, UserInfo Users, Limits Limits); private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); + + // All limits that the frontend should know about (for UI purposes) + private record Limits( + int MemberCount, + int BioLength); } \ No newline at end of file diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 1650860..f8f8379 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -127,12 +127,14 @@ public static class ValidationUtils return errors; } + public const int MaxBioLength = 1024; + public static ValidationError? ValidateBio(string? bio) { return bio?.Length switch { - 0 => ValidationError.LengthError("Bio is too short", 1, 1024, bio.Length), - > 1024 => ValidationError.LengthError("Bio is too long", 1, 1024, bio.Length), + 0 => ValidationError.LengthError("Bio is too short", 1, MaxBioLength, bio.Length), + > MaxBioLength => ValidationError.LengthError("Bio is too long", 1, MaxBioLength, bio.Length), _ => null }; } From 562ecc46bd64ae1142cb203c86944e50dc148687 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 30 Sep 2024 22:05:14 +0200 Subject: [PATCH 090/261] feat(frontend): grab limits from API, add created time + member count to settings --- Foxnouns.Frontend/app/env.server.ts | 6 + Foxnouns.Frontend/app/lib/api/meta.ts | 5 + Foxnouns.Frontend/app/lib/utils.ts | 4 + .../app/routes/settings._index/route.tsx | 51 ++++- Foxnouns.Frontend/package.json | 2 + Foxnouns.Frontend/public/locales/en.json | 210 +++++++++--------- Foxnouns.Frontend/yarn.lock | 10 + 7 files changed, 174 insertions(+), 114 deletions(-) diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts index 2add747..24870fe 100644 --- a/Foxnouns.Frontend/app/env.server.ts +++ b/Foxnouns.Frontend/app/env.server.ts @@ -1,5 +1,11 @@ import "dotenv/config"; import { env } from "node:process"; +import { Limits } from "~/lib/api/meta"; export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; export const LANGUAGE = env.LANGUAGE || "en"; + +const apiLimits: Limits = await fetch(`${API_BASE}/v2/meta`) + .then((resp) => resp.json()) + .then((m) => m.limits); +export const limits: Limits = Object.freeze(apiLimits); diff --git a/Foxnouns.Frontend/app/lib/api/meta.ts b/Foxnouns.Frontend/app/lib/api/meta.ts index 89f1aa3..8f67ada 100644 --- a/Foxnouns.Frontend/app/lib/api/meta.ts +++ b/Foxnouns.Frontend/app/lib/api/meta.ts @@ -9,3 +9,8 @@ export default interface Meta { }; members: number; } + +export type Limits = { + member_count: number; + bio_length: number; +}; diff --git a/Foxnouns.Frontend/app/lib/utils.ts b/Foxnouns.Frontend/app/lib/utils.ts index 89b9f0f..f91176d 100644 --- a/Foxnouns.Frontend/app/lib/utils.ts +++ b/Foxnouns.Frontend/app/lib/utils.ts @@ -1,2 +1,6 @@ +import { DateTime } from "luxon"; + export const defaultAvatarUrl = "https://pronouns.cc/default/512.webp"; export const tokenCookieName = "__Host-pronounscc-token"; +export const idTimestamp = (id: string) => + DateTime.fromMillis(parseInt(id, 10) / (1 << 22) + 1_640_995_200_000); diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx index 38ccd19..de202a3 100644 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -1,18 +1,49 @@ import { Table } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import { useRouteLoaderData } from "@remix-run/react"; +import { useLoaderData, useRouteLoaderData } from "@remix-run/react"; import { loader as settingsLoader } from "../settings/route"; +import { LoaderFunctionArgs, json } from "@remix-run/node"; +import serverRequest, { getToken } from "~/lib/request.server"; +import { PartialMember } from "~/lib/api/user"; +import { limits } from "~/env.server"; +import { DateTime } from "luxon"; +import { idTimestamp } from "~/lib/utils"; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const token = getToken(request); + const members = await serverRequest("GET", "/users/@me/members", { token }); + return json({ members, maxMemberCount: limits.member_count }); +}; export default function SettingsIndex() { + const { members, maxMemberCount } = useLoaderData(); const { user } = useRouteLoaderData("routes/settings")!; const { t } = useTranslation(); - - return <> - - - - - -
    {t("settings.general.id")}{user.id}
    - + + const createdAt = idTimestamp(user.id); + + return ( + <> + + + + + + + + + + + + + + + +
    {t("settings.general.id")} + {user.id} +
    {t("settings.general.created")}{createdAt.toLocaleString(DateTime.DATETIME_MED)}
    {t("settings.general.member-count")} + {members.length}/{maxMemberCount} +
    + + ); } diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index af6e5cd..6db78c8 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -30,6 +30,7 @@ "i18next-fs-backend": "^2.3.2", "i18next-http-backend": "^2.6.1", "isbot": "^4.1.0", + "luxon": "^3.5.0", "markdown-it": "^14.1.0", "morgan": "^1.10.0", "react": "^18.2.0", @@ -46,6 +47,7 @@ "@types/compression": "^1.7.5", "@types/cookie": "^0.6.0", "@types/express": "^4.17.21", + "@types/luxon": "^3.4.2", "@types/markdown-it": "^14.1.2", "@types/morgan": "^1.9.9", "@types/react": "^18.2.20", diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index da882a8..0baebf2 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -1,106 +1,108 @@ { - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." - }, - "title": "An error occurred", - "more-info": "Click here for a more detailed error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up" - }, - "user": { - "avatar-alt": "Avatar for @{{username}}", - "heading": { - "names": "Names", - "pronouns": "Pronouns", - "members": "Members" - }, - "member-avatar-alt": "Avatar for {{name}}", - "member-hidden": "This member is unlisted, and not shown in your public member list.", - "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", - "create-member-button": "Create member", - "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" - }, - "member": { - "avatar-alt": "Avatar for {{name}}", - "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", - "back": "Back to {{name}}" - }, - "log-in": { - "callback": { - "title": { - "discord-success": "Log in with Discord", - "discord-register": "Register with Discord" - }, - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your discord username" - }, - "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." - }, - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - }, - "welcome": { - "title": "Welcome", - "header": "Welcome to pronouns.cc!", - "blurb": "{welcome.blurb}", - "customize-profile": "Customize your profile", - "customize-profile-blurb": "{welcome.customize-profile-blurb}", - "create-members": "Create members", - "create-members-blurb": "{welcome.create-members-blurb}", - "custom-preferences": "Customize your preferences", - "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", - "profile-button": "Go to your profile" - }, - "settings": { - "general": { - "id": "Your user ID" - }, - "title": "Settings", - "nav": { - "general-information": "General information", - "profile": "Base profile", - "members": "Members", - "authentication": "Authentication", - "export": "Export your data" - } - } + "error": { + "heading": "An error occurred", + "validation": { + "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", + "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", + "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", + "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", + "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" + }, + "errors": { + "authentication-error": "There was an error validating your credentials.", + "authentication-required": "You need to log in.", + "bad-request": "Server rejected your input, please check anything for errors.", + "forbidden": "You are not allowed to perform that action.", + "generic-error": "An unknown error occurred.", + "internal-server-error": "Server experienced an internal error, please try again later.", + "member-not-found": "Member not found, please check your spelling and try again.", + "user-not-found": "User not found, please check your spelling and try again." + }, + "title": "An error occurred", + "more-info": "Click here for a more detailed error" + }, + "navbar": { + "view-profile": "View profile", + "settings": "Settings", + "log-out": "Log out", + "log-in": "Log in or sign up" + }, + "user": { + "avatar-alt": "Avatar for @{{username}}", + "heading": { + "names": "Names", + "pronouns": "Pronouns", + "members": "Members" + }, + "member-avatar-alt": "Avatar for {{name}}", + "member-hidden": "This member is unlisted, and not shown in your public member list.", + "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", + "create-member-button": "Create member", + "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" + }, + "member": { + "avatar-alt": "Avatar for {{name}}", + "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", + "back": "Back to {{name}}" + }, + "log-in": { + "callback": { + "title": { + "discord-success": "Log in with Discord", + "discord-register": "Register with Discord" + }, + "success": "Successfully logged in!", + "success-link": "Welcome back, <1>@{{username}}!", + "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", + "remote-username": { + "discord": "Your discord username" + }, + "username": "Username", + "sign-up-button": "Sign up", + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else." + }, + "title": "Log in", + "form-title": "Log in with email", + "email": "Email address", + "password": "Password", + "log-in-button": "Log in", + "register-with-email": "Register with email", + "3rd-party": { + "title": "Log in with another service", + "desc": "If you prefer, you can also log in with one of these services:", + "discord": "Log in with Discord", + "google": "Log in with Google", + "tumblr": "Log in with Tumblr" + }, + "invalid-credentials": "Invalid email address or password, please check your spelling and try again." + }, + "welcome": { + "title": "Welcome", + "header": "Welcome to pronouns.cc!", + "blurb": "{welcome.blurb}", + "customize-profile": "Customize your profile", + "customize-profile-blurb": "{welcome.customize-profile-blurb}", + "create-members": "Create members", + "create-members-blurb": "{welcome.create-members-blurb}", + "custom-preferences": "Customize your preferences", + "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", + "profile-button": "Go to your profile" + }, + "settings": { + "general": { + "id": "Your user ID", + "created": "Account created at", + "member-count": "Members" + }, + "title": "Settings", + "nav": { + "general-information": "General information", + "profile": "Base profile", + "members": "Members", + "authentication": "Authentication", + "export": "Export your data" + } + } } diff --git a/Foxnouns.Frontend/yarn.lock b/Foxnouns.Frontend/yarn.lock index 4596d08..a987e09 100644 --- a/Foxnouns.Frontend/yarn.lock +++ b/Foxnouns.Frontend/yarn.lock @@ -1341,6 +1341,11 @@ resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-5.0.0.tgz#21413001973106cda1c3a9b91eedd4ccd5469d76" integrity sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q== +"@types/luxon@^3.4.2": + version "3.4.2" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7" + integrity sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA== + "@types/markdown-it@^14.1.2": version "14.1.2" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-14.1.2.tgz#57f2532a0800067d9b934f3521429a2e8bfb4c61" @@ -4499,6 +4504,11 @@ lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== +luxon@^3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20" + integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== + markdown-extensions@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3" From b1165c37803c3a06dfd235320dfa571e12c143f6 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 14:44:34 +0200 Subject: [PATCH 091/261] refactor(frontend): extract avatar image component --- .../app/components/profile/AvatarImage.tsx | 22 +++++++++++++++++++ .../app/components/profile/BaseProfile.tsx | 13 +++++------ .../app/routes/$username/MemberCard.tsx | 9 ++++---- 3 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 Foxnouns.Frontend/app/components/profile/AvatarImage.tsx diff --git a/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx b/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx new file mode 100644 index 0000000..e29ff75 --- /dev/null +++ b/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx @@ -0,0 +1,22 @@ +export default function AvatarImage({ + src, + width, + alt, + lazyLoad, +}: { + src: string; + width: number; + alt: string; + lazyLoad?: boolean; +}) { + return ( + {alt} + ); +} diff --git a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx index a058755..2d6171e 100644 --- a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx +++ b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx @@ -6,6 +6,7 @@ import ProfileLink from "~/components/profile/ProfileLink"; import ProfileField from "~/components/profile/ProfileField"; import { useTranslation } from "react-i18next"; import { renderMarkdown } from "~/lib/markdown"; +import AvatarImage from "~/components/profile/AvatarImage"; export type Props = { name: string; @@ -31,20 +32,16 @@ export default function BaseProfile({
    {userI18nKeys ? ( - {t("user.avatar-alt", ) : ( - {t("member.avatar-alt", )} {profile.flags && profile.bio && ( diff --git a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx index bc6e516..b112a08 100644 --- a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx +++ b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx @@ -10,6 +10,7 @@ import { defaultAvatarUrl } from "~/lib/utils"; import { useTranslation } from "react-i18next"; import { OverlayTrigger, Tooltip } from "react-bootstrap"; import { Lock } from "react-bootstrap-icons"; +import AvatarImage from "~/components/profile/AvatarImage"; export default function MemberCard({ user, member }: { user: PartialUser; member: PartialMember }) { const { t } = useTranslation(); @@ -37,13 +38,11 @@ export default function MemberCard({ user, member }: { user: PartialUser; member return (
    - {t("user.member-avatar-alt",

    From 5a8b7aae80bc21f035fcc8a15ef4d6544a665926 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 16:04:36 +0200 Subject: [PATCH 092/261] fix(backend): fix username regex accepting characters with diacritics --- Foxnouns.Backend/Utils/ValidationUtils.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index f8f8379..392e5ed 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -7,13 +7,8 @@ namespace Foxnouns.Backend.Utils; ///

    /// Static methods for validating user input (mostly making sure it's not too short or too long) /// -public static class ValidationUtils +public static partial class ValidationUtils { - private static readonly Regex UsernameRegex = new("^[\\w-.]{2,40}$", RegexOptions.IgnoreCase); - - private static readonly Regex MemberRegex = - new("^[^@\\?!#/\\\\[\\]\"\\{\\}'$%&()+<=>^|~`,\\*]{1,100}$", RegexOptions.IgnoreCase); - private static readonly string[] InvalidUsernames = [ "..", @@ -40,7 +35,7 @@ public static class ValidationUtils public static ValidationError? ValidateUsername(string username) { - if (!UsernameRegex.IsMatch(username)) + if (!UsernameRegex().IsMatch(username)) return username.Length switch { < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), @@ -57,7 +52,7 @@ public static class ValidationUtils public static ValidationError? ValidateMemberName(string memberName) { - if (!MemberRegex.IsMatch(memberName)) + if (!MemberRegex().IsMatch(memberName)) return memberName.Length switch { < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), @@ -128,7 +123,7 @@ public static class ValidationUtils } public const int MaxBioLength = 1024; - + public static ValidationError? ValidateBio(string? bio) { return bio?.Length switch @@ -291,4 +286,9 @@ public static class ValidationUtils return errors; } + + [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] + private static partial Regex UsernameRegex(); + [GeneratedRegex("""^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", RegexOptions.IgnoreCase, "en-NL")] + private static partial Regex MemberRegex(); } \ No newline at end of file From 2a66e3e25e1393e4bbf494b6238546fa5ccbaaf8 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 16:06:02 +0200 Subject: [PATCH 093/261] feat(frontend): add username editing --- .../Controllers/MetaController.cs | 14 +- .../Controllers/UsersController.cs | 6 +- .../Services/UserRendererService.cs | 3 + Foxnouns.Frontend/app/app.scss | 5 + Foxnouns.Frontend/app/lib/api/meta.ts | 2 + Foxnouns.Frontend/app/lib/api/user.ts | 16 +++ .../routes/auth.callback.discord/route.tsx | 1 - .../app/routes/settings._index/route.tsx | 121 ++++++++++++++++-- .../app/routes/settings/route.tsx | 4 +- Foxnouns.Frontend/public/locales/en.json | 15 ++- 10 files changed, 164 insertions(+), 23 deletions(-) diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index ffd4fe6..53a38e7 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -22,19 +22,27 @@ public class MetaController : ApiControllerBase ), new Limits( MemberCount: MembersController.MaxMemberCount, - BioLength: ValidationUtils.MaxBioLength)) + BioLength: ValidationUtils.MaxBioLength, + CustomPreferences: UsersController.MaxCustomPreferences)) ); } [HttpGet("/api/v2/coffee")] public IActionResult BrewCoffee() => Problem("Sorry, I'm a teapot!", statusCode: StatusCodes.Status418ImATeapot); - private record MetaResponse(string Repository, string Version, string Hash, int Members, UserInfo Users, Limits Limits); + private record MetaResponse( + string Repository, + string Version, + string Hash, + int Members, + UserInfo Users, + Limits Limits); private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); // All limits that the frontend should know about (for UI purposes) private record Limits( int MemberCount, - int BioLength); + int BioLength, + int CustomPreferences); } \ No newline at end of file diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index e292fc3..9a182f4 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -177,14 +177,16 @@ public class UsersController( public bool Favourite { get; set; } } + public const int MaxCustomPreferences = 25; + private static List<(string, ValidationError?)> ValidateCustomPreferences( List preferences) { var errors = new List<(string, ValidationError?)>(); - if (preferences.Count > 25) + if (preferences.Count > MaxCustomPreferences) errors.Add(("custom_preferences", - ValidationError.LengthError("Too many custom preferences", 0, 25, preferences.Count))); + ValidationError.LengthError("Too many custom preferences", 0, MaxCustomPreferences, preferences.Count))); if (preferences.Count > 50) return errors; // TODO: validate individual preferences diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 95d40d3..ee64fe1 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -43,6 +43,7 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe user.Links, user.Names, user.Pronouns, user.Fields, user.CustomPreferences, flags.Select(f => RenderPrideFlag(f.PrideFlag)), + user.Role, renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, renderAuthMethods ? authMethods.Select(a => new AuthenticationMethodResponse( @@ -78,6 +79,8 @@ public class UserRendererService(DatabaseContext db, MemberRendererService membe IEnumerable Fields, Dictionary CustomPreferences, IEnumerable Flags, + [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] + UserRole Role, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] diff --git a/Foxnouns.Frontend/app/app.scss b/Foxnouns.Frontend/app/app.scss index 0d30b1e..50b0ad0 100644 --- a/Foxnouns.Frontend/app/app.scss +++ b/Foxnouns.Frontend/app/app.scss @@ -27,3 +27,8 @@ max-width: 200px; border-radius: 3px; } + +// This is necessary for line breaks in translation strings to show up. Don't ask me why +.text-has-newline { + white-space: pre-line; +} diff --git a/Foxnouns.Frontend/app/lib/api/meta.ts b/Foxnouns.Frontend/app/lib/api/meta.ts index 8f67ada..5f2bd11 100644 --- a/Foxnouns.Frontend/app/lib/api/meta.ts +++ b/Foxnouns.Frontend/app/lib/api/meta.ts @@ -8,9 +8,11 @@ export default interface Meta { active_day: number; }; members: number; + limits: Limits; } export type Limits = { member_count: number; bio_length: number; + custom_preferences: number; }; diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts index 0b6f375..4f07301 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -14,6 +14,14 @@ export type User = PartialUser & { pronouns: Pronoun[]; fields: Field[]; flags: PrideFlag[]; + role: "USER" | "MODERATOR" | "ADMIN"; +}; + +export type MeUser = UserWithMembers & { + auth_methods: AuthMethod[]; + member_list_hidden: boolean; + last_active: string; + last_sid_reroll: string; }; export type UserWithMembers = User & { members: PartialMember[] }; @@ -58,6 +66,14 @@ export type PrideFlag = { description: string | null; }; +export type AuthMethod = { + id: string; + type: "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; + remote_id: string; + remote_username?: string; + fediverse_instance?: string; +}; + export type CustomPreference = { icon: string; tooltip: string; diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index f1e8fb7..c5200fd 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -15,7 +15,6 @@ import { useLoaderData, ShouldRevalidateFunction, useNavigate, - Navigate, } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; import { Form, Button, Alert } from "react-bootstrap"; diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx index de202a3..d575108 100644 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -1,30 +1,94 @@ -import { Table } from "react-bootstrap"; +import { Button, Form, InputGroup, Table } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import { useLoaderData, useRouteLoaderData } from "@remix-run/react"; +import { Form as RemixForm, useActionData, useRouteLoaderData } from "@remix-run/react"; import { loader as settingsLoader } from "../settings/route"; -import { LoaderFunctionArgs, json } from "@remix-run/node"; -import serverRequest, { getToken } from "~/lib/request.server"; -import { PartialMember } from "~/lib/api/user"; -import { limits } from "~/env.server"; +import { loader as rootLoader } from "../../root"; import { DateTime } from "luxon"; -import { idTimestamp } from "~/lib/utils"; +import { defaultAvatarUrl, idTimestamp } from "~/lib/utils"; +import { ExclamationTriangleFill, InfoCircleFill } from "react-bootstrap-icons"; +import AvatarImage from "~/components/profile/AvatarImage"; +import { ActionFunctionArgs, json } from "@remix-run/node"; +import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; +import serverRequest, { getToken } from "~/lib/request.server"; +import { MeUser } from "~/lib/api/user"; +import ErrorAlert from "~/components/ErrorAlert"; -export const loader = async ({ request }: LoaderFunctionArgs) => { +export const action = async ({ request }: ActionFunctionArgs) => { + const data = await request.formData(); + const username = data.get("username") as string | null; const token = getToken(request); - const members = await serverRequest("GET", "/users/@me/members", { token }); - return json({ members, maxMemberCount: limits.member_count }); + + if (!username) { + return json({ + error: { + status: 403, + code: ErrorCode.BadRequest, + message: "Invalid username", + } as ApiError, + user: null, + }); + } + + try { + const resp = await serverRequest("PATCH", "/users/@me", { body: { username }, token }); + + return json({ user: resp, error: null }); + } catch (e) { + return json({ error: e as ApiError, user: null }); + } }; export default function SettingsIndex() { - const { members, maxMemberCount } = useLoaderData(); const { user } = useRouteLoaderData("routes/settings")!; + const actionData = useActionData(); + const { meta } = useRouteLoaderData("root")!; const { t } = useTranslation(); const createdAt = idTimestamp(user.id); return ( <> - +
    +
    + + + + {t("settings.general.username")} + + + + + + + + +

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

    + {actionData?.error && } +
    +
    + +
    +
    +
    +

    {t("settings.general.log-out-everywhere")}

    +

    +
    +

    {t("settings.general.table-header")}

    +
    @@ -39,7 +103,23 @@ export default function SettingsIndex() { + + + + + + + + + + + + @@ -47,3 +127,18 @@ export default function SettingsIndex() { ); } + +function UsernameUpdateError({ error }: { error: ApiError }) { + const { t } = useTranslation(); + const usernameError = firstErrorFor(error, "username"); + if (!usernameError) { + return ; + } + + return ( +

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

    + ); +} diff --git a/Foxnouns.Frontend/app/routes/settings/route.tsx b/Foxnouns.Frontend/app/routes/settings/route.tsx index 74123e9..3ef35fd 100644 --- a/Foxnouns.Frontend/app/routes/settings/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings/route.tsx @@ -1,7 +1,7 @@ import { LoaderFunctionArgs, json, redirect, MetaFunction } from "@remix-run/node"; import i18n from "~/i18next.server"; import serverRequest, { getToken } from "~/lib/request.server"; -import { User } from "~/lib/api/user"; +import { MeUser } from "~/lib/api/user"; import { Link, Outlet, useLocation } from "@remix-run/react"; import { Nav } from "react-bootstrap"; import { useTranslation } from "react-i18next"; @@ -16,7 +16,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { if (token) { try { - const user = await serverRequest("GET", "/users/@me", { token }); + const user = await serverRequest("GET", "/users/@me", { token }); return json({ user, meta: { title: t("settings.title") } }); } catch (e) { return redirect("/auth/log-in"); diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 0baebf2..279173b 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -92,9 +92,18 @@ }, "settings": { "general": { + "username": "Username", + "change-username": "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.", + "log-out-everywhere": "Log out everywhere", + "table-header": "General account information", "id": "Your user ID", "created": "Account created at", - "member-count": "Members" + "member-count": "Members", + "member-list-hidden": "Member list hidden?", + "custom-preferences": "Custom preferences", + "role": "Account role", + "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}" }, "title": "Settings", "nav": { @@ -104,5 +113,7 @@ "authentication": "Authentication", "export": "Export your data" } - } + }, + "yes": "Yes", + "no": "No" } From 3f8fe307abb1fa341de7960b22ce4dbffa6b38f6 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 16:19:04 +0200 Subject: [PATCH 094/261] fix(frontend): remove unused limits object from env.server --- Foxnouns.Frontend/app/env.server.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts index 24870fe..2add747 100644 --- a/Foxnouns.Frontend/app/env.server.ts +++ b/Foxnouns.Frontend/app/env.server.ts @@ -1,11 +1,5 @@ import "dotenv/config"; import { env } from "node:process"; -import { Limits } from "~/lib/api/meta"; export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; export const LANGUAGE = env.LANGUAGE || "en"; - -const apiLimits: Limits = await fetch(`${API_BASE}/v2/meta`) - .then((resp) => resp.json()) - .then((m) => m.limits); -export const limits: Limits = Object.freeze(apiLimits); From 9b55747657ca446a0d295055cf40f11e2bb56d72 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 16:27:44 +0200 Subject: [PATCH 095/261] fix(frontend): only cache locale files for a minute --- Foxnouns.Frontend/server.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Frontend/server.js b/Foxnouns.Frontend/server.js index e9e6c4b..f9bc18c 100644 --- a/Foxnouns.Frontend/server.js +++ b/Foxnouns.Frontend/server.js @@ -34,9 +34,13 @@ if (viteDevServer) { app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" })); } +// Only cache locales for a minute, as they can change without the filename changing +// TODO: figure out how to change the filenames on update? +app.use(express.static("build/client/locales", { maxAge: "1m" })); + // Everything else (like favicon.ico) is cached for an hour. You may want to be // more aggressive with this caching. -app.use(express.static("build/client", { maxAge: "1h" })); +app.use(express.static("build/client", { maxAge: "1d" })); app.use(morgan("tiny")); From c18b79e570ce3db476549375f0dfdcd6696934d8 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 16:30:51 +0200 Subject: [PATCH 096/261] sam struggles with caching 2024 colorized --- Foxnouns.Frontend/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Foxnouns.Frontend/server.js b/Foxnouns.Frontend/server.js index f9bc18c..88ce3db 100644 --- a/Foxnouns.Frontend/server.js +++ b/Foxnouns.Frontend/server.js @@ -36,7 +36,7 @@ if (viteDevServer) { // Only cache locales for a minute, as they can change without the filename changing // TODO: figure out how to change the filenames on update? -app.use(express.static("build/client/locales", { maxAge: "1m" })); +app.use("/locales", express.static("build/client/locales", { maxAge: "1m" })); // Everything else (like favicon.ico) is cached for an hour. You may want to be // more aggressive with this caching. From 42041d49bccec3af28994537c9f47fc9dae5412b Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 21:25:51 +0200 Subject: [PATCH 097/261] feat: add force log out endpoint --- .../Controllers/InternalController.cs | 20 +++++++++++++++- Foxnouns.Frontend/app/env.server.ts | 1 + Foxnouns.Frontend/app/lib/request.server.ts | 24 +++++++++++++++---- .../app/routes/settings._index/route.tsx | 20 ++++++++-------- .../routes/settings.force-log-out/route.tsx | 19 +++++++++++++++ Foxnouns.Frontend/public/locales/en.json | 2 ++ docker-compose.yml | 1 + 7 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index b79de1c..2048f59 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -1,17 +1,35 @@ using System.Text.RegularExpressions; using Foxnouns.Backend.Database; +using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing.Template; +using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers; [ApiController] [Route("/api/internal")] -public partial class InternalController(DatabaseContext db) : ControllerBase +public partial class InternalController(ILogger logger, DatabaseContext db) : ControllerBase { + private readonly ILogger _logger = logger.ForContext(); + + [HttpPost("force-log-out")] + [Authenticate] + [Authorize("identify")] + public async Task ForceLogoutAsync() + { + var user = HttpContext.GetUser()!; + + _logger.Information("Invalidating all tokens for user {UserId}", user.Id); + await db.Tokens.Where(t => t.UserId == user.Id) + .ExecuteUpdateAsync(s => s.SetProperty(t => t.ManuallyExpired, true)); + + return NoContent(); + } + [GeneratedRegex(@"(\{\w+\})")] private static partial Regex PathVarRegex(); diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts index 2add747..5e5e84b 100644 --- a/Foxnouns.Frontend/app/env.server.ts +++ b/Foxnouns.Frontend/app/env.server.ts @@ -2,4 +2,5 @@ import "dotenv/config"; import { env } from "node:process"; export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; +export const INTERNAL_API_BASE = env.INTERNAL_API_BASE || "https://localhost:5000/api"; export const LANGUAGE = env.LANGUAGE || "en"; diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 4648d5f..7777422 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -1,5 +1,5 @@ import { parse as parseCookie, serialize as serializeCookie } from "cookie"; -import { API_BASE } from "~/env.server"; +import { API_BASE, INTERNAL_API_BASE } from "~/env.server"; import { ApiError, ErrorCode } from "./api/error"; import { tokenCookieName } from "~/lib/utils"; @@ -8,14 +8,17 @@ export type RequestParams = { // eslint-disable-next-line @typescript-eslint/no-explicit-any body?: any; headers?: Record; + isInternal?: boolean; }; -export default async function serverRequest( +async function requestInternal( method: string, path: string, params: RequestParams = {}, -) { - const url = `${API_BASE}/v2${path}`; +): Promise { + const base = params.isInternal ? INTERNAL_API_BASE : API_BASE + "/v2"; + + const url = `${base}${path}`; const resp = await fetch(url, { method, body: params.body ? JSON.stringify(params.body) : undefined, @@ -37,6 +40,19 @@ export default async function serverRequest( } if (resp.status < 200 || resp.status >= 400) throw (await resp.json()) as ApiError; + return resp; +} + +export async function fastRequest(method: string, path: string, params: RequestParams = {}) { + await requestInternal(method, path, params); +} + +export default async function serverRequest( + method: string, + path: string, + params: RequestParams = {}, +) { + const resp = await requestInternal(method, path, params); return (await resp.json()) as T; } diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx index d575108..5b90851 100644 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -1,6 +1,6 @@ import { Button, Form, InputGroup, Table } from "react-bootstrap"; import { useTranslation } from "react-i18next"; -import { Form as RemixForm, useActionData, useRouteLoaderData } from "@remix-run/react"; +import { Form as RemixForm, useActionData, useFetcher, useRouteLoaderData } from "@remix-run/react"; import { loader as settingsLoader } from "../settings/route"; import { loader as rootLoader } from "../../root"; import { DateTime } from "luxon"; @@ -43,6 +43,7 @@ export default function SettingsIndex() { const actionData = useActionData(); const { meta } = useRouteLoaderData("root")!; const { t } = useTranslation(); + const fetcher = useFetcher(); const createdAt = idTimestamp(user.id); @@ -55,13 +56,7 @@ export default function SettingsIndex() { {t("settings.general.username")} - + @@ -85,9 +80,14 @@ export default function SettingsIndex() {

    {t("settings.general.log-out-everywhere")}

    -

    +

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

    + + +
    -

    {t("settings.general.table-header")}

    +

    {t("settings.general.table-header")}

    {t("settings.general.id")}
    {t("settings.general.member-count")} - {members.length}/{maxMemberCount} + {user.members.length}/{meta.limits.member_count} +
    {t("settings.general.member-list-hidden")}{user.member_list_hidden ? t("yes") : t("no")}
    {t("settings.general.custom-preferences")} + {Object.keys(user.custom_preferences).length}/{meta.limits.custom_preferences} +
    {t("settings.general.role")} + {user.role}
    diff --git a/Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx b/Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx new file mode 100644 index 0000000..caa9d4a --- /dev/null +++ b/Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx @@ -0,0 +1,19 @@ +import { ActionFunction, redirect } from "@remix-run/node"; +import { fastRequest, getToken, writeCookie } from "~/lib/request.server"; +import { tokenCookieName } from "~/lib/utils"; + +export const action: ActionFunction = async ({ request }) => { + const token = getToken(request); + if (!token) + return redirect("/", { + status: 303, + headers: { "Set-Cookie": writeCookie(tokenCookieName, "token", 0) }, + }); + + await fastRequest("POST", "/internal/force-log-out", { token, isInternal: true }); + + return redirect("/", { + status: 303, + headers: { "Set-Cookie": writeCookie(tokenCookieName, "token", 0) }, + }); +}; diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 279173b..c35a1d7 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -96,6 +96,8 @@ "change-username": "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.", "log-out-everywhere": "Log out everywhere", + "log-out-everywhere-hint": "If you think one of your tokens might have been compromised, you can log out on all devices by clicking this button.", + "force-log-out-button": "Force log out", "table-header": "General account information", "id": "Your user ID", "created": "Account created at", diff --git a/docker-compose.yml b/docker-compose.yml index 7176fc2..6fafd18 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: build: ./Foxnouns.Frontend environment: - "API_BASE=http://rate:5003/api" + - "INTERNAL_API_BASE=http://backend:5000/api" restart: unless-stopped volumes: - ./docker/frontend.env:/app/.env From aa756ac56ad93255d0e3af1514f71b412dc2c5c4 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 21:58:13 +0200 Subject: [PATCH 098/261] chore(backend): format --- .../Controllers/Authentication/EmailAuthController.cs | 2 +- Foxnouns.Backend/Controllers/UsersController.cs | 3 ++- Foxnouns.Backend/Utils/ValidationUtils.cs | 1 + 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 1649948..18fbacc 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -125,7 +125,7 @@ public class EmailAuthController( private void CheckRequirements() { - if (!config.DiscordAuth.Enabled) + if (!config.EmailAuth.Enabled) throw new ApiError.BadRequest("Email authentication is not enabled on this instance."); } diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 9a182f4..fb0e301 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -186,7 +186,8 @@ public class UsersController( if (preferences.Count > MaxCustomPreferences) errors.Add(("custom_preferences", - ValidationError.LengthError("Too many custom preferences", 0, MaxCustomPreferences, preferences.Count))); + ValidationError.LengthError("Too many custom preferences", 0, MaxCustomPreferences, + preferences.Count))); if (preferences.Count > 50) return errors; // TODO: validate individual preferences diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 392e5ed..2dd52b9 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -289,6 +289,7 @@ public static partial class ValidationUtils [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] private static partial Regex UsernameRegex(); + [GeneratedRegex("""^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", RegexOptions.IgnoreCase, "en-NL")] private static partial Regex MemberRegex(); } \ No newline at end of file From eac0a17473a6bdb0a2b329a9062dbc0255a6bc39 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 1 Oct 2024 22:35:17 +0200 Subject: [PATCH 099/261] chore: add husky + prettier/dotnet format pre-commit --- .config/dotnet-tools.json | 13 +++++++++++ .husky/pre-commit | 22 +++++++++++++++++++ .husky/task-runner.json | 21 ++++++++++++++++++ .../.idea/jsLinters/eslint.xml | 1 + .idea/.idea.Foxnouns.NET/.idea/prettier.xml | 2 +- package.json | 3 ++- 6 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100755 .husky/pre-commit create mode 100644 .husky/task-runner.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..e6df774 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "husky": { + "version": "0.7.1", + "commands": [ + "husky" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..fd85d23 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,22 @@ +#!/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 new file mode 100644 index 0000000..bb845ca --- /dev/null +++ b/.husky/task-runner.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", + "tasks": [ + { + "name": "run-prettier", + "command": "yarn", + "args": [ + "format", + "${staged}" + ], + "pathMode": "absolute" + }, + { + "name": "dotnet-format", + "command": "dotnet", + "args": [ + "format" + ] + } + ] +} diff --git a/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml b/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml index 204acf7..5f8621e 100644 --- a/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml +++ b/.idea/.idea.Foxnouns.NET/.idea/jsLinters/eslint.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/.idea/.idea.Foxnouns.NET/.idea/prettier.xml b/.idea/.idea.Foxnouns.NET/.idea/prettier.xml index 653a9e0..ffcf89b 100644 --- a/.idea/.idea.Foxnouns.NET/.idea/prettier.xml +++ b/.idea/.idea.Foxnouns.NET/.idea/prettier.xml @@ -1,7 +1,7 @@ -
    diff --git a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx index 22d2fcd..a8ca303 100644 --- a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx @@ -44,7 +44,7 @@ function EmailSettings({ user }: { user: MeUser }) { )} {emails.length < 3 && (

    - {/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */} + {/* @ts-expect-error as=Link */} + {/* @ts-expect-error as=Link */} + + + + + ); +} diff --git a/Foxnouns.Frontend/app/routes/settings/route.tsx b/Foxnouns.Frontend/app/routes/settings/route.tsx index 3ef35fd..9d8247a 100644 --- a/Foxnouns.Frontend/app/routes/settings/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings/route.tsx @@ -30,30 +30,35 @@ export default function SettingsLayout() { const { t } = useTranslation(); const { pathname } = useLocation(); + const isActive = (matches: string[] | string, startsWith: boolean = false) => + startsWith + ? typeof matches === "string" + ? pathname.startsWith(matches) + : matches.some((m) => pathname.startsWith(m)) + : typeof matches === "string" + ? matches === pathname + : matches.includes(pathname); + return ( <>

    diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 5a25098..c834e95 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -105,7 +105,8 @@ "member-list-hidden": "Member list hidden?", "custom-preferences": "Custom preferences", "role": "Account role", - "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}" + "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}", + "log-out-everywhere-confirm": "Are you sure you want to log out everywhere?\nPlease double check your authentication methods before doing so, as it might lock you out of your account." }, "auth": { "title": "Authentication", From 567e7941543fe830c083beadd876544f5d9406f6 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 2 Oct 2024 21:05:52 +0200 Subject: [PATCH 106/261] feat(frontend): hide everything email related if it's disabled on the backend --- .../Authentication/AuthController.cs | 4 +- .../Authentication/EmailAuthController.cs | 4 ++ Foxnouns.Frontend/app/lib/api/auth.ts | 1 + .../app/routes/auth.log-in/route.tsx | 62 ++++++++++--------- .../app/routes/settings.auth/route.tsx | 15 ++--- 5 files changed, 48 insertions(+), 38 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 53aa171..1a737eb 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -39,10 +39,10 @@ public class AuthController( + $"&prompt=none&state={state}" + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; - return Ok(new UrlsResponse(discord, null, null)); + return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); } - private record UrlsResponse(string? Discord, string? Google, string? Tumblr); + private record UrlsResponse(bool EmailEnabled, string? Discord, string? Google, string? Tumblr); public record AuthResponse( UserRendererService.UserResponse User, diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 937ab3a..7e3706e 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -100,6 +100,8 @@ public class EmailAuthController( [FromBody] CompleteRegistrationRequest req ) { + CheckRequirements(); + var email = await keyCacheService.GetKeyAsync($"email:{req.Ticket}"); if (email == null) throw new ApiError.BadRequest("Unknown ticket", "ticket", req.Ticket); @@ -185,6 +187,8 @@ public class EmailAuthController( [Authorize("*")] public async Task AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) { + CheckRequirements(); + var emails = await db .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Email) .ToListAsync(); diff --git a/Foxnouns.Frontend/app/lib/api/auth.ts b/Foxnouns.Frontend/app/lib/api/auth.ts index a0d5bf1..0f8ce27 100644 --- a/Foxnouns.Frontend/app/lib/api/auth.ts +++ b/Foxnouns.Frontend/app/lib/api/auth.ts @@ -16,6 +16,7 @@ export type CallbackResponse = { }; export type AuthUrls = { + email_enabled: boolean; discord?: string; google?: string; tumblr?: string; diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index eadbaa9..aaffd80 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -11,7 +11,7 @@ import { useActionData, useLoaderData, } from "@remix-run/react"; -import { Form, Button, ButtonGroup, ListGroup, Row, Col } from "react-bootstrap"; +import { Form, Button, ButtonGroup, ListGroup } from "react-bootstrap"; import { useTranslation } from "react-i18next"; import i18n from "~/i18next.server"; import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; @@ -78,33 +78,36 @@ export default function LoginPage() { return ( <> - - -

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

    - {actionData?.error && } - -
    - - {t("log-in.email")} - - - - {t("log-in.password")} - - +
    + {!urls.email_enabled &&
    } + {urls.email_enabled && ( +
    +

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

    + {actionData?.error && } + + + + {t("log-in.email")} + + + + {t("log-in.password")} + + - - - - - - - -
    + + + + + + + + )} +

    {t("log-in.3rd-party.title")}

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

    @@ -124,8 +127,9 @@ export default function LoginPage() { )} - - +
    + {!urls.email_enabled &&
    } + ); } diff --git a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx index a8ca303..125f413 100644 --- a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx @@ -1,10 +1,12 @@ import i18n from "~/i18next.server"; import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link, useRouteLoaderData } from "@remix-run/react"; +import { Link, useLoaderData, useRouteLoaderData } from "@remix-run/react"; import { Button, ListGroup } from "react-bootstrap"; import { loader as settingsLoader } from "~/routes/settings/route"; import { useTranslation } from "react-i18next"; import { AuthMethod, MeUser } from "~/lib/api/user"; +import serverRequest from "~/lib/request.server"; +import { AuthUrls } from "~/lib/api/auth"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }]; @@ -12,17 +14,16 @@ export const meta: MetaFunction = ({ data }) => { export const loader = async ({ request }: LoaderFunctionArgs) => { const t = await i18n.getFixedT(request); - return { meta: { title: t("settings.auth.title") } }; + const urls = await serverRequest("POST", "/auth/urls", { isInternal: true }); + + return { urls, meta: { title: t("settings.auth.title") } }; }; export default function AuthSettings() { + const { urls } = useLoaderData(); const { user } = useRouteLoaderData("routes/settings")!; - return ( -
    - -
    - ); + return
    {urls.email_enabled && }
    ; } function EmailSettings({ user }: { user: MeUser }) { From a4ca0902a3d4f8353a5a43d04dbe35428292d007 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 3 Oct 2024 16:53:26 +0200 Subject: [PATCH 107/261] fix(frontend): proxy authenticated non-GET requests through rate limiter --- Foxnouns.Frontend/app/lib/request.server.ts | 15 ++++++++++----- .../app/routes/settings._index/route.tsx | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 7da3e84..8dab154 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -1,5 +1,5 @@ import { parse as parseCookie, serialize as serializeCookie } from "cookie"; -import { INTERNAL_API_BASE } from "~/env.server"; +import { API_BASE, INTERNAL_API_BASE } from "~/env.server"; import { ApiError, ErrorCode } from "./api/error"; import { tokenCookieName } from "~/lib/utils"; @@ -11,12 +11,17 @@ export type RequestParams = { isInternal?: boolean; }; +export type RequestMethod = "GET" | "POST" | "PATCH" | "DELETE"; + export async function baseRequest( - method: string, + method: RequestMethod, path: string, params: RequestParams = {}, ): Promise { - const base = params.isInternal ? INTERNAL_API_BASE + "/internal" : INTERNAL_API_BASE + "/v2"; + // Internal requests, unauthenticated requests, and GET requests bypass the rate limiting proxy. + // All other requests go through the proxy, and are rate limited. + let base = params.isInternal || !params.token || method === "GET" ? INTERNAL_API_BASE : API_BASE; + base += params.isInternal ? "/internal" : "/v2"; const url = `${base}${path}`; const resp = await fetch(url, { @@ -43,12 +48,12 @@ export async function baseRequest( return resp; } -export async function fastRequest(method: string, path: string, params: RequestParams = {}) { +export async function fastRequest(method: RequestMethod, path: string, params: RequestParams = {}) { await baseRequest(method, path, params); } export default async function serverRequest( - method: string, + method: RequestMethod, path: string, params: RequestParams = {}, ) { diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx index 88655fc..2829098 100644 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings._index/route.tsx @@ -5,7 +5,6 @@ import { Link, Outlet, useActionData, - useFetcher, useRouteLoaderData, } from "@remix-run/react"; import { loader as settingsLoader } from "../settings/route"; From 0077a165b51dfccf4d87e9cccefe1a3540611e14 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 6 Oct 2024 15:34:31 +0200 Subject: [PATCH 108/261] feat: add some fediverse authentication code * create applications on instances * generate authorize URLs * exchange oauth code for token and user info (untested) * recreate mastodon app on authentication failure --- .../Authentication/FediverseAuthController.cs | 25 ++ ...20241006125003_AddFediverseAccessTokens.cs | 40 +++ .../DatabaseContextModelSnapshot.cs | 8 + .../Database/Models/FediverseApplication.cs | 6 + Foxnouns.Backend/ExpectedError.cs | 9 + .../Extensions/WebApplicationExtensions.cs | 1 + .../Services/FediverseAuthService.Mastodon.cs | 246 ++++++++++++++++++ .../Services/FediverseAuthService.cs | 162 ++++++++++++ 8 files changed, 497 insertions(+) create mode 100644 Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20241006125003_AddFediverseAccessTokens.cs create mode 100644 Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs create mode 100644 Foxnouns.Backend/Services/FediverseAuthService.cs diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs new file mode 100644 index 0000000..fdd10b7 --- /dev/null +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -0,0 +1,25 @@ +using Foxnouns.Backend.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Foxnouns.Backend.Controllers.Authentication; + +[Route("/api/internal/auth/fediverse")] +public class FediverseAuthController(FediverseAuthService fediverseAuthService) : ApiControllerBase +{ + [HttpGet] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task GetFediverseUrlAsync([FromQuery] string instance) + { + var url = await fediverseAuthService.GenerateAuthUrlAsync(instance); + return Ok(new FediverseUrlResponse(url)); + } + + public async Task FediverseCallbackAsync([FromBody] CallbackRequest req) + { + throw new NotImplementedException(); + } + + public record CallbackRequest(string Instance, string Code); + + private record FediverseUrlResponse(string Url); +} diff --git a/Foxnouns.Backend/Database/Migrations/20241006125003_AddFediverseAccessTokens.cs b/Foxnouns.Backend/Database/Migrations/20241006125003_AddFediverseAccessTokens.cs new file mode 100644 index 0000000..37023f0 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20241006125003_AddFediverseAccessTokens.cs @@ -0,0 +1,40 @@ +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index e1e05c2..97316ac 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -107,6 +107,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("bigint") .HasColumnName("id"); + b.Property("AccessToken") + .HasColumnType("text") + .HasColumnName("access_token"); + b.Property("ClientId") .IsRequired() .HasColumnType("text") @@ -126,6 +130,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("integer") .HasColumnName("instance_type"); + b.Property("TokenValidUntil") + .HasColumnType("timestamp with time zone") + .HasColumnName("token_valid_until"); + b.HasKey("Id") .HasName("pk_fediverse_applications"); diff --git a/Foxnouns.Backend/Database/Models/FediverseApplication.cs b/Foxnouns.Backend/Database/Models/FediverseApplication.cs index 6dc813d..fa7b6a6 100644 --- a/Foxnouns.Backend/Database/Models/FediverseApplication.cs +++ b/Foxnouns.Backend/Database/Models/FediverseApplication.cs @@ -1,3 +1,5 @@ +using NodaTime; + namespace Foxnouns.Backend.Database.Models; public class FediverseApplication : BaseModel @@ -6,6 +8,10 @@ public class FediverseApplication : BaseModel public required string ClientId { get; set; } public required string ClientSecret { get; set; } public required FediverseInstanceType InstanceType { get; set; } + + // These are for ensuring the application is still valid. + public string? AccessToken { get; set; } + public Instant? TokenValidUntil { get; set; } } public enum FediverseInstanceType diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index fdd0b5d..1b92a7e 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -14,6 +14,15 @@ public class FoxnounsError(string message, Exception? inner = null) : Exception( 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( diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 3e6926c..b2e519d 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -101,6 +101,7 @@ public static class WebApplicationExtensions .AddScoped() .AddScoped() .AddScoped() + .AddScoped() .AddScoped() // Background services .AddHostedService() diff --git a/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs new file mode 100644 index 0000000..c8d9dd5 --- /dev/null +++ b/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs @@ -0,0 +1,246 @@ +using System.Net; +using System.Web; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Minio.DataModel.ILM; +using Duration = NodaTime.Duration; +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Foxnouns.Backend.Services; + +public partial class FediverseAuthService +{ + private string MastodonRedirectUri(string instance) => + $"{_config.BaseUrl}/auth/login/mastodon/{instance}"; + + private async Task CreateMastodonApplicationAsync( + string instance, + Snowflake? existingAppId = null + ) + { + var resp = await _client.PostAsync( + $"https://{instance}/api/v1/apps", + new FormUrlEncodedContent( + new Dictionary + { + { "client_name", $"pronouns.cc (+{_config.BaseUrl})" }, + { "redirect_uris", MastodonRedirectUri(instance) }, + { "scope", "read:accounts" }, + { "website", _config.BaseUrl }, + } + ) + ); + resp.EnsureSuccessStatusCode(); + + var mastodonApp = await resp.Content.ReadFromJsonAsync(); + if (mastodonApp == null) + throw new FoxnounsError( + $"Application created on Mastodon-compatible instance {instance} was null" + ); + + var token = await GetMastodonAppTokenAsync( + instance, + mastodonApp.ClientId, + mastodonApp.ClientSecret + ); + + FediverseApplication app; + + if (existingAppId == null) + { + app = new FediverseApplication + { + Id = existingAppId ?? _snowflakeGenerator.GenerateSnowflake(), + ClientId = mastodonApp.ClientId, + ClientSecret = mastodonApp.ClientSecret, + Domain = instance, + InstanceType = FediverseInstanceType.MastodonApi, + AccessToken = token, + TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60), + }; + + _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; + app.AccessToken = null; + app.TokenValidUntil = null; + } + + await _db.SaveChangesAsync(); + + return app; + } + + private async Task GetMastodonUserAsync(FediverseApplication app, string code) + { + var 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(); + var 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}"); + + var currentUserResp = await _client.SendAsync(req); + currentUserResp.EnsureSuccessStatusCode(); + var 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); + + // TODO: Mastodon's OAuth documentation doesn't specify a "state" parameter. that feels... wrong + // https://docs.joinmastodon.org/methods/oauth/ + private async Task GenerateMastodonAuthUrlAsync(FediverseApplication app) + { + try + { + await ValidateMastodonAppAsync(app); + } + catch (FoxnounsError.RemoteAuthError e) + { + _logger.Error( + e, + "Error validating app token for {AppId} on {Instance}", + app.Id, + app.Domain + ); + + app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id); + } + + 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))}"; + } + + private async Task ValidateMastodonAppAsync(FediverseApplication app) + { + // If we don't have an access token stored, or it's too old, get one + // When doing this we don't need to fetch the application info + if (app.AccessToken == null || app.TokenValidUntil < _clock.GetCurrentInstant()) + { + _logger.Debug( + "Application {AppId} on instance {Instance} has no valid token, fetching it", + app.Id, + app.Domain + ); + + app.AccessToken = await GetMastodonAppTokenAsync( + app.Domain, + app.ClientId, + app.ClientSecret + ); + app.TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60); + + _db.Update(app); + await _db.SaveChangesAsync(); + return; + } + + _logger.Debug( + "Checking whether application {AppId} on instance {Instance} is still valid", + app.Id, + app.Domain + ); + + var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentAppUri(app.Domain)); + req.Headers.Add("Authorization", $"Bearer {app.AccessToken}"); + + var resp = await _client.SendAsync(req); + if (!resp.IsSuccessStatusCode) + { + var error = await resp.Content.ReadAsStringAsync(); + throw new FoxnounsError.RemoteAuthError( + "Verifying app credentials returned an error", + error + ); + } + } + + private async Task GetMastodonAppTokenAsync( + string instance, + string clientId, + string clientSecret + ) + { + var resp = await _client.PostAsync( + MastodonTokenUri(instance), + new FormUrlEncodedContent( + new Dictionary + { + { "grant_type", "client_credentials" }, + { "client_id", clientId }, + { "client_secret", clientSecret }, + } + ) + ); + if (!resp.IsSuccessStatusCode) + { + var error = await resp.Content.ReadAsStringAsync(); + throw new FoxnounsError.RemoteAuthError( + "Requesting app token returned an error", + error + ); + } + + var token = (await resp.Content.ReadFromJsonAsync())?.AccessToken; + if (token == null) + { + throw new FoxnounsError($"Token response from instance {instance} was invalid"); + } + + return token; + } + + private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token"; + + private static string MastodonCurrentUserUri(string instance) => + $"https://{instance}/api/v1/accounts/verify_credentials"; + + private static string MastodonCurrentAppUri(string instance) => + $"https://{instance}/api/v1/apps/verify_credentials"; + + private record PartialMastodonApplication( + [property: J("name")] string Name, + [property: J("client_id")] string ClientId, + [property: J("client_secret")] string ClientSecret + ); +} diff --git a/Foxnouns.Backend/Services/FediverseAuthService.cs b/Foxnouns.Backend/Services/FediverseAuthService.cs new file mode 100644 index 0000000..ff39e88 --- /dev/null +++ b/Foxnouns.Backend/Services/FediverseAuthService.cs @@ -0,0 +1,162 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; + +namespace Foxnouns.Backend.Services; + +public partial class FediverseAuthService +{ + private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0"; + + private readonly ILogger _logger; + private readonly HttpClient _client; + private readonly DatabaseContext _db; + private readonly Config _config; + private readonly ISnowflakeGenerator _snowflakeGenerator; + private readonly IClock _clock; + + public FediverseAuthService( + ILogger logger, + Config config, + DatabaseContext db, + ISnowflakeGenerator snowflakeGenerator, + IClock clock + ) + { + _config = config; + _db = db; + _snowflakeGenerator = snowflakeGenerator; + _clock = clock; + _logger = logger.ForContext(); + _client = new HttpClient(); + _client.DefaultRequestHeaders.Remove("User-Agent"); + _client.DefaultRequestHeaders.Remove("Accept"); + _client.DefaultRequestHeaders.Add("User-Agent", $"pronouns.cc/{BuildInfo.Version}"); + _client.DefaultRequestHeaders.Add("Accept", "application/json"); + } + + public async Task GenerateAuthUrlAsync(string instance) + { + var app = await GetApplicationAsync(instance); + return await GenerateAuthUrlAsync(app); + } + + public async Task GetRemoteFediverseUserAsync(string instance, string code) + { + var app = await GetApplicationAsync(instance); + return await GetRemoteUserAsync(app, code); + } + + // 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 + ); + + private async Task GetApplicationAsync(string instance) + { + var 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); + + var softwareName = await GetSoftwareNameAsync(instance); + + if (IsMastodonCompatible(softwareName)) + { + return await CreateMastodonApplicationAsync(instance); + } + + throw new NotImplementedException(); + } + + private async Task GetSoftwareNameAsync(string instance) + { + _logger.Debug("Requesting software name for fediverse instance {Instance}", instance); + + var wellKnownResp = await _client.GetAsync( + new Uri($"https://{instance}/.well-known/nodeinfo") + ); + wellKnownResp.EnsureSuccessStatusCode(); + + var wellKnown = await wellKnownResp.Content.ReadFromJsonAsync(); + var 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" + ); + } + + var nodeInfoResp = await _client.GetAsync(nodeInfoUrl); + nodeInfoResp.EnsureSuccessStatusCode(); + + var 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) => + app.InstanceType switch + { + FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(app), + FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), + _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), + }; + + private async Task GetRemoteUserAsync(FediverseApplication app, string code) => + app.InstanceType switch + { + FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code), + FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), + _ => 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); +} From d982342ab8fa255678a6103db0402f91cc424484 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 30 Oct 2024 15:35:23 +0100 Subject: [PATCH 109/261] refactor: pass DbContextOptions into context directly turns out efcore doesn't like it when we create a new options instance (which includes a new data source *and* a new logger factory) every single time we create a context. this commit extracts OnConfiguring into static methods which are called when the context is added to the service collection and when it's manually created for migrations and the importer. --- Foxnouns.Backend/Database/DatabaseContext.cs | 76 ++++++++++--------- .../Database/DatabaseServiceExtensions.cs | 21 +++++ .../Extensions/WebApplicationExtensions.cs | 2 +- .../Services/FediverseAuthService.Mastodon.cs | 1 - migrators/NetImporter/NetImporter.cs | 6 +- 5 files changed, 69 insertions(+), 37 deletions(-) create mode 100644 Foxnouns.Backend/Database/DatabaseServiceExtensions.cs diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 95e0317..e6dc524 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -9,10 +9,42 @@ using Npgsql; namespace Foxnouns.Backend.Database; -public class DatabaseContext : DbContext +public class DatabaseContext(DbContextOptions options) : DbContext(options) { - private readonly NpgsqlDataSource _dataSource; - private readonly ILoggerFactory? _loggerFactory; + private static string GenerateConnectionString(Config.DatabaseConfig config) + { + return new NpgsqlConnectionStringBuilder(config.Url) + { + Pooling = config.EnablePooling ?? true, + Timeout = config.Timeout ?? 5, + MaxPoolSize = config.MaxPoolSize ?? 50, + MinPoolSize = 0, + ConnectionPruningInterval = 10, + ConnectionIdleLifetime = 10, + }.ConnectionString; + } + + public static NpgsqlDataSource BuildDataSource(Config config) + { + var dataSourceBuilder = new NpgsqlDataSourceBuilder( + GenerateConnectionString(config.Database) + ); + dataSourceBuilder.UseNodaTime(); + dataSourceBuilder.UseJsonNet(); + return 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; set; } public DbSet Members { get; set; } @@ -26,36 +58,6 @@ public class DatabaseContext : DbContext public DbSet UserFlags { get; set; } public DbSet MemberFlags { get; set; } - public DatabaseContext(Config config, ILoggerFactory? loggerFactory) - { - var connString = new NpgsqlConnectionStringBuilder(config.Database.Url) - { - Pooling = config.Database.EnablePooling ?? true, - Timeout = config.Database.Timeout ?? 5, - MaxPoolSize = config.Database.MaxPoolSize ?? 50, - MinPoolSize = 0, - ConnectionPruningInterval = 10, - ConnectionIdleLifetime = 10, - }.ConnectionString; - - var dataSourceBuilder = new NpgsqlDataSourceBuilder(connString); - dataSourceBuilder.UseNodaTime(); - dataSourceBuilder.UseJsonNet(); - _dataSource = dataSourceBuilder.Build(); - _loggerFactory = loggerFactory; - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => - optionsBuilder - .ConfigureWarnings(c => - c.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning) - .Ignore(CoreEventId.SaveChangesFailed) - ) - .UseNpgsql(_dataSource, o => o.UseNodaTime()) - .UseSnakeCaseNamingConvention() - .UseLoggerFactory(_loggerFactory) - .UseExceptionProcessor(); - protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { // Snowflakes are stored as longs @@ -125,6 +127,12 @@ public class DesignTimeDatabaseContextFactory : IDesignTimeDbContextFactory() ?? new(); - return new DatabaseContext(config, null); + var dataSource = DatabaseContext.BuildDataSource(config); + + var options = DatabaseContext + .BuildOptions(new DbContextOptionsBuilder(), dataSource, null) + .Options; + + return new DatabaseContext(options); } } diff --git a/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs b/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs new file mode 100644 index 0000000..4d966bf --- /dev/null +++ b/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs @@ -0,0 +1,21 @@ +using Serilog; + +namespace Foxnouns.Backend.Database; + +public static class DatabaseServiceExtensions +{ + public static IServiceCollection AddFoxnounsDatabase( + this IServiceCollection serviceCollection, + Config config + ) + { + var dataSource = DatabaseContext.BuildDataSource(config); + var loggerFactory = new LoggerFactory().AddSerilog(dispose: false); + + serviceCollection.AddDbContext(options => + DatabaseContext.BuildOptions(options, dataSource, loggerFactory) + ); + + return serviceCollection; + } +} diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index b2e519d..ce2f59b 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -85,7 +85,7 @@ public static class WebApplicationExtensions services .AddQueue() .AddSmtpMailer(ctx.Configuration) - .AddDbContext() + .AddFoxnounsDatabase(config) .AddMetricServer(o => o.Port = config.Logging.MetricsPort) .AddMinio(c => c.WithEndpoint(config.Storage.Endpoint) diff --git a/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs index c8d9dd5..e0e5e98 100644 --- a/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs @@ -2,7 +2,6 @@ using System.Net; using System.Web; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Minio.DataModel.ILM; using Duration = NodaTime.Duration; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; diff --git a/migrators/NetImporter/NetImporter.cs b/migrators/NetImporter/NetImporter.cs index 1f1bd9a..7a0ddf6 100644 --- a/migrators/NetImporter/NetImporter.cs +++ b/migrators/NetImporter/NetImporter.cs @@ -56,7 +56,11 @@ internal static class NetImporter var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); var config = new Config { Database = new Config.DatabaseConfig { Url = connString } }; - var db = new DatabaseContext(config, loggerFactory); + var dataSource = DatabaseContext.BuildDataSource(config); + var options = DatabaseContext + .BuildOptions(new DbContextOptionsBuilder(), dataSource, loggerFactory) + .Options; + var db = new DatabaseContext(options); if ((await db.Database.GetPendingMigrationsAsync()).Any()) { From 5a22807410491caffdf2479d8dd851eb633a3f2d Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 2 Nov 2024 21:23:49 +0100 Subject: [PATCH 110/261] fix: don't pass CancellationToken to method that shouldn't abort also add license header to project --- .../Authentication/DiscordAuthController.cs | 16 ++++++---------- Foxnouns.Backend/Services/RemoteAuthService.cs | 1 - Foxnouns.NET.sln.DotSettings | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 344d8ff..54ca24b 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -28,18 +28,15 @@ public class DiscordAuthController( // TODO: duplicating attribute doesn't work, find another way to mark both as possible response // leaving it here for documentation purposes [ProducesResponseType(StatusCodes.Status200OK)] - public async Task CallbackAsync( - [FromBody] AuthController.CallbackRequest req, - CancellationToken ct = default - ) + public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req) { CheckRequirements(); - await keyCacheService.ValidateAuthStateAsync(req.State, ct); + await keyCacheService.ValidateAuthStateAsync(req.State); - var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code, req.State, ct); - var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id, ct: ct); + var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); + var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); if (user != null) - return Ok(await GenerateUserTokenAsync(user, ct)); + return Ok(await GenerateUserTokenAsync(user)); _logger.Debug( "Discord user {Username} ({Id}) authenticated with no local account", @@ -51,8 +48,7 @@ public class DiscordAuthController( await keyCacheService.SetKeyAsync( $"discord:{ticket}", remoteUser, - Duration.FromMinutes(20), - ct + Duration.FromMinutes(20) ); return Ok( diff --git a/Foxnouns.Backend/Services/RemoteAuthService.cs b/Foxnouns.Backend/Services/RemoteAuthService.cs index 505029d..91a2dc5 100644 --- a/Foxnouns.Backend/Services/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/RemoteAuthService.cs @@ -13,7 +13,6 @@ public class RemoteAuthService(Config config, ILogger logger) public async Task RequestDiscordTokenAsync( string code, - string state, CancellationToken ct = default ) { diff --git a/Foxnouns.NET.sln.DotSettings b/Foxnouns.NET.sln.DotSettings index 69e6273..e9d37e2 100644 --- a/Foxnouns.NET.sln.DotSettings +++ b/Foxnouns.NET.sln.DotSettings @@ -1,3 +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 <https://www.gnu.org/licenses/>. + True True True \ No newline at end of file From c4cb08cdc18f84f78859445e6e2c54c5d3cc4f3c Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 3 Nov 2024 02:07:07 +0100 Subject: [PATCH 111/261] feat: initial fediverse registration/login --- .../Authentication/AuthController.cs | 21 ++- .../Authentication/DiscordAuthController.cs | 50 +----- .../Authentication/EmailAuthController.cs | 3 +- .../Authentication/FediverseAuthController.cs | 95 +++++++++- .../Extensions/WebApplicationExtensions.cs | 2 + .../Services/{ => Auth}/AuthService.cs | 51 +++++- .../FediverseAuthService.Mastodon.cs | 4 +- .../{ => Auth}/FediverseAuthService.cs | 15 +- .../Services/UserRendererService.cs | 2 +- .../app/components/RegisterError.tsx | 36 ++++ Foxnouns.Frontend/app/lib/request.server.ts | 2 +- .../routes/auth.callback.discord/route.tsx | 32 +--- .../route.tsx | 163 ++++++++++++++++++ .../app/routes/auth.log-in/route.tsx | 3 + .../routes/auth.log-in_.fediverse/route.tsx | 75 ++++++++ Foxnouns.Frontend/public/locales/en.json | 24 ++- 16 files changed, 467 insertions(+), 111 deletions(-) rename Foxnouns.Backend/Services/{ => Auth}/AuthService.cs (85%) rename Foxnouns.Backend/Services/{ => Auth}/FediverseAuthService.Mastodon.cs (98%) rename Foxnouns.Backend/Services/{ => Auth}/FediverseAuthService.cs (93%) create mode 100644 Foxnouns.Frontend/app/components/RegisterError.tsx create mode 100644 Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx create mode 100644 Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 1a737eb..30bcbe9 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -50,17 +50,6 @@ public class AuthController( Instant ExpiresAt ); - public record CallbackResponse( - bool HasAccount, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - string? RemoteUsername, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - UserRendererService.UserResponse? User, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt - ); - public record OauthRegisterRequest(string Ticket, string Username); public record CallbackRequest(string Code, string State); @@ -77,3 +66,13 @@ public class AuthController( return NoContent(); } } + +public record CallbackResponse( + bool HasAccount, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Ticket, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? RemoteUsername, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + UserRendererService.UserResponse? User, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Token, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? ExpiresAt +); diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 54ca24b..aad683f 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -2,6 +2,7 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Services.Auth; using Foxnouns.Backend.Utils; using JetBrains.Annotations; using Microsoft.AspNetCore.Mvc; @@ -14,20 +15,16 @@ namespace Foxnouns.Backend.Controllers.Authentication; public class DiscordAuthController( [UsedImplicitly] Config config, ILogger logger, - IClock clock, DatabaseContext db, KeyCacheService keyCacheService, AuthService authService, - RemoteAuthService remoteAuthService, - UserRendererService userRenderer + RemoteAuthService remoteAuthService ) : ApiControllerBase { private readonly ILogger _logger = logger.ForContext(); [HttpPost("callback")] - // TODO: duplicating attribute doesn't work, find another way to mark both as possible response - // leaving it here for documentation purposes - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] public async Task CallbackAsync([FromBody] AuthController.CallbackRequest req) { CheckRequirements(); @@ -36,7 +33,7 @@ public class DiscordAuthController( var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); var user = await authService.AuthenticateUserAsync(AuthType.Discord, remoteUser.Id); if (user != null) - return Ok(await GenerateUserTokenAsync(user)); + return Ok(await authService.GenerateUserTokenAsync(user)); _logger.Debug( "Discord user {Username} ({Id}) authenticated with no local account", @@ -52,7 +49,7 @@ public class DiscordAuthController( ); return Ok( - new AuthController.CallbackResponse( + new CallbackResponse( HasAccount: false, Ticket: ticket, RemoteUsername: remoteUser.Username, @@ -94,42 +91,7 @@ public class DiscordAuthController( remoteUser.Username ); - return Ok(await GenerateUserTokenAsync(user)); - } - - private async Task GenerateUserTokenAsync( - User user, - CancellationToken ct = default - ) - { - var frontendApp = await db.GetFrontendApplicationAsync(ct); - _logger.Debug("Logging user {Id} in with Discord", user.Id); - - var (tokenStr, token) = authService.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 AuthController.CallbackResponse( - HasAccount: true, - Ticket: null, - RemoteUsername: null, - User: await userRenderer.RenderUserAsync( - user, - selfUser: user, - renderMembers: false, - ct: ct - ), - Token: tokenStr, - ExpiresAt: token.ExpiresAt - ); + return Ok(await authService.GenerateUserTokenAsync(user)); } private void CheckRequirements() diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 7e3706e..6aadf65 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -3,6 +3,7 @@ using Foxnouns.Backend.Database.Models; 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; @@ -84,7 +85,7 @@ public class EmailAuthController( await keyCacheService.SetKeyAsync($"email:{ticket}", state.Email, Duration.FromMinutes(20)); return Ok( - new AuthController.CallbackResponse( + new CallbackResponse( HasAccount: false, Ticket: ticket, RemoteUsername: state.Email, diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index fdd10b7..43a2955 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -1,11 +1,26 @@ +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Services.Auth; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using NodaTime; +using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth/fediverse")] -public class FediverseAuthController(FediverseAuthService fediverseAuthService) : ApiControllerBase +public class FediverseAuthController( + ILogger logger, + DatabaseContext db, + FediverseAuthService fediverseAuthService, + AuthService authService, + KeyCacheService keyCacheService +) : ApiControllerBase { + private readonly ILogger _logger = logger.ForContext(); + [HttpGet] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetFediverseUrlAsync([FromQuery] string instance) @@ -14,12 +29,88 @@ public class FediverseAuthController(FediverseAuthService fediverseAuthService) return Ok(new FediverseUrlResponse(url)); } + [HttpPost("callback")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task FediverseCallbackAsync([FromBody] CallbackRequest req) { - throw new NotImplementedException(); + var app = await fediverseAuthService.GetApplicationAsync(req.Instance); + var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); + + var user = await authService.AuthenticateUserAsync( + AuthType.Fediverse, + remoteUser.Id, + instance: app + ); + if (user != null) + return Ok(await authService.GenerateUserTokenAsync(user)); + + var ticket = AuthUtils.RandomToken(); + await keyCacheService.SetKeyAsync( + $"fediverse:{ticket}", + new FediverseTicketData(app.Id, remoteUser), + Duration.FromMinutes(20) + ); + + return Ok( + new CallbackResponse( + HasAccount: false, + Ticket: ticket, + RemoteUsername: $"@{remoteUser.Username}@{app.Domain}", + User: null, + Token: null, + ExpiresAt: null + ) + ); + } + + [HttpPost("register")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + public async Task RegisterAsync( + [FromBody] AuthController.OauthRegisterRequest req + ) + { + var ticketData = await keyCacheService.GetKeyAsync( + $"fediverse:{req.Ticket}" + ); + if (ticketData == null) + throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); + + var app = await db.FediverseApplications.FindAsync(ticketData.ApplicationId); + 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); + } + + var user = await authService.CreateUserWithRemoteAuthAsync( + req.Username, + AuthType.Fediverse, + ticketData.User.Id, + ticketData.User.Username, + instance: app + ); + + return Ok(await authService.GenerateUserTokenAsync(user)); } public record CallbackRequest(string Instance, string Code); private record FediverseUrlResponse(string Url); + + private record FediverseTicketData( + Snowflake ApplicationId, + FediverseAuthService.FediverseUser User + ); } diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index ce2f59b..c505f4d 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -4,12 +4,14 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Jobs; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Services.Auth; using Microsoft.EntityFrameworkCore; using Minio; using NodaTime; using Prometheus; using Serilog; using Serilog.Events; +using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; using IClock = NodaTime.IClock; namespace Foxnouns.Backend.Extensions; diff --git a/Foxnouns.Backend/Services/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs similarity index 85% rename from Foxnouns.Backend/Services/AuthService.cs rename to Foxnouns.Backend/Services/Auth/AuthService.cs index d03496c..9675f22 100644 --- a/Foxnouns.Backend/Services/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography; +using Foxnouns.Backend.Controllers.Authentication; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; @@ -6,10 +7,17 @@ using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using NodaTime; -namespace Foxnouns.Backend.Services; +namespace Foxnouns.Backend.Services.Auth; -public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator) +public class AuthService( + IClock clock, + ILogger logger, + DatabaseContext db, + ISnowflakeGenerator snowflakeGenerator, + UserRendererService userRenderer +) { + private readonly ILogger _logger = logger.ForContext(); private readonly PasswordHasher _passwordHasher = new(); /// @@ -256,6 +264,45 @@ public class AuthService(IClock clock, DatabaseContext db, ISnowflakeGenerator s ); } + /// + /// 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 + ) + { + var frontendApp = await db.GetFrontendApplicationAsync(ct); + + var (tokenStr, 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( + HasAccount: true, + Ticket: null, + RemoteUsername: null, + User: await userRenderer.RenderUserAsync( + user, + selfUser: user, + renderMembers: false, + ct: ct + ), + Token: tokenStr, + ExpiresAt: token.ExpiresAt + ); + } + private static (string, byte[]) GenerateToken() { var token = AuthUtils.RandomToken(); diff --git a/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs similarity index 98% rename from Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs rename to Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs index e0e5e98..139830b 100644 --- a/Foxnouns.Backend/Services/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs @@ -5,12 +5,12 @@ using Foxnouns.Backend.Database.Models; using Duration = NodaTime.Duration; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; -namespace Foxnouns.Backend.Services; +namespace Foxnouns.Backend.Services.Auth; public partial class FediverseAuthService { private string MastodonRedirectUri(string instance) => - $"{_config.BaseUrl}/auth/login/mastodon/{instance}"; + $"{_config.BaseUrl}/auth/callback/mastodon/{instance}"; private async Task CreateMastodonApplicationAsync( string instance, diff --git a/Foxnouns.Backend/Services/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs similarity index 93% rename from Foxnouns.Backend/Services/FediverseAuthService.cs rename to Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index ff39e88..fc54017 100644 --- a/Foxnouns.Backend/Services/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -4,7 +4,7 @@ using Microsoft.EntityFrameworkCore; using NodaTime; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; -namespace Foxnouns.Backend.Services; +namespace Foxnouns.Backend.Services.Auth; public partial class FediverseAuthService { @@ -43,12 +43,6 @@ public partial class FediverseAuthService return await GenerateAuthUrlAsync(app); } - public async Task GetRemoteFediverseUserAsync(string instance, string code) - { - var app = await GetApplicationAsync(instance); - return await GetRemoteUserAsync(app, code); - } - // 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( @@ -56,7 +50,7 @@ public partial class FediverseAuthService [property: J("username")] string Username ); - private async Task GetApplicationAsync(string instance) + public async Task GetApplicationAsync(string instance) { var app = await _db.FediverseApplications.FirstOrDefaultAsync(a => a.Domain == instance); if (app != null) @@ -110,7 +104,10 @@ public partial class FediverseAuthService _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; - private async Task GetRemoteUserAsync(FediverseApplication app, string code) => + public async Task GetRemoteFediverseUserAsync( + FediverseApplication app, + string code + ) => app.InstanceType switch { FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code), diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index b47832d..b560ba6 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -97,7 +97,7 @@ public class UserRendererService( ? $"{config.MediaBaseUrl}/users/{user.Id}/avatars/{user.Avatar}.webp" : null; - public string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; + private string ImageUrlFor(PrideFlag flag) => $"{config.MediaBaseUrl}/flags/{flag.Hash}.webp"; public record UserResponse( Snowflake Id, diff --git a/Foxnouns.Frontend/app/components/RegisterError.tsx b/Foxnouns.Frontend/app/components/RegisterError.tsx new file mode 100644 index 0000000..1ecbbdb --- /dev/null +++ b/Foxnouns.Frontend/app/components/RegisterError.tsx @@ -0,0 +1,36 @@ +import { ApiError, firstErrorFor } from "~/lib/api/error"; +import { Trans, useTranslation } from "react-i18next"; +import { Alert } from "react-bootstrap"; +import { Link } from "@remix-run/react"; +import ErrorAlert from "~/components/ErrorAlert"; + +export default function RegisterError({ error }: { error: ApiError }) { + const { t } = useTranslation(); + + // TODO: maybe turn these messages into their own error codes? + const ticketMessage = firstErrorFor(error, "ticket")?.message; + const usernameMessage = firstErrorFor(error, "username")?.message; + + if (ticketMessage === "Invalid ticket") { + return ( + + {t("error.heading")} + + Invalid ticket (it might have been too long since you logged in), please{" "} + try again. + + + ); + } + + if (usernameMessage === "Username is already taken") { + return ( + + {t("log-in.callback.invalid-username")} + {t("log-in.callback.username-taken")} + + ); + } + + return ; +} diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts index 8dab154..49f2d20 100644 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ b/Foxnouns.Frontend/app/lib/request.server.ts @@ -30,7 +30,7 @@ export async function baseRequest( headers: { ...params.headers, ...(params.token ? { Authorization: params.token } : {}), - "Content-Type": "application/json", + ...(params.body ? { "Content-Type": "application/json" } : {}), }, }); diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index 5fb246c..3a1fd1a 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -22,6 +22,7 @@ import ErrorAlert from "~/components/ErrorAlert"; import i18n from "~/i18next.server"; import { tokenCookieName } from "~/lib/utils"; import { useEffect } from "react"; +import RegisterError from "~/components/RegisterError"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; @@ -163,34 +164,3 @@ export default function DiscordCallbackPage() { ); } - -function RegisterError({ error }: { error: ApiError }) { - const { t } = useTranslation(); - - // TODO: maybe turn these messages into their own error codes? - const ticketMessage = firstErrorFor(error, "ticket")?.message; - const usernameMessage = firstErrorFor(error, "username")?.message; - - if (ticketMessage === "Invalid ticket") { - return ( - - {t("error.heading")} - - Invalid ticket (it might have been too long since you logged in with Discord), please{" "} - try again. - - - ); - } - - if (usernameMessage === "Username is already taken") { - return ( - - {t("log-in.callback.invalid-username")} - {t("log-in.callback.username-taken")} - - ); - } - - return ; -} diff --git a/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx new file mode 100644 index 0000000..8444bb5 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx @@ -0,0 +1,163 @@ +import { + ActionFunctionArgs, + json, + LoaderFunctionArgs, + MetaFunction, + redirect, +} from "@remix-run/node"; +import i18n from "~/i18next.server"; +import { type ApiError, ErrorCode } from "~/lib/api/error"; +import serverRequest, { writeCookie } from "~/lib/request.server"; +import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; +import { tokenCookieName } from "~/lib/utils"; +import { + Link, + ShouldRevalidateFunction, + useActionData, + useLoaderData, + useNavigate, +} from "@remix-run/react"; +import { Trans, useTranslation } from "react-i18next"; +import { useEffect } from "react"; +import { Form as RemixForm } from "@remix-run/react/dist/components"; +import { Button, Form } from "react-bootstrap"; +import RegisterError from "~/components/RegisterError"; + +export const meta: MetaFunction = ({ data }) => { + return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; +}; + +export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { + return !actionResult; +}; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); + const url = new URL(request.url); + + const code = url.searchParams.get("code"); + if (!code) throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code" } as ApiError; + + const resp = await serverRequest("POST", "/auth/fediverse/callback", { + body: { code, instance: params.instance! }, + isInternal: true, + }); + + if (resp.has_account) { + return json( + { + meta: { title: t("log-in.callback.title.fediverse-success") }, + hasAccount: true, + user: resp.user!, + ticket: null, + remoteUser: null, + }, + { + headers: { + "Set-Cookie": writeCookie(tokenCookieName, resp.token!), + }, + }, + ); + } + + return json({ + meta: { title: t("log-in.callback.title.fediverse-register") }, + hasAccount: false, + user: null, + instance: params.instance!, + ticket: resp.ticket!, + remoteUser: resp.remote_username!, + }); +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + 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 json({ + error: { + status: 403, + code: ErrorCode.BadRequest, + message: "Invalid username or ticket", + } as ApiError, + user: null, + }); + + try { + const resp = await serverRequest("POST", "/auth/fediverse/register", { + body: { username, ticket }, + isInternal: true, + }); + + return redirect("/auth/welcome", { + headers: { + "Set-Cookie": writeCookie(tokenCookieName, resp.token), + }, + status: 303, + }); + } catch (e) { + JSON.stringify(e); + + return json({ error: e as ApiError }); + } +}; + +export default function FediverseCallbackPage() { + const { t } = useTranslation(); + const data = useLoaderData(); + const actionData = useActionData(); + const navigate = useNavigate(); + + useEffect(() => { + setTimeout(() => { + if (data.hasAccount) { + navigate(`/@${data.user!.username}`); + } + }, 2000); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (data.hasAccount) { + const username = data.user!.username; + + return ( + <> +

    {t("log-in.callback.success")}

    +

    + + {/* @ts-expect-error react-i18next handles interpolation here */} + Welcome back, @{{ username }}! + +
    + {t("log-in.callback.redirect-hint")} +

    + + ); + } + + return ( + +
    + {actionData?.error && } + + {t("log-in.callback.remote-username.fediverse")} + + + + {t("log-in.callback.username")} + + + + + +
    + ); +} diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx index aaffd80..dc3af98 100644 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx @@ -126,6 +126,9 @@ export default function LoginPage() { {t("log-in.3rd-party.tumblr")} )} + + {t("log-in.3rd-party.fediverse")} + {!urls.email_enabled &&
    } diff --git a/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx new file mode 100644 index 0000000..a7304f5 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx @@ -0,0 +1,75 @@ +import { + LoaderFunctionArgs, + json, + MetaFunction, + ActionFunctionArgs, + redirect, +} from "@remix-run/node"; +import i18n from "~/i18next.server"; +import { useTranslation } from "react-i18next"; +import { Form as RemixForm, useActionData } from "@remix-run/react"; +import { Button, Form } from "react-bootstrap"; +import serverRequest from "~/lib/request.server"; +import { ApiError, ErrorCode } from "~/lib/api/error"; +import ErrorAlert from "~/components/ErrorAlert"; + +export const meta: MetaFunction = ({ data }) => { + return [{ title: `${data?.meta.title || "Log in with a Fediverse account"} • pronouns.cc` }]; +}; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); + + return json({ meta: { title: t("log-in.fediverse.choose-title") } }); +}; + +export const action = async ({ request }: ActionFunctionArgs) => { + const body = await request.formData(); + const instance = body.get("instance") as string | null; + if (!instance) + return json({ + error: { + status: 403, + code: ErrorCode.BadRequest, + message: "Invalid instance name", + } as ApiError, + }); + + try { + const resp = await serverRequest<{ url: string }>( + "GET", + `/auth/fediverse?instance=${encodeURIComponent(instance)}`, + { + isInternal: true, + }, + ); + + return redirect(resp.url); + } catch (e) { + return json({ error: e as ApiError }); + } +}; + +export default function AuthFediversePage() { + const { t } = useTranslation(); + + const data = useActionData(); + + return ( + <> +

    {t("log-in.fediverse.choose-form-title")}

    + {data?.error && } + +
    + + {t("log-in.fediverse-instance-label")} + + + + +
    + + ); +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index c834e95..78e2bf3 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -47,22 +47,31 @@ }, "log-in": { "callback": { + "invalid-ticket": "Invalid ticket (it might have been too long since you logged in), please <2>try again.", + "invalid-username": "Invalid username", + "username-taken": "That username is already taken, please try something else.", "title": { "discord-success": "Log in with Discord", - "discord-register": "Register with Discord" + "discord-register": "Register with Discord", + "fediverse-success": "Log in with a Fediverse account", + "fediverse-register": "Register with a Fediverse account" }, "success": "Successfully logged in!", "success-link": "Welcome back, <1>@{{username}}!", "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", "remote-username": { - "discord": "Your discord username" + "discord": "Your Discord username", + "fediverse": "Your Fediverse account" }, "username": "Username", - "sign-up-button": "Sign up", - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in with Discord), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else." + "sign-up-button": "Sign up" }, + "fediverse": { + "choose-title": "Log in with a Fediverse account", + "choose-form-title": "Choose a Fediverse instance" + }, + "fediverse-instance-label": "Your Fediverse instance", + "fediverse-log-in-button": "Log in", "title": "Log in", "form-title": "Log in with email", "email": "Email address", @@ -74,7 +83,8 @@ "desc": "If you prefer, you can also log in with one of these services:", "discord": "Log in with Discord", "google": "Log in with Google", - "tumblr": "Log in with Tumblr" + "tumblr": "Log in with Tumblr", + "fediverse": "Log in with the Fediverse" }, "invalid-credentials": "Invalid email address or password, please check your spelling and try again." }, From 201c56c3dda5f1070912de895f1cd053c19187c2 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 3 Nov 2024 13:53:16 +0100 Subject: [PATCH 112/261] feat: link discord account to existing account --- .../Authentication/AuthController.cs | 10 ++ .../Authentication/DiscordAuthController.cs | 85 +++++++++++++++++ Foxnouns.Backend/ExpectedError.cs | 1 + .../Extensions/KeyCacheExtensions.cs | 31 +++++++ .../Services/UserRendererService.cs | 9 +- .../app/components/ErrorAlert.tsx | 2 + Foxnouns.Frontend/app/lib/api/error.ts | 1 + Foxnouns.Frontend/app/lib/api/user.ts | 1 - .../routes/auth.callback.discord/route.tsx | 73 ++++++++++++++- .../app/routes/settings.auth/route.tsx | 91 ++++++++++++++++++- .../route.tsx | 26 ++++++ Foxnouns.Frontend/public/locales/en.json | 17 +++- 12 files changed, 333 insertions(+), 14 deletions(-) create mode 100644 Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 30bcbe9..a634eb2 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -1,5 +1,6 @@ using System.Web; using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; @@ -50,6 +51,15 @@ public class AuthController( Instant ExpiresAt ); + public record SingleUrlResponse(string Url); + + public record AddOauthAccountResponse( + Snowflake Id, + AuthType Type, + string RemoteId, + string? RemoteUsername + ); + public record OauthRegisterRequest(string Ticket, string Username); public record CallbackRequest(string Code, string State); diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index aad683f..ee22804 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -1,6 +1,10 @@ +using System.Net; +using System.Web; +using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; +using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; using Foxnouns.Backend.Services.Auth; using Foxnouns.Backend.Utils; @@ -94,6 +98,87 @@ public class DiscordAuthController( return Ok(await authService.GenerateUserTokenAsync(user)); } + [HttpGet("add-account")] + [Authorize("*")] + public async Task AddDiscordAccountAsync() + { + CheckRequirements(); + + var existingAccounts = await db + .AuthMethods.Where(m => m.UserId == CurrentUser!.Id && m.AuthType == AuthType.Discord) + .CountAsync(); + if (existingAccounts > AuthUtils.MaxAuthMethodsPerType) + { + throw new ApiError.BadRequest( + "Too many linked Discord accounts, maximum of 3 per account." + ); + } + + var state = HttpUtility.UrlEncode( + await keyCacheService.GenerateAddExtraAccountStateAsync( + AuthType.Discord, + CurrentUser!.Id + ) + ); + + var 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 AuthController.SingleUrlResponse(url)); + } + + [HttpPost("add-account/callback")] + [Authorize("*")] + public async Task AddAccountCallbackAsync( + [FromBody] AuthController.CallbackRequest req + ) + { + CheckRequirements(); + + var accountState = await keyCacheService.GetAddExtraAccountStateAsync(req.State); + if ( + accountState is not { AuthType: AuthType.Discord } + || accountState.UserId != CurrentUser!.Id + ) + throw new ApiError.BadRequest("Invalid state", "state", req.State); + + var remoteUser = await remoteAuthService.RequestDiscordTokenAsync(req.Code); + try + { + var 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 AuthController.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) diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 1b92a7e..e185d4e 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -147,6 +147,7 @@ public enum ErrorCode GenericApiError, UserNotFound, MemberNotFound, + AccountAlreadyLinked, } public class ValidationError diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index 8178dd6..522c8d6 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -1,4 +1,5 @@ using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; using Newtonsoft.Json; @@ -57,9 +58,39 @@ public static class KeyCacheExtensions delete: true, ct ); + + public static async Task GenerateAddExtraAccountStateAsync( + this KeyCacheService keyCacheService, + AuthType authType, + Snowflake userId, + CancellationToken ct = default + ) + { + var state = AuthUtils.RandomToken(); + await keyCacheService.SetKeyAsync( + $"add_account:{state}", + new AddExtraAccountState(authType, userId), + Duration.FromDays(1), + ct + ); + return state; + } + + public static async Task GetAddExtraAccountStateAsync( + this KeyCacheService keyCacheService, + string state, + CancellationToken ct = default + ) => + await keyCacheService.GetKeyAsync( + $"add_account:{state}", + delete: true, + ct + ); } public record RegisterEmailState( string Email, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Snowflake? ExistingUserId ); + +public record AddExtraAccountState(AuthType AuthType, Snowflake UserId); diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index b560ba6..145de1a 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -72,8 +72,9 @@ public class UserRendererService( a.Id, a.AuthType, a.RemoteId, - a.RemoteUsername, - a.FediverseApplication?.Domain + a.FediverseApplication != null + ? $"@{a.RemoteUsername}@{a.FediverseApplication.Domain}" + : a.RemoteUsername )) : null, tokenHidden ? user.ListHidden : null, @@ -130,9 +131,7 @@ public class UserRendererService( [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, string RemoteId, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - string? RemoteUsername, - [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - string? FediverseInstance + string? RemoteUsername ); public record PartialUser( diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx index d66b6a1..be470a4 100644 --- a/Foxnouns.Frontend/app/components/ErrorAlert.tsx +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -140,6 +140,8 @@ export const errorCodeDesc = (t: TFunction, code: ErrorCode) => { return t("error.errors.member-not-found"); case ErrorCode.UserNotFound: return t("error.errors.user-not-found"); + case ErrorCode.AccountAlreadyLinked: + return t("error.errors.account-already-linked"); } return t("error.errors.generic-error"); diff --git a/Foxnouns.Frontend/app/lib/api/error.ts b/Foxnouns.Frontend/app/lib/api/error.ts index 0a3d9b9..3f33566 100644 --- a/Foxnouns.Frontend/app/lib/api/error.ts +++ b/Foxnouns.Frontend/app/lib/api/error.ts @@ -16,6 +16,7 @@ export enum ErrorCode { GenericApiError = "GENERIC_API_ERROR", UserNotFound = "USER_NOT_FOUND", MemberNotFound = "MEMBER_NOT_FOUND", + AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED", } export type ValidationError = { diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/app/lib/api/user.ts index 4f07301..4b39502 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/app/lib/api/user.ts @@ -71,7 +71,6 @@ export type AuthMethod = { type: "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; remote_id: string; remote_username?: string; - fediverse_instance?: string; }; export type CustomPreference = { diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx index 3a1fd1a..c34ac58 100644 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx @@ -5,8 +5,8 @@ import { LoaderFunctionArgs, MetaFunction, } from "@remix-run/node"; -import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; -import serverRequest, { writeCookie } from "~/lib/request.server"; +import { type ApiError, ErrorCode } from "~/lib/api/error"; +import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; import { Form as RemixForm, @@ -17,12 +17,13 @@ import { useNavigate, } from "@remix-run/react"; import { Trans, useTranslation } from "react-i18next"; -import { Form, Button, Alert } from "react-bootstrap"; -import ErrorAlert from "~/components/ErrorAlert"; +import { Form, Button } from "react-bootstrap"; import i18n from "~/i18next.server"; import { tokenCookieName } from "~/lib/utils"; import { useEffect } from "react"; import RegisterError from "~/components/RegisterError"; +import { AuthMethod } from "~/lib/api/user"; +import { errorCodeDesc } from "~/components/ErrorAlert"; export const meta: MetaFunction = ({ data }) => { return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; @@ -39,9 +40,43 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const code = url.searchParams.get("code"); const state = url.searchParams.get("state"); + const token = getToken(request); + if (!code || !state) throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code or state" } as ApiError; + if (token) { + try { + const resp = await serverRequest("POST", "/auth/discord/add-account/callback", { + body: { code, state }, + token, + isInternal: true, + }); + + return json({ + isLinkRequest: true, + meta: { title: t("log-in.callback.title.discord-link") }, + error: null, + hasAccount: false, + user: null, + ticket: null, + remoteUser: null, + newAuthMethod: resp, + }); + } catch (e) { + return json({ + isLinkRequest: true, + meta: { title: t("log-in.callback.title.discord-link") }, + error: e as ApiError, + hasAccount: false, + user: null, + ticket: null, + remoteUser: null, + newAuthMethod: null, + }); + } + } + const resp = await serverRequest("POST", "/auth/discord/callback", { body: { code, state }, isInternal: true, @@ -50,11 +85,14 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { if (resp.has_account) { return json( { + isLinkRequest: false, meta: { title: t("log-in.callback.title.discord-success") }, + error: null, hasAccount: true, user: resp.user!, ticket: null, remoteUser: null, + newAuthMethod: null, }, { headers: { @@ -65,11 +103,14 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { } return json({ + isLinkRequest: false, meta: { title: t("log-in.callback.title.discord-register") }, + error: null, hasAccount: false, user: null, ticket: resp.ticket!, remoteUser: resp.remote_username!, + newAuthMethod: null, }); }; @@ -122,6 +163,30 @@ export default function DiscordCallbackPage() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + if (data.isLinkRequest) { + if (data.error) { + return ( + <> +

    {t("log-in.callback.link-error")}

    +

    {errorCodeDesc(t, data.error.code)}

    + + ); + } + + const authMethod = data.newAuthMethod!; + + return ( + <> +

    {t("log-in.callback.discord-link-success")}

    +

    + {t("log-in.callback.discord-link-success-hint", { + username: authMethod.remote_username ?? authMethod.remote_id, + })} +

    + + ); + } + if (data.hasAccount) { const username = data.user!.username; diff --git a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx index 125f413..4954027 100644 --- a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx +++ b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx @@ -23,7 +23,13 @@ export default function AuthSettings() { const { urls } = useLoaderData(); const { user } = useRouteLoaderData("routes/settings")!; - return
    {urls.email_enabled && }
    ; + return ( +
    + {urls.email_enabled && } + {urls.discord && } + +
    + ); } function EmailSettings({ user }: { user: MeUser }) { @@ -75,3 +81,86 @@ function EmailRow({ email, disabled }: { email: AuthMethod; disabled: boolean }) ); } + +function DiscordSettings({ user }: { user: MeUser }) { + const { t } = useTranslation(); + const oneAuthMethod = user.auth_methods.length === 1; + const discordAccounts = user.auth_methods.filter((m) => m.type === "DISCORD"); + + return ( + <> +

    {t("settings.auth.discord-accounts")}

    + {discordAccounts.length > 0 && ( + <> + + {discordAccounts.map((a) => ( + + ))} + + + )} + {discordAccounts.length < 3 && ( +

    + {/* @ts-expect-error as=Link */} + +

    + )} + + ); +} + +function FediverseSettings({ user }: { user: MeUser }) { + const { t } = useTranslation(); + const oneAuthMethod = user.auth_methods.length === 1; + const fediAccounts = user.auth_methods.filter((m) => m.type === "FEDIVERSE"); + + return ( + <> +

    {t("settings.auth.fediverse-accounts")}

    + {fediAccounts.length > 0 && ( + <> + + {fediAccounts.map((a) => ( + + ))} + + + )} + {fediAccounts.length < 3 && ( +

    + {/* @ts-expect-error as=Link */} + +

    + )} + + ); +} + +function NonEmailRow({ account, disabled }: { account: AuthMethod; disabled: boolean }) { + const { t } = useTranslation(); + + return ( + +
    +
    + {account.remote_username} {account.type !== "FEDIVERSE" && <>({account.remote_id})} +
    + {!disabled && ( +
    + + {t("settings.auth.remove-auth-method")} + +
    + )} +
    +
    + ); +} diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx new file mode 100644 index 0000000..045fe85 --- /dev/null +++ b/Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx @@ -0,0 +1,26 @@ +import { LoaderFunctionArgs, redirect, json } from "@remix-run/node"; +import serverRequest, { getToken } from "~/lib/request.server"; +import { ApiError } from "~/lib/api/error"; +import { useLoaderData } from "@remix-run/react"; +import ErrorAlert from "~/components/ErrorAlert"; + +export const loader = async ({ request }: LoaderFunctionArgs) => { + const token = getToken(request); + + try { + const { url } = await serverRequest<{ url: string }>("GET", "/auth/discord/add-account", { + isInternal: true, + token, + }); + + return redirect(url, 303); + } catch (e) { + return json({ error: e as ApiError }); + } +}; + +export default function AddDiscordAccountPage() { + const { error } = useLoaderData(); + + return ; +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index 78e2bf3..c7c1779 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -16,7 +16,8 @@ "generic-error": "An unknown error occurred.", "internal-server-error": "Server experienced an internal error, please try again later.", "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User not found, please check your spelling and try again." + "user-not-found": "User not found, please check your spelling and try again.", + "account-already-linked": "This account is already linked with a pronouns.cc account." }, "title": "An error occurred", "more-info": "Click here for a more detailed error" @@ -51,11 +52,15 @@ "invalid-username": "Invalid username", "username-taken": "That username is already taken, please try something else.", "title": { + "discord-link": "Link a new Discord account", "discord-success": "Log in with Discord", "discord-register": "Register with Discord", "fediverse-success": "Log in with a Fediverse account", "fediverse-register": "Register with a Fediverse account" }, + "link-error": "Could not link account", + "discord-link-success": "Linked a new Discord account!", + "discord-link-success-hint": "Successfully linked the Discord account {{username}} with your pronouns.cc account. You can now close this page.", "success": "Successfully logged in!", "success-link": "Welcome back, <1>@{{username}}!", "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", @@ -126,14 +131,20 @@ "email-address": "Email address", "password-1": "Password", "password-2": "Confirm password", - "add-email-button": "Add email address" + "add-email-button": "Add email address", + "add-first-discord-account": "Link a Discord account", + "add-extra-discord-account": "Link another Discord account", + "add-first-fediverse-account": "Link a Fediverse account", + "add-extra-fediverse-account": "Link another Fediverse account" }, "no-email": "You haven't linked any email addresses yet. You can add one using this form.", "new-email-pending": "Email address added! Click the link in your inbox to confirm.", "email-link-success": "Email successfully linked", "redirect-to-auth-hint": "You will be redirected back to the authentication page in a few seconds.", "email-addresses": "Email addresses", - "remove-auth-method": "Remove" + "remove-auth-method": "Remove", + "discord-accounts": "Linked Discord accounts", + "fediverse-accounts": "Linked Fediverse accounts" }, "title": "Settings", "nav": { From 9160281ea2255f5fc337916cb487dab084a2c349 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 4 Nov 2024 22:04:04 +0100 Subject: [PATCH 113/261] feat: remove auth method --- .../Authentication/AuthController.cs | 51 ++++++++++++- Foxnouns.Backend/ExpectedError.cs | 1 + .../Services/UserRendererService.cs | 25 ++++--- .../app/components/ErrorAlert.tsx | 2 + Foxnouns.Frontend/app/lib/api/error.ts | 1 + .../route.tsx | 73 +++++++++++++++++++ Foxnouns.Frontend/public/locales/en.json | 7 +- 7 files changed, 144 insertions(+), 16 deletions(-) create mode 100644 Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index a634eb2..abb403c 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Web; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -15,7 +16,7 @@ namespace Foxnouns.Backend.Controllers.Authentication; public class AuthController( Config config, DatabaseContext db, - KeyCacheService keyCache, + KeyCacheService keyCacheService, ILogger logger ) : ApiControllerBase { @@ -31,7 +32,7 @@ public class AuthController( config.GoogleAuth.Enabled, config.TumblrAuth.Enabled ); - var state = HttpUtility.UrlEncode(await keyCache.GenerateAuthStateAsync(ct)); + var state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); string? discord = null; if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) discord = @@ -75,6 +76,52 @@ public class AuthController( return NoContent(); } + + [HttpGet("methods/{id}")] + [Authorize("*")] + [ProducesResponseType( + statusCode: StatusCodes.Status200OK + )] + public async Task GetAuthMethodAsync(Snowflake id) + { + var 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) + { + var 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 + ); + + var 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 + ); + + db.Remove(authMethod); + await db.SaveChangesAsync(); + + return NoContent(); + } } public record CallbackResponse( diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index e185d4e..9d30a43 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -148,6 +148,7 @@ public enum ErrorCode UserNotFound, MemberNotFound, AccountAlreadyLinked, + LastAuthMethod, } public class ValidationError diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 145de1a..c6f9e5b 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -67,22 +67,23 @@ public class UserRendererService( renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) : null, - renderAuthMethods - ? authMethods.Select(a => new AuthenticationMethodResponse( - a.Id, - a.AuthType, - a.RemoteId, - a.FediverseApplication != null - ? $"@{a.RemoteUsername}@{a.FediverseApplication.Domain}" - : a.RemoteUsername - )) - : null, + renderAuthMethods ? authMethods.Select(RenderAuthMethod) : null, tokenHidden ? user.ListHidden : null, tokenHidden ? user.LastActive : null, tokenHidden ? user.LastSidReroll : 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 PartialUser RenderPartialUser(User user) => new( user.Id, @@ -118,7 +119,7 @@ public class UserRendererService( [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - IEnumerable? AuthMethods, + IEnumerable? AuthMethods, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? MemberListHidden, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive, @@ -126,7 +127,7 @@ public class UserRendererService( Instant? LastSidReroll ); - public record AuthenticationMethodResponse( + public record AuthMethodResponse( Snowflake Id, [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, string RemoteId, diff --git a/Foxnouns.Frontend/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx index be470a4..68cfc75 100644 --- a/Foxnouns.Frontend/app/components/ErrorAlert.tsx +++ b/Foxnouns.Frontend/app/components/ErrorAlert.tsx @@ -142,6 +142,8 @@ export const errorCodeDesc = (t: TFunction, code: ErrorCode) => { return t("error.errors.user-not-found"); case ErrorCode.AccountAlreadyLinked: return t("error.errors.account-already-linked"); + case ErrorCode.LastAuthMetod: + return t("error.errors.last-auth-method"); } return t("error.errors.generic-error"); diff --git a/Foxnouns.Frontend/app/lib/api/error.ts b/Foxnouns.Frontend/app/lib/api/error.ts index 3f33566..1f0ee6b 100644 --- a/Foxnouns.Frontend/app/lib/api/error.ts +++ b/Foxnouns.Frontend/app/lib/api/error.ts @@ -17,6 +17,7 @@ export enum ErrorCode { UserNotFound = "USER_NOT_FOUND", MemberNotFound = "MEMBER_NOT_FOUND", AccountAlreadyLinked = "ACCOUNT_ALREADY_LINKED", + LastAuthMetod = "LAST_AUTH_METHOD", } export type ValidationError = { diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx new file mode 100644 index 0000000..922f28d --- /dev/null +++ b/Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx @@ -0,0 +1,73 @@ +import { ActionFunctionArgs, json, LoaderFunctionArgs, redirect } from "@remix-run/node"; +import i18n from "~/i18next.server"; +import serverRequest, { fastRequest, getToken } from "~/lib/request.server"; +import { AuthMethod } from "~/lib/api/user"; +import { useTranslation } from "react-i18next"; +import { useLoaderData, Form } from "@remix-run/react"; +import { Button } from "react-bootstrap"; + +export const action = async ({ request }: ActionFunctionArgs) => { + const data = await request.formData(); + const token = getToken(request); + + const id = data.get("remove-id") as string; + + await fastRequest("DELETE", `/auth/methods/${id}`, { token, isInternal: true }); + + return redirect("/settings/auth", 303); +}; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const t = await i18n.getFixedT(request); + const token = getToken(request); + + const method = await serverRequest("GET", `/auth/methods/${params.id}`, { + token, + isInternal: true, + }); + return json({ method, meta: { title: t("settings.auth.remove-auth-method-title") } }); +}; + +export default function RemoveAuthMethodPage() { + const { t } = useTranslation(); + const { method } = useLoaderData(); + + let methodName; + switch (method.type) { + case "EMAIL": + methodName = "email"; + break; + case "DISCORD": + methodName = "Discord"; + break; + case "FEDIVERSE": + methodName = "Fediverse"; + break; + case "GOOGLE": + methodName = "Google"; + break; + case "TUMBLR": + methodName = "Tumblr"; + break; + } + + return ( + <> +

    {t("settings.auth.remove-auth-method-title")}

    +

    + {t("settings.auth.remove-auth-method-hint", { + username: method.remote_username || method.remote_id, + methodName, + })} +

    +

    +
    + + + +

    + + ); +} diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json index c7c1779..a9aa060 100644 --- a/Foxnouns.Frontend/public/locales/en.json +++ b/Foxnouns.Frontend/public/locales/en.json @@ -17,7 +17,8 @@ "internal-server-error": "Server experienced an internal error, please try again later.", "member-not-found": "Member not found, please check your spelling and try again.", "user-not-found": "User not found, please check your spelling and try again.", - "account-already-linked": "This account is already linked with a pronouns.cc account." + "account-already-linked": "This account is already linked with a pronouns.cc account.", + "last-auth-method": "You cannot remove your last authentication method." }, "title": "An error occurred", "more-info": "Click here for a more detailed error" @@ -141,8 +142,10 @@ "new-email-pending": "Email address added! Click the link in your inbox to confirm.", "email-link-success": "Email successfully linked", "redirect-to-auth-hint": "You will be redirected back to the authentication page in a few seconds.", - "email-addresses": "Email addresses", + "remove-auth-method-title": "Remove authentication method", + "remove-auth-method-hint": "Are you sure you want to remove {{username}} ({{methodName}}) from your account? You will no longer be able to log in using it.", "remove-auth-method": "Remove", + "email-addresses": "Email addresses", "discord-accounts": "Linked Discord accounts", "fediverse-accounts": "Linked Fediverse accounts" }, From d0bf638a21f73ef433963c56d718690e9e6745c0 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 23 Nov 2024 20:40:09 +0100 Subject: [PATCH 114/261] fix: check for obviously invalid instance URLs, use correct JSON key for mastodon scopes --- .../Authentication/FediverseAuthController.cs | 4 +++- .../Auth/FediverseAuthService.Mastodon.cs | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 43a2955..8dca588 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -6,7 +6,6 @@ using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using NodaTime; -using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; namespace Foxnouns.Backend.Controllers.Authentication; @@ -25,6 +24,9 @@ public class FediverseAuthController( [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task GetFediverseUrlAsync([FromQuery] string instance) { + if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) + throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); + var url = await fediverseAuthService.GenerateAuthUrlAsync(instance); return Ok(new FediverseUrlResponse(url)); } diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs index 139830b..665e07f 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Web; using Foxnouns.Backend.Database; @@ -17,16 +18,13 @@ public partial class FediverseAuthService Snowflake? existingAppId = null ) { - var resp = await _client.PostAsync( + var resp = await _client.PostAsJsonAsync( $"https://{instance}/api/v1/apps", - new FormUrlEncodedContent( - new Dictionary - { - { "client_name", $"pronouns.cc (+{_config.BaseUrl})" }, - { "redirect_uris", MastodonRedirectUri(instance) }, - { "scope", "read:accounts" }, - { "website", _config.BaseUrl }, - } + new CreateMastodonApplicationRequest( + ClientName: $"pronouns.cc (+{_config.BaseUrl})", + RedirectUris: MastodonRedirectUri(instance), + Scopes: "read read:accounts", + Website: _config.BaseUrl ) ); resp.EnsureSuccessStatusCode(); @@ -237,9 +235,17 @@ public partial class FediverseAuthService private static string MastodonCurrentAppUri(string instance) => $"https://{instance}/api/v1/apps/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 + ); } From 6abf505c40e3bb22cb08474a90581a98b19d8622 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 23 Nov 2024 20:41:11 +0100 Subject: [PATCH 115/261] refactor: make Member.display_name non-nullable and fall back to Member.name --- Foxnouns.Backend/Services/MemberRendererService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index f8588c2..7d7cac0 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -32,7 +32,7 @@ public class MemberRendererService(DatabaseContext db, Config config) member.Id, member.Sid, member.Name, - member.DisplayName, + member.DisplayName ?? member.Name, member.Bio, AvatarUrlFor(member), member.Links, @@ -60,7 +60,7 @@ public class MemberRendererService(DatabaseContext db, Config config) member.Id, member.Sid, member.Name, - member.DisplayName, + member.DisplayName ?? member.Name, member.Bio, AvatarUrlFor(member), member.Names, @@ -87,7 +87,7 @@ public class MemberRendererService(DatabaseContext db, Config config) Snowflake Id, string Sid, string Name, - string? DisplayName, + string DisplayName, string? Bio, string? AvatarUrl, IEnumerable Names, @@ -99,7 +99,7 @@ public class MemberRendererService(DatabaseContext db, Config config) Snowflake Id, string Sid, string Name, - string? DisplayName, + string DisplayName, string? Bio, string? AvatarUrl, string[] Links, From d87856bf2c77c662f8231030acc41e8002cfa358 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 23 Nov 2024 20:41:41 +0100 Subject: [PATCH 116/261] refactor: change ConvertBase64UriToImage from extension method to static method --- .../{AvatarObjectExtensions.cs => ImageObjectExtensions.cs} | 4 ++-- Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 3 ++- Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs | 6 +++++- Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs | 6 +++++- 4 files changed, 14 insertions(+), 5 deletions(-) rename Foxnouns.Backend/Extensions/{AvatarObjectExtensions.cs => ImageObjectExtensions.cs} (97%) diff --git a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs similarity index 97% rename from Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs rename to Foxnouns.Backend/Extensions/ImageObjectExtensions.cs index efa2d60..2126610 100644 --- a/Foxnouns.Backend/Extensions/AvatarObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs @@ -10,7 +10,7 @@ using SixLabors.ImageSharp.Processing.Processors.Transforms; namespace Foxnouns.Backend.Extensions; -public static class AvatarObjectExtensions +public static class ImageObjectExtensions { private static readonly string[] ValidContentTypes = ["image/png", "image/webp", "image/jpeg"]; @@ -39,7 +39,7 @@ public static class AvatarObjectExtensions ) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct); public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage( - this string uri, + string uri, int size, bool crop ) diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index cfe1ca0..e7ce0e3 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -26,7 +26,8 @@ public class CreateFlagInvocable( try { - var (hash, image) = await Payload.ImageData.ConvertBase64UriToImage( + var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( + Payload.ImageData, size: 256, crop: false ); diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs index 91640cb..d97e1a7 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -39,7 +39,11 @@ public class MemberAvatarUpdateInvocable( try { - var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); + var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( + newAvatar, + size: 512, + crop: true + ); var prevHash = member.Avatar; await objectStorageService.PutObjectAsync(Path(id, hash), image, "image/webp"); diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index 31433f9..8147424 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -39,7 +39,11 @@ public class UserAvatarUpdateInvocable( try { - var (hash, image) = await newAvatar.ConvertBase64UriToImage(size: 512, crop: true); + var (hash, image) = await ImageObjectExtensions.ConvertBase64UriToImage( + newAvatar, + size: 512, + crop: true + ); image.Seek(0, SeekOrigin.Begin); var prevHash = user.Avatar; From 142ff36d3a70e23c68b66af342dbe34fa180453a Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 23 Nov 2024 20:43:43 +0100 Subject: [PATCH 117/261] fix: stop crash on start with empty sentry dsn, make max avatar length a constant --- Foxnouns.Backend/Program.cs | 2 +- Foxnouns.Backend/Utils/ValidationUtils.cs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index f0c3e19..17a56d9 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -18,7 +18,7 @@ builder.AddSerilog(); builder .WebHost.UseSentry(opts => { - opts.Dsn = config.Logging.SentryUrl; + opts.Dsn = config.Logging.SentryUrl ?? ""; opts.TracesSampleRate = config.Logging.SentryTracesSampleRate; opts.MaxRequestBodySize = RequestSize.Small; }) diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 0969a47..bb225ff 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -162,6 +162,7 @@ public static partial class ValidationUtils } public const int MaxBioLength = 1024; + public const int MaxAvatarLength = 1_500_000; public static ValidationError? ValidateBio(string? bio) { @@ -183,7 +184,10 @@ public static partial class ValidationUtils return avatar?.Length switch { 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), - > 1_500_000 => ValidationError.GenericValidationError("Avatar is too large", null), + > MaxAvatarLength => ValidationError.GenericValidationError( + "Avatar is too large", + null + ), _ => null, }; } From 4e9c4af4a5244887e854d4df7d19908b4776451f Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 24 Nov 2024 15:37:36 +0100 Subject: [PATCH 118/261] feat(auth): misc fediverse auth improvements - remove automatic app validation - add force refresh option to GetFediverseUrlAsync - pass state to mastodon authorization URI --- .../Authentication/FediverseAuthController.cs | 21 ++- Foxnouns.Backend/Controllers/SidController.cs | 3 + ...210306_RemoveFediverseApplicationTokens.cs | 40 ++++++ .../DatabaseContextModelSnapshot.cs | 8 -- .../Database/Models/FediverseApplication.cs | 4 - Foxnouns.Backend/Foxnouns.Backend.csproj | 2 + .../Auth/FediverseAuthService.Mastodon.cs | 129 +++--------------- .../Services/Auth/FediverseAuthService.cs | 32 +++-- Foxnouns.Backend/packages.lock.json | 84 ++++++------ 9 files changed, 143 insertions(+), 180 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/SidController.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 8dca588..7cb52c8 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -22,12 +22,15 @@ public class FediverseAuthController( [HttpGet] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] - public async Task GetFediverseUrlAsync([FromQuery] string instance) + public async Task GetFediverseUrlAsync( + [FromQuery] string instance, + [FromQuery] bool forceRefresh = false + ) { if (instance.Any(c => c is '@' or ':' or '/') || !instance.Contains('.')) throw new ApiError.BadRequest("Not a valid domain.", "instance", instance); - var url = await fediverseAuthService.GenerateAuthUrlAsync(instance); + var url = await fediverseAuthService.GenerateAuthUrlAsync(instance, forceRefresh); return Ok(new FediverseUrlResponse(url)); } @@ -36,7 +39,11 @@ public class FediverseAuthController( public async Task FediverseCallbackAsync([FromBody] CallbackRequest req) { var app = await fediverseAuthService.GetApplicationAsync(req.Instance); - var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); + var remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync( + app, + req.Code, + req.State + ); var user = await authService.AuthenticateUserAsync( AuthType.Fediverse, @@ -72,12 +79,16 @@ public class FediverseAuthController( ) { var ticketData = await keyCacheService.GetKeyAsync( - $"fediverse:{req.Ticket}" + $"fediverse:{req.Ticket}", + delete: true ); if (ticketData == null) throw new ApiError.BadRequest("Invalid ticket", "ticket", req.Ticket); var 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 @@ -107,7 +118,7 @@ public class FediverseAuthController( return Ok(await authService.GenerateUserTokenAsync(user)); } - public record CallbackRequest(string Instance, string Code); + public record CallbackRequest(string Instance, string Code, string State); private record FediverseUrlResponse(string Url); diff --git a/Foxnouns.Backend/Controllers/SidController.cs b/Foxnouns.Backend/Controllers/SidController.cs new file mode 100644 index 0000000..26c66ef --- /dev/null +++ b/Foxnouns.Backend/Controllers/SidController.cs @@ -0,0 +1,3 @@ +namespace Foxnouns.Backend.Controllers; + +public class SidController { } diff --git a/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs b/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs new file mode 100644 index 0000000..fbc8d3d --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20241123210306_RemoveFediverseApplicationTokens.cs @@ -0,0 +1,40 @@ +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 97316ac..e1e05c2 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -107,10 +107,6 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("bigint") .HasColumnName("id"); - b.Property("AccessToken") - .HasColumnType("text") - .HasColumnName("access_token"); - b.Property("ClientId") .IsRequired() .HasColumnType("text") @@ -130,10 +126,6 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("integer") .HasColumnName("instance_type"); - b.Property("TokenValidUntil") - .HasColumnType("timestamp with time zone") - .HasColumnName("token_valid_until"); - b.HasKey("Id") .HasName("pk_fediverse_applications"); diff --git a/Foxnouns.Backend/Database/Models/FediverseApplication.cs b/Foxnouns.Backend/Database/Models/FediverseApplication.cs index fa7b6a6..882a377 100644 --- a/Foxnouns.Backend/Database/Models/FediverseApplication.cs +++ b/Foxnouns.Backend/Database/Models/FediverseApplication.cs @@ -8,10 +8,6 @@ public class FediverseApplication : BaseModel public required string ClientId { get; set; } public required string ClientSecret { get; set; } public required FediverseInstanceType InstanceType { get; set; } - - // These are for ensuring the application is still valid. - public string? AccessToken { get; set; } - public Instant? TokenValidUntil { get; set; } } public enum FediverseInstanceType diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index a9e7b74..dbc46d3 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -20,6 +20,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + @@ -35,6 +36,7 @@ + diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs index 665e07f..97e411a 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs @@ -3,7 +3,7 @@ using System.Net; using System.Web; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Duration = NodaTime.Duration; +using Foxnouns.Backend.Extensions; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Foxnouns.Backend.Services.Auth; @@ -35,12 +35,6 @@ public partial class FediverseAuthService $"Application created on Mastodon-compatible instance {instance} was null" ); - var token = await GetMastodonAppTokenAsync( - instance, - mastodonApp.ClientId, - mastodonApp.ClientSecret - ); - FediverseApplication app; if (existingAppId == null) @@ -52,8 +46,6 @@ public partial class FediverseAuthService ClientSecret = mastodonApp.ClientSecret, Domain = instance, InstanceType = FediverseInstanceType.MastodonApi, - AccessToken = token, - TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60), }; _db.Add(app); @@ -67,8 +59,6 @@ public partial class FediverseAuthService app.ClientId = mastodonApp.ClientId; app.ClientSecret = mastodonApp.ClientSecret; app.InstanceType = FediverseInstanceType.MastodonApi; - app.AccessToken = null; - app.TokenValidUntil = null; } await _db.SaveChangesAsync(); @@ -76,8 +66,14 @@ public partial class FediverseAuthService return app; } - private async Task GetMastodonUserAsync(FediverseApplication app, string code) + private async Task GetMastodonUserAsync( + FediverseApplication app, + string code, + string state + ) { + await _keyCacheService.ValidateAuthStateAsync(state); + var tokenResp = await _client.PostAsync( MastodonTokenUri(app.Domain), new FormUrlEncodedContent( @@ -122,109 +118,27 @@ public partial class FediverseAuthService private record MastodonTokenResponse([property: J("access_token")] string AccessToken); - // TODO: Mastodon's OAuth documentation doesn't specify a "state" parameter. that feels... wrong - // https://docs.joinmastodon.org/methods/oauth/ - private async Task GenerateMastodonAuthUrlAsync(FediverseApplication app) + private async Task GenerateMastodonAuthUrlAsync( + FediverseApplication app, + bool forceRefresh + ) { - try + if (forceRefresh) { - await ValidateMastodonAppAsync(app); - } - catch (FoxnounsError.RemoteAuthError e) - { - _logger.Error( - e, - "Error validating app token for {AppId} on {Instance}", - app.Id, - app.Domain + _logger.Information( + "An app credentials refresh was requested for {ApplicationId}, creating a new application", + app.Id ); - app = await CreateMastodonApplicationAsync(app.Domain, existingAppId: app.Id); } + var 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))}"; - } - - private async Task ValidateMastodonAppAsync(FediverseApplication app) - { - // If we don't have an access token stored, or it's too old, get one - // When doing this we don't need to fetch the application info - if (app.AccessToken == null || app.TokenValidUntil < _clock.GetCurrentInstant()) - { - _logger.Debug( - "Application {AppId} on instance {Instance} has no valid token, fetching it", - app.Id, - app.Domain - ); - - app.AccessToken = await GetMastodonAppTokenAsync( - app.Domain, - app.ClientId, - app.ClientSecret - ); - app.TokenValidUntil = _clock.GetCurrentInstant() + Duration.FromDays(60); - - _db.Update(app); - await _db.SaveChangesAsync(); - return; - } - - _logger.Debug( - "Checking whether application {AppId} on instance {Instance} is still valid", - app.Id, - app.Domain - ); - - var req = new HttpRequestMessage(HttpMethod.Get, MastodonCurrentAppUri(app.Domain)); - req.Headers.Add("Authorization", $"Bearer {app.AccessToken}"); - - var resp = await _client.SendAsync(req); - if (!resp.IsSuccessStatusCode) - { - var error = await resp.Content.ReadAsStringAsync(); - throw new FoxnounsError.RemoteAuthError( - "Verifying app credentials returned an error", - error - ); - } - } - - private async Task GetMastodonAppTokenAsync( - string instance, - string clientId, - string clientSecret - ) - { - var resp = await _client.PostAsync( - MastodonTokenUri(instance), - new FormUrlEncodedContent( - new Dictionary - { - { "grant_type", "client_credentials" }, - { "client_id", clientId }, - { "client_secret", clientSecret }, - } - ) - ); - if (!resp.IsSuccessStatusCode) - { - var error = await resp.Content.ReadAsStringAsync(); - throw new FoxnounsError.RemoteAuthError( - "Requesting app token returned an error", - error - ); - } - - var token = (await resp.Content.ReadFromJsonAsync())?.AccessToken; - if (token == null) - { - throw new FoxnounsError($"Token response from instance {instance} was invalid"); - } - - return token; + + $"&redirect_uri={HttpUtility.UrlEncode(MastodonRedirectUri(app.Domain))}" + + $"&state={state}"; } private static string MastodonTokenUri(string instance) => $"https://{instance}/oauth/token"; @@ -232,9 +146,6 @@ public partial class FediverseAuthService private static string MastodonCurrentUserUri(string instance) => $"https://{instance}/api/v1/accounts/verify_credentials"; - private static string MastodonCurrentAppUri(string instance) => - $"https://{instance}/api/v1/apps/verify_credentials"; - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] private record PartialMastodonApplication( [property: J("name")] string Name, diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index fc54017..f78fbde 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -1,7 +1,6 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; using Microsoft.EntityFrameworkCore; -using NodaTime; using J = System.Text.Json.Serialization.JsonPropertyNameAttribute; namespace Foxnouns.Backend.Services.Auth; @@ -10,26 +9,27 @@ public partial class FediverseAuthService { private const string NodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/2.0"; - private readonly ILogger _logger; private readonly HttpClient _client; - private readonly DatabaseContext _db; + private readonly ILogger _logger; private readonly Config _config; + private readonly DatabaseContext _db; + private readonly KeyCacheService _keyCacheService; private readonly ISnowflakeGenerator _snowflakeGenerator; - private readonly IClock _clock; public FediverseAuthService( ILogger logger, Config config, DatabaseContext db, - ISnowflakeGenerator snowflakeGenerator, - IClock clock + KeyCacheService keyCacheService, + ISnowflakeGenerator snowflakeGenerator ) { + _logger = logger.ForContext(); _config = config; _db = db; + _keyCacheService = keyCacheService; _snowflakeGenerator = snowflakeGenerator; - _clock = clock; - _logger = logger.ForContext(); + _client = new HttpClient(); _client.DefaultRequestHeaders.Remove("User-Agent"); _client.DefaultRequestHeaders.Remove("Accept"); @@ -37,10 +37,10 @@ public partial class FediverseAuthService _client.DefaultRequestHeaders.Add("Accept", "application/json"); } - public async Task GenerateAuthUrlAsync(string instance) + public async Task GenerateAuthUrlAsync(string instance, bool forceRefresh) { var app = await GetApplicationAsync(instance); - return await GenerateAuthUrlAsync(app); + return await GenerateAuthUrlAsync(app, forceRefresh); } // thank you, gargron and syuilo, for agreeing on a name for *once* in your lives, @@ -96,21 +96,25 @@ public partial class FediverseAuthService ); } - private async Task GenerateAuthUrlAsync(FediverseApplication app) => + private async Task GenerateAuthUrlAsync(FediverseApplication app, bool forceRefresh) => app.InstanceType switch { - FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync(app), + FediverseInstanceType.MastodonApi => await GenerateMastodonAuthUrlAsync( + app, + forceRefresh + ), FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; public async Task GetRemoteFediverseUserAsync( FediverseApplication app, - string code + string code, + string state ) => app.InstanceType switch { - FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code), + FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state), FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index 90ba53c..02ca7ca 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -96,6 +96,19 @@ "Mono.TextTemplating": "2.2.1" } }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Direct", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" + } + }, "Minio": { "type": "Direct", "requested": "[6.0.3, )", @@ -246,6 +259,16 @@ "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" } }, + "System.Text.Json": { + "type": "Direct", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==", + "dependencies": { + "System.IO.Pipelines": "9.0.0", + "System.Text.Encodings.Web": "9.0.0" + } + }, "System.Text.RegularExpressions": { "type": "Direct", "requested": "[4.3.1, )", @@ -412,22 +435,10 @@ }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3KuSxeHoNYdxVYfg2IRZCThcrlJ1XJqIXkAWikCsbm5C/bCjv7G0WoKDyuR98Q+T607QT2Zl5GsbGRkENcV2yQ==", + "resolved": "9.0.0", + "contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==", "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" - } - }, - "Microsoft.Extensions.Caching.Memory": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "7pqivmrZDzo1ADPkRwjy+8jtRKWRCPag9qPI+p7sgu7Q4QreWhcvbiWXsbhP+yY8XSiDvZpu2/LWdBv7PnmOpQ==", - "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Configuration": { @@ -465,8 +476,8 @@ }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "fGLiCRLMYd00JYpClraLjJTNKLmMJPnqxMaiRzEBIIvevlzxz33mXy39Lkd48hu1G+N21S7QpaO5ZzKsI6FRuA==" + "resolved": "9.0.0", + "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", @@ -542,10 +553,11 @@ }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", + "resolved": "9.0.0", + "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "System.Diagnostics.DiagnosticSource": "9.0.0" } }, "Microsoft.Extensions.Logging.Configuration": { @@ -570,11 +582,11 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", + "resolved": "9.0.0", + "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { @@ -591,8 +603,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==" + "resolved": "9.0.0", + "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -983,8 +995,8 @@ }, "System.Diagnostics.DiagnosticSource": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "c9xLpVz6PL9lp/djOWtk5KPDZq3cSYpmXoJQY524EOtuFl5z9ZtsotpsyrDW40U1DRnQSYvcPKEUV0X//u6gkQ==" + "resolved": "9.0.0", + "contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==" }, "System.Diagnostics.Tracing": { "type": "Transitive", @@ -1072,8 +1084,8 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "6.0.3", - "contentHash": "ryTgF+iFkpGZY1vRQhfCzX0xTdlV3pyaTTqRu2ETbEv+HlV7O6y7hyQURnghNIXvctl5DuZ//Dpks6HdL/Txgw==" + "resolved": "9.0.0", + "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" }, "System.Linq": { "type": "Transitive", @@ -1484,16 +1496,8 @@ }, "System.Text.Encodings.Web": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==" - }, - "System.Text.Json": { - "type": "Transitive", - "resolved": "8.0.4", - "contentHash": "bAkhgDJ88XTsqczoxEMliSrpijKZHhbJQldhAmObj/RbrN3sU5dcokuXmWJWsdQAhiMJ9bTayWsL1C9fbbCRhw==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0" - } + "resolved": "9.0.0", + "contentHash": "e2hMgAErLbKyUUwt18qSBf9T5Y+SFAL3ZedM8fLupkVj8Rj2PZ9oxQ37XX2LF8fTO1wNIxvKpihD7Of7D/NxZw==" }, "System.Threading": { "type": "Transitive", From 7cb17409cd3221ac805d75fac51c589b610e7979 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 24 Nov 2024 15:39:44 +0100 Subject: [PATCH 119/261] fix: explicitly set sids to null so the find free sid functions actually trigger --- Foxnouns.Backend/Controllers/MembersController.cs | 1 + Foxnouns.Backend/Services/Auth/AuthService.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 22d23d4..ba9cf28 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -94,6 +94,7 @@ public class MembersController( Names = req.Names ?? [], Pronouns = req.Pronouns ?? [], Unlisted = req.Unlisted ?? false, + Sid = null!, }; db.Add(member); diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs index 9675f22..adbf5b1 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -45,6 +45,7 @@ public class AuthService( }, }, LastActive = clock.GetCurrentInstant(), + Sid = null!, }; db.Add(user); @@ -88,6 +89,7 @@ public class AuthService( }, }, LastActive = clock.GetCurrentInstant(), + Sid = null!, }; db.Add(user); From c8cd483d20c12f71d02f52304fa44ff8c3c2dcf9 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 24 Nov 2024 15:40:12 +0100 Subject: [PATCH 120/261] feat: sid redirect controller --- Foxnouns.Backend/Controllers/SidController.cs | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Controllers/SidController.cs b/Foxnouns.Backend/Controllers/SidController.cs index 26c66ef..b8f5948 100644 --- a/Foxnouns.Backend/Controllers/SidController.cs +++ b/Foxnouns.Backend/Controllers/SidController.cs @@ -1,3 +1,52 @@ +using System.Diagnostics.CodeAnalysis; +using Foxnouns.Backend.Database; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + namespace Foxnouns.Backend.Controllers; -public class SidController { } +[Route("/sid")] +[SuppressMessage( + "Performance", + "CA1862:Use the \'StringComparison\' method overloads to perform case-insensitive string comparisons", + Justification = "Not usable with EFCore" +)] +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) + { + var 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}"); + } +} From 0d47f1fb0198f2959e740fc51e7a644607979922 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 24 Nov 2024 15:55:29 +0100 Subject: [PATCH 121/261] you know what let's just change frontend framework again --- .husky/task-runner.json | 2 +- Foxnouns.Frontend/.env.example | 5 + Foxnouns.Frontend/.eslintrc.cjs | 84 - Foxnouns.Frontend/.gitignore | 18 +- Foxnouns.Frontend/.npmrc | 1 + Foxnouns.Frontend/.prettierignore | 4 + Foxnouns.Frontend/.prettierrc | 11 +- Foxnouns.Frontend/.vscode/settings.json | 8 + Foxnouns.Frontend/Dockerfile | 12 - Foxnouns.Frontend/README.md | 54 +- .../app/components/ErrorAlert.tsx | 150 - .../app/components/KeyedIcon.tsx | 16 - .../app/components/RegisterError.tsx | 36 - .../app/components/nav/BaseNavbar.tsx | 22 - Foxnouns.Frontend/app/components/nav/Logo.tsx | 38 - .../app/components/nav/Navbar.tsx | 35 - .../app/components/profile/AvatarImage.tsx | 22 - .../app/components/profile/BaseProfile.tsx | 118 - .../app/components/profile/ProfileField.tsx | 25 - .../app/components/profile/ProfileFlag.tsx | 28 - .../app/components/profile/ProfileLink.tsx | 27 - .../app/components/profile/PronounLink.tsx | 32 - .../app/components/profile/StatusIcon.tsx | 34 - .../app/components/profile/StatusLine.tsx | 45 - Foxnouns.Frontend/app/entry.client.tsx | 50 - Foxnouns.Frontend/app/entry.server.tsx | 72 - Foxnouns.Frontend/app/env.server.ts | 6 - Foxnouns.Frontend/app/i18n.ts | 5 - Foxnouns.Frontend/app/i18next.server.ts | 28 - Foxnouns.Frontend/app/lib/api/error.ts | 58 - Foxnouns.Frontend/app/lib/request.server.ts | 83 - Foxnouns.Frontend/app/lib/settings.server.ts | 23 - Foxnouns.Frontend/app/lib/utils.ts | 6 - Foxnouns.Frontend/app/root.tsx | 159 - .../app/routes/$username/MemberCard.tsx | 73 - .../app/routes/$username/route.tsx | 128 - .../app/routes/$username_.$member/route.tsx | 64 - Foxnouns.Frontend/app/routes/_index.tsx | 13 - .../routes/auth.callback.discord/route.tsx | 231 - .../route.tsx | 163 - .../app/routes/auth.log-in/route.tsx | 146 - .../routes/auth.log-in_.fediverse/route.tsx | 75 - .../app/routes/auth.log-out/route.tsx | 12 - .../app/routes/auth.welcome/route.tsx | 52 - .../app/routes/dark-mode/route.tsx | 38 - .../app/routes/settings._index/route.tsx | 149 - .../app/routes/settings.auth/route.tsx | 166 - .../route.tsx | 26 - .../routes/settings.auth_.add-email/route.tsx | 105 - .../route.tsx | 38 - .../route.tsx | 73 - .../routes/settings.force-log-out/route.tsx | 48 - .../app/routes/settings/route.tsx | 70 - Foxnouns.Frontend/eslint.config.js | 33 + Foxnouns.Frontend/i18next-parser.config.js | 3 - Foxnouns.Frontend/package.json | 99 +- Foxnouns.Frontend/pnpm-lock.yaml | 2620 ++++++ Foxnouns.Frontend/public/favicon.svg | 2 - Foxnouns.Frontend/public/locales/en.json | 163 - Foxnouns.Frontend/server.js | 51 - Foxnouns.Frontend/src/app.d.ts | 13 + Foxnouns.Frontend/src/app.html | 12 + Foxnouns.Frontend/{app => src}/app.scss | 13 +- Foxnouns.Frontend/src/hooks.server.ts | 13 + Foxnouns.Frontend/src/lib/api/error.ts | 54 + Foxnouns.Frontend/src/lib/api/index.ts | 92 + .../lib/api => src/lib/api/models}/auth.ts | 2 +- Foxnouns.Frontend/src/lib/api/models/index.ts | 4 + .../lib/api => src/lib/api/models}/member.ts | 2 +- .../lib/api => src/lib/api/models}/meta.ts | 5 +- .../lib/api => src/lib/api/models}/user.ts | 10 +- .../src/lib/components/Avatar.svelte | 14 + .../src/lib/components/Error.svelte | 32 + .../src/lib/components/ErrorAlert.svelte | 11 + .../src/lib/components/Logo.svelte | 34 + .../src/lib/components/Navbar.svelte | 69 + .../src/lib/components/StatusIcon.svelte | 17 + .../errors/KeyedValidationErrors.svelte | 16 + .../errors/RequestValidationError.svelte | 41 + .../profile/OwnProfileNotice.svelte | 16 + .../components/profile/ProfileFields.svelte | 26 + .../lib/components/profile/ProfileFlag.svelte | 24 + .../components/profile/ProfileHeader.svelte | 69 + .../lib/components/profile/ProfileLink.svelte | 33 + .../profile/field/ProfileField.svelte | 30 + .../profile/field/ProfileFieldEntry.svelte | 28 + .../profile/field/PronounLink.svelte | 41 + .../components/profile/user/MemberCard.svelte | 49 + .../src/lib/errorCodes.svelte.ts | 36 + Foxnouns.Frontend/src/lib/i18n/index.ts | 24 + .../src/lib/i18n/locales/en-PR.json | 28 + .../src/lib/i18n/locales/en.json | 63 + Foxnouns.Frontend/src/lib/index.ts | 12 + Foxnouns.Frontend/src/lib/log.ts | 4 + .../{app => src}/lib/markdown.ts | 8 +- .../src/routes/+layout.server.ts | 21 + Foxnouns.Frontend/src/routes/+layout.svelte | 13 + Foxnouns.Frontend/src/routes/+page.svelte | 21 + .../src/routes/@[username]/+page.server.ts | 20 + .../src/routes/@[username]/+page.svelte | 60 + .../src/routes/@[username]/Paginator.svelte | 31 + .../mastodon/[instance]/+page.server.ts | 62 + .../callback/mastodon/[instance]/+page.svelte | 35 + .../src/routes/auth/log-in/+page.server.ts | 79 + .../src/routes/auth/log-in/+page.svelte | 88 + .../src/routes/auth/welcome/+page.server.ts | 6 + .../src/routes/auth/welcome/+page.svelte | 34 + Foxnouns.Frontend/static/favicon.png | Bin 0 -> 1571 bytes Foxnouns.Frontend/svelte.config.js | 27 + Foxnouns.Frontend/tsconfig.json | 39 +- Foxnouns.Frontend/vite.config.ts | 26 +- Foxnouns.Frontend/yarn.lock | 7372 ----------------- package.json | 7 +- pnpm-lock.yaml | 211 + yarn.lock | 176 - 115 files changed, 4407 insertions(+), 10824 deletions(-) create mode 100644 Foxnouns.Frontend/.env.example delete mode 100644 Foxnouns.Frontend/.eslintrc.cjs create mode 100644 Foxnouns.Frontend/.npmrc create mode 100644 Foxnouns.Frontend/.prettierignore create mode 100644 Foxnouns.Frontend/.vscode/settings.json delete mode 100644 Foxnouns.Frontend/Dockerfile delete mode 100644 Foxnouns.Frontend/app/components/ErrorAlert.tsx delete mode 100644 Foxnouns.Frontend/app/components/KeyedIcon.tsx delete mode 100644 Foxnouns.Frontend/app/components/RegisterError.tsx delete mode 100644 Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx delete mode 100644 Foxnouns.Frontend/app/components/nav/Logo.tsx delete mode 100644 Foxnouns.Frontend/app/components/nav/Navbar.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/AvatarImage.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/BaseProfile.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/ProfileField.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/ProfileLink.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/PronounLink.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/StatusIcon.tsx delete mode 100644 Foxnouns.Frontend/app/components/profile/StatusLine.tsx delete mode 100644 Foxnouns.Frontend/app/entry.client.tsx delete mode 100644 Foxnouns.Frontend/app/entry.server.tsx delete mode 100644 Foxnouns.Frontend/app/env.server.ts delete mode 100644 Foxnouns.Frontend/app/i18n.ts delete mode 100644 Foxnouns.Frontend/app/i18next.server.ts delete mode 100644 Foxnouns.Frontend/app/lib/api/error.ts delete mode 100644 Foxnouns.Frontend/app/lib/request.server.ts delete mode 100644 Foxnouns.Frontend/app/lib/settings.server.ts delete mode 100644 Foxnouns.Frontend/app/lib/utils.ts delete mode 100644 Foxnouns.Frontend/app/root.tsx delete mode 100644 Foxnouns.Frontend/app/routes/$username/MemberCard.tsx delete mode 100644 Foxnouns.Frontend/app/routes/$username/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/$username_.$member/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/_index.tsx delete mode 100644 Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/auth.log-in/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/auth.log-out/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/auth.welcome/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/dark-mode/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings._index/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings.auth/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings.auth_.add-email/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings.auth_.confirm-email.$code/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx delete mode 100644 Foxnouns.Frontend/app/routes/settings/route.tsx create mode 100644 Foxnouns.Frontend/eslint.config.js delete mode 100644 Foxnouns.Frontend/i18next-parser.config.js create mode 100644 Foxnouns.Frontend/pnpm-lock.yaml delete mode 100644 Foxnouns.Frontend/public/favicon.svg delete mode 100644 Foxnouns.Frontend/public/locales/en.json delete mode 100644 Foxnouns.Frontend/server.js create mode 100644 Foxnouns.Frontend/src/app.d.ts create mode 100644 Foxnouns.Frontend/src/app.html rename Foxnouns.Frontend/{app => src}/app.scss (68%) create mode 100644 Foxnouns.Frontend/src/hooks.server.ts create mode 100644 Foxnouns.Frontend/src/lib/api/error.ts create mode 100644 Foxnouns.Frontend/src/lib/api/index.ts rename Foxnouns.Frontend/{app/lib/api => src/lib/api/models}/auth.ts (89%) create mode 100644 Foxnouns.Frontend/src/lib/api/models/index.ts rename Foxnouns.Frontend/{app/lib/api => src/lib/api/models}/member.ts (60%) rename Foxnouns.Frontend/{app/lib/api => src/lib/api/models}/meta.ts (85%) rename Foxnouns.Frontend/{app/lib/api => src/lib/api/models}/user.ts (93%) create mode 100644 Foxnouns.Frontend/src/lib/components/Avatar.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/Error.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/ErrorAlert.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/Logo.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/Navbar.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/StatusIcon.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/errors/KeyedValidationErrors.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/errors/RequestValidationError.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/OwnProfileNotice.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/ProfileFields.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/field/ProfileField.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/field/ProfileFieldEntry.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/field/PronounLink.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte create mode 100644 Foxnouns.Frontend/src/lib/errorCodes.svelte.ts create mode 100644 Foxnouns.Frontend/src/lib/i18n/index.ts create mode 100644 Foxnouns.Frontend/src/lib/i18n/locales/en-PR.json create mode 100644 Foxnouns.Frontend/src/lib/i18n/locales/en.json create mode 100644 Foxnouns.Frontend/src/lib/index.ts create mode 100644 Foxnouns.Frontend/src/lib/log.ts rename Foxnouns.Frontend/{app => src}/lib/markdown.ts (60%) create mode 100644 Foxnouns.Frontend/src/routes/+layout.server.ts create mode 100644 Foxnouns.Frontend/src/routes/+layout.svelte create mode 100644 Foxnouns.Frontend/src/routes/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/@[username]/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/@[username]/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/@[username]/Paginator.svelte create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/auth/log-in/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/auth/welcome/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/welcome/+page.svelte create mode 100644 Foxnouns.Frontend/static/favicon.png create mode 100644 Foxnouns.Frontend/svelte.config.js delete mode 100644 Foxnouns.Frontend/yarn.lock create mode 100644 pnpm-lock.yaml delete mode 100644 yarn.lock diff --git a/.husky/task-runner.json b/.husky/task-runner.json index aaa37a4..8e50f6a 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -3,7 +3,7 @@ "tasks": [ { "name": "run-prettier", - "command": "yarn", + "command": "pnpm", "args": ["format"], "pathMode": "absolute" }, diff --git a/Foxnouns.Frontend/.env.example b/Foxnouns.Frontend/.env.example new file mode 100644 index 0000000..d3d5832 --- /dev/null +++ b/Foxnouns.Frontend/.env.example @@ -0,0 +1,5 @@ +# Example .env file--DO NOT EDIT +PUBLIC_LANGUAGE=en +PUBLIC_API_BASE=https://pronouns.cc/api +PRIVATE_API_HOST=http://localhost:5003/api +PRIVATE_INTERNAL_API_HOST=http://localhost:5000/api diff --git a/Foxnouns.Frontend/.eslintrc.cjs b/Foxnouns.Frontend/.eslintrc.cjs deleted file mode 100644 index a50c150..0000000 --- a/Foxnouns.Frontend/.eslintrc.cjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - ignorePatterns: ["!**/.server", "!**/.client"], - - // Base config - extends: ["eslint:recommended"], - - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - settings: { - react: { - version: "detect", - }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - - // Node - { - files: [".eslintrc.cjs"], - env: { - node: true, - }, - }, - ], -}; diff --git a/Foxnouns.Frontend/.gitignore b/Foxnouns.Frontend/.gitignore index 80ec311..79518f7 100644 --- a/Foxnouns.Frontend/.gitignore +++ b/Foxnouns.Frontend/.gitignore @@ -1,5 +1,21 @@ node_modules -/.cache +# 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 new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/Foxnouns.Frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/Foxnouns.Frontend/.prettierignore b/Foxnouns.Frontend/.prettierignore new file mode 100644 index 0000000..ab78a95 --- /dev/null +++ b/Foxnouns.Frontend/.prettierignore @@ -0,0 +1,4 @@ +# Package Managers +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/Foxnouns.Frontend/.prettierrc b/Foxnouns.Frontend/.prettierrc index cb96cd0..f166279 100644 --- a/Foxnouns.Frontend/.prettierrc +++ b/Foxnouns.Frontend/.prettierrc @@ -1,4 +1,13 @@ { "useTabs": true, - "printWidth": 100 + "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 new file mode 100644 index 0000000..5703e7f --- /dev/null +++ b/Foxnouns.Frontend/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "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 +} diff --git a/Foxnouns.Frontend/Dockerfile b/Foxnouns.Frontend/Dockerfile deleted file mode 100644 index 4150c99..0000000 --- a/Foxnouns.Frontend/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM docker.io/node:22 - -RUN mkdir -p /app/node_modules && chown -R node:node /app -WORKDIR /app -COPY package.json yarn.lock ./ -USER node -RUN yarn -COPY --chown=node:node . . - -RUN yarn build - -CMD ["yarn", "start"] diff --git a/Foxnouns.Frontend/README.md b/Foxnouns.Frontend/README.md index 6c4d216..b5b2950 100644 --- a/Foxnouns.Frontend/README.md +++ b/Foxnouns.Frontend/README.md @@ -1,40 +1,38 @@ -# Welcome to Remix! +# sv -- 📖 [Remix docs](https://remix.run/docs) +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). -## Development +## Creating a project -Run the dev server: +If you're seeing this, you've probably already done this step. Congrats! -```shellscript -npm run dev +```bash +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app ``` -## Deployment +## Developing -First, build your app for production: +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: -```sh +```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 ``` -Then run the app in production mode: +You can preview the production build with `npm run preview`. -```sh -npm start -``` - -Now you'll need to pick a host to deploy it to. - -### DIY - -If you're familiar with deploying Node applications, the built-in Remix app server is production-ready. - -Make sure to deploy the output of `npm run build` - -- `build/server` -- `build/client` - -## Styling - -This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever css framework you prefer. See the [Vite docs on css](https://vitejs.dev/guide/features.html#css) for more information. +> 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/app/components/ErrorAlert.tsx b/Foxnouns.Frontend/app/components/ErrorAlert.tsx deleted file mode 100644 index 68cfc75..0000000 --- a/Foxnouns.Frontend/app/components/ErrorAlert.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { TFunction } from "i18next"; -import { Alert } from "react-bootstrap"; -import { Trans, useTranslation } from "react-i18next"; -import { - ApiError, - ErrorCode, - ValidationError, - validationErrorType, - ValidationErrorType, -} from "~/lib/api/error"; - -export default function ErrorAlert({ error }: { error: ApiError }) { - const { t } = useTranslation(); - - return ( - - {t("error.heading")} - {errorCodeDesc(t, error.code)} - {error.errors && ( -
      - {error.errors.map((e, i) => ( - - ))} -
    - )} -
    - ); -} - -function ValidationErrors({ errorKey, errors }: { errorKey: string; errors: ValidationError[] }) { - return ( -
  • - - {errorKey} - - : -
      - {errors.map((e, i) => ( -
    • - -
    • - ))} -
    -
  • - ); -} - -function ValidationErrorEntry({ error }: { error: ValidationError }) { - const { t } = useTranslation(); - - const { - min_length: minLength, - max_length: maxLength, - actual_length: actualLength, - message: reason, - actual_value: actualValue, - allowed_values: allowedValues, - } = error; - - switch (validationErrorType(error)) { - case ValidationErrorType.LengthError: - if (error.actual_length! > error.max_length!) { - return ( - - Value is too long, maximum length is {{ maxLength }}, current length is{" "} - {{ actualLength }}. - - ); - } - - if (error.actual_length! < error.min_length!) { - return ( - - Value is too short, minimum length is {{ minLength }}, current length is{" "} - {{ actualLength }}. - - ); - } - - break; - - case ValidationErrorType.DisallowedValueError: - return ( - v.toString()).join(", "), - }} - > - {/* @ts-expect-error i18next handles interpolation */} - The value {{ actualValue }} is not allowed here. Allowed values are:{" "} - {/* @ts-expect-error i18next handles interpolation */} - {{ allowedValues }} - - ); - - default: - if (error.actual_value) { - return ( - - {/* @ts-expect-error i18next handles interpolation */} - The value {{ actualValue }} is not allowed here. Reason: {{ reason }} - - ); - } - - return <>{t("error.validation.generic-no-value", { reason: error.message })}; - } -} - -export const errorCodeDesc = (t: TFunction, code: ErrorCode) => { - switch (code) { - case ErrorCode.AuthenticationError: - return t("error.errors.authentication-error"); - case ErrorCode.AuthenticationRequired: - return t("error.errors.authentication-required"); - case ErrorCode.BadRequest: - return t("error.errors.bad-request"); - case ErrorCode.Forbidden: - return t("error.errors.forbidden"); - case ErrorCode.GenericApiError: - return t("error.errors.generic-error"); - case ErrorCode.InternalServerError: - return t("error.errors.internal-server-error"); - case ErrorCode.MemberNotFound: - return t("error.errors.member-not-found"); - case ErrorCode.UserNotFound: - return t("error.errors.user-not-found"); - case ErrorCode.AccountAlreadyLinked: - return t("error.errors.account-already-linked"); - case ErrorCode.LastAuthMetod: - return t("error.errors.last-auth-method"); - } - - return t("error.errors.generic-error"); -}; diff --git a/Foxnouns.Frontend/app/components/KeyedIcon.tsx b/Foxnouns.Frontend/app/components/KeyedIcon.tsx deleted file mode 100644 index 27a4ff0..0000000 --- a/Foxnouns.Frontend/app/components/KeyedIcon.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import * as icons from "react-bootstrap-icons"; -import { IconProps as BaseIconProps } from "react-bootstrap-icons"; -import { pascalCase } from "change-case"; - -const startsWithNumberRegex = /^\d/; - -export default function Icon({ iconName, ...props }: BaseIconProps & { iconName: string }) { - let icon = pascalCase(iconName); - if (startsWithNumberRegex.test(icon)) { - icon = `Icon${icon}`; - } - - // eslint-disable-next-line import/namespace - const BootstrapIcon = icons[icon as keyof typeof icons]; - return ; -} diff --git a/Foxnouns.Frontend/app/components/RegisterError.tsx b/Foxnouns.Frontend/app/components/RegisterError.tsx deleted file mode 100644 index 1ecbbdb..0000000 --- a/Foxnouns.Frontend/app/components/RegisterError.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ApiError, firstErrorFor } from "~/lib/api/error"; -import { Trans, useTranslation } from "react-i18next"; -import { Alert } from "react-bootstrap"; -import { Link } from "@remix-run/react"; -import ErrorAlert from "~/components/ErrorAlert"; - -export default function RegisterError({ error }: { error: ApiError }) { - const { t } = useTranslation(); - - // TODO: maybe turn these messages into their own error codes? - const ticketMessage = firstErrorFor(error, "ticket")?.message; - const usernameMessage = firstErrorFor(error, "username")?.message; - - if (ticketMessage === "Invalid ticket") { - return ( - - {t("error.heading")} - - Invalid ticket (it might have been too long since you logged in), please{" "} - try again. - - - ); - } - - if (usernameMessage === "Username is already taken") { - return ( - - {t("log-in.callback.invalid-username")} - {t("log-in.callback.username-taken")} - - ); - } - - return ; -} diff --git a/Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx b/Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx deleted file mode 100644 index 43970d9..0000000 --- a/Foxnouns.Frontend/app/components/nav/BaseNavbar.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { ReactNode } from "react"; -import { Nav, Navbar } from "react-bootstrap"; -import { Link } from "@remix-run/react"; -import Logo from "~/components/nav/Logo"; - -export default function BaseNavbar({ children }: { children?: ReactNode }) { - return ( - - - - - {children && ( - <> - - - - - - )} - - ); -} diff --git a/Foxnouns.Frontend/app/components/nav/Logo.tsx b/Foxnouns.Frontend/app/components/nav/Logo.tsx deleted file mode 100644 index f1c8e40..0000000 --- a/Foxnouns.Frontend/app/components/nav/Logo.tsx +++ /dev/null @@ -1,38 +0,0 @@ -export default function Logo() { - return ( - - - - - - - - - - - - - - - - ); -} diff --git a/Foxnouns.Frontend/app/components/nav/Navbar.tsx b/Foxnouns.Frontend/app/components/nav/Navbar.tsx deleted file mode 100644 index 60471b9..0000000 --- a/Foxnouns.Frontend/app/components/nav/Navbar.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Link, useFetcher } from "@remix-run/react"; -import Meta from "~/lib/api/meta"; -import { User } from "~/lib/api/user"; - -import { Nav, NavDropdown } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; -import BaseNavbar from "~/components/nav/BaseNavbar"; - -export default function MainNavbar({ user }: { meta: Meta; user?: User }) { - const fetcher = useFetcher(); - const { t } = useTranslation(); - - const userMenu = user ? ( - @{user.username}} align="end"> - - {t("navbar.view-profile")} - - - {t("navbar.settings")} - - - - - {t("navbar.log-out")} - - - - ) : ( - - {t("navbar.log-in")} - - ); - - return {userMenu}; -} diff --git a/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx b/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx deleted file mode 100644 index e29ff75..0000000 --- a/Foxnouns.Frontend/app/components/profile/AvatarImage.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export default function AvatarImage({ - src, - width, - alt, - lazyLoad, -}: { - src: string; - width: number; - alt: string; - lazyLoad?: boolean; -}) { - return ( - {alt} - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx b/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx deleted file mode 100644 index 2d6171e..0000000 --- a/Foxnouns.Frontend/app/components/profile/BaseProfile.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { CustomPreference, User } from "~/lib/api/user"; -import { Member } from "~/lib/api/member"; -import { defaultAvatarUrl } from "~/lib/utils"; -import ProfileFlag from "~/components/profile/ProfileFlag"; -import ProfileLink from "~/components/profile/ProfileLink"; -import ProfileField from "~/components/profile/ProfileField"; -import { useTranslation } from "react-i18next"; -import { renderMarkdown } from "~/lib/markdown"; -import AvatarImage from "~/components/profile/AvatarImage"; - -export type Props = { - name: string; - fullName?: string; - userI18nKeys: boolean; - profile: User | Member; - customPreferences: Record; -}; - -export default function BaseProfile({ - name, - userI18nKeys, - fullName, - profile, - customPreferences, -}: Props) { - const { t } = useTranslation(); - const bio = renderMarkdown(profile.bio); - - return ( - <> -
    -
    -
    - {userI18nKeys ? ( - - ) : ( - - )} - {profile.flags && profile.bio && ( -
    - {profile.flags.map((f, i) => ( - - ))} -
    - )} -
    -
    - {profile.display_name || fullName ? ( - <> -

    {profile.display_name || name}

    -

    {fullName || `@${name}`}

    - - ) : ( - <> -

    {fullName || `@${name}`}

    - - )} - {bio && ( - <> -
    -

    - - )} -
    - {profile.links.length > 0 && ( -
    -
      - {profile.links.map((l, i) => ( - - ))} -
    -
    - )} -
    -
    - {profile.names.length > 0 && ( - - )} - {profile.pronouns.length > 0 && ( - - )} - {profile.fields.map((f, i) => ( - - ))} -
    -
    - {/* If a user or member has no bio, flags are displayed in a row below the other profile info, rather than just below the avatar */} - {profile.flags && !profile.bio && ( -
    - {profile.flags.map((f, i) => ( - - ))} -
    - )} - - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/ProfileField.tsx b/Foxnouns.Frontend/app/components/profile/ProfileField.tsx deleted file mode 100644 index 92d8a46..0000000 --- a/Foxnouns.Frontend/app/components/profile/ProfileField.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { CustomPreference, FieldEntry, Pronoun } from "~/lib/api/user"; -import StatusLine from "~/components/profile/StatusLine"; - -export default function ProfileField({ - name, - entries, - preferences, -}: { - name: string; - entries: Array; - preferences: Record; -}) { - return ( -
    -

    {name}

    -
      - {entries.map((e, i) => ( -
    • - -
    • - ))} -
    -
    - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx b/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx deleted file mode 100644 index 756783d..0000000 --- a/Foxnouns.Frontend/app/components/profile/ProfileFlag.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { PrideFlag } from "~/lib/api/user"; -import { OverlayTrigger, Tooltip } from "react-bootstrap"; - -export default function ProfileFlag({ flag }: { flag: PrideFlag }) { - return ( - - - {flag.description ?? flag.name} - - } - > - - {flag.description - - {" "} - {flag.name} - - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/ProfileLink.tsx b/Foxnouns.Frontend/app/components/profile/ProfileLink.tsx deleted file mode 100644 index da152b6..0000000 --- a/Foxnouns.Frontend/app/components/profile/ProfileLink.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Globe } from "react-bootstrap-icons"; - -export default function ProfileLink({ link }: { link: string }) { - const isLink = link.startsWith("http://") || link.startsWith("https://"); - - let displayLink = link; - if (link.startsWith("http://")) displayLink = link.substring("http://".length); - else if (link.startsWith("https://")) displayLink = link.substring("https://".length); - if (displayLink.endsWith("/")) displayLink = displayLink.substring(0, displayLink.length - 1); - - if (isLink) { - return ( - -
  • - {" "} - {displayLink} -
  • -
    - ); - } - - return ( -
  • - {displayLink} -
  • - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/PronounLink.tsx b/Foxnouns.Frontend/app/components/profile/PronounLink.tsx deleted file mode 100644 index 2c74013..0000000 --- a/Foxnouns.Frontend/app/components/profile/PronounLink.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Pronoun } from "~/lib/api/user"; -import { Link } from "@remix-run/react"; - -export default function PronounLink({ pronoun }: { pronoun: Pronoun }) { - let displayText: string; - if (pronoun.display_text) displayText = pronoun.display_text; - else { - const split = pronoun.value.split("/"); - if (split.length === 5) displayText = split.splice(0, 2).join("/"); - else displayText = pronoun.value; - } - - let link: string; - const linkBase = pronoun.value - .split("/") - .map((value) => encodeURIComponent(value)) - .join("/"); - - if (pronoun.display_text) { - link = `${linkBase},${encodeURIComponent(pronoun.display_text)}`; - } else { - link = linkBase; - } - - return pronoun.value.split("/").length === 5 ? ( - - {displayText} - - ) : ( - <>{displayText} - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/StatusIcon.tsx b/Foxnouns.Frontend/app/components/profile/StatusIcon.tsx deleted file mode 100644 index 9f2fa89..0000000 --- a/Foxnouns.Frontend/app/components/profile/StatusIcon.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { CustomPreference, defaultPreferences, mergePreferences } from "~/lib/api/user"; -import { OverlayTrigger, Tooltip } from "react-bootstrap"; -import Icon from "~/components/KeyedIcon"; - -export default function StatusIcon({ - preferences, - status, -}: { - preferences: Record; - status: string; -}) { - const mergedPrefs = mergePreferences(preferences); - const currentPref = status in mergedPrefs ? mergedPrefs[status] : defaultPreferences.missing; - - const id = crypto.randomUUID(); - return ( - <> - - {currentPref.tooltip} - - } - > - - - - - {currentPref.tooltip}: - - ); -} diff --git a/Foxnouns.Frontend/app/components/profile/StatusLine.tsx b/Foxnouns.Frontend/app/components/profile/StatusLine.tsx deleted file mode 100644 index 3729e8a..0000000 --- a/Foxnouns.Frontend/app/components/profile/StatusLine.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { - CustomPreference, - defaultPreferences, - FieldEntry, - mergePreferences, - PreferenceSize, - Pronoun, -} from "~/lib/api/user"; -import classNames from "classnames"; -import StatusIcon from "~/components/profile/StatusIcon"; -import PronounLink from "~/components/profile/PronounLink"; - -export default function StatusLine({ - entry, - preferences, -}: { - entry: FieldEntry | Pronoun; - preferences: Record; -}) { - const mergedPrefs = mergePreferences(preferences); - const currentPref = - entry.status in mergedPrefs ? mergedPrefs[entry.status] : defaultPreferences.missing; - - const classes = classNames({ - "text-muted": currentPref.muted, - "fw-bold fs-5": currentPref.size == PreferenceSize.Large, - "fs-6": currentPref.size == PreferenceSize.Small, - }); - - if ("display_text" in entry) { - const pronoun = entry as Pronoun; - return ( - - {" "} - - - ); - } - - return ( - - {entry.value} - - ); -} diff --git a/Foxnouns.Frontend/app/entry.client.tsx b/Foxnouns.Frontend/app/entry.client.tsx deleted file mode 100644 index d082e6c..0000000 --- a/Foxnouns.Frontend/app/entry.client.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; -import { hydrateRoot } from "react-dom/client"; -import i18n from "./i18n"; -import i18next from "i18next"; -import { I18nextProvider, initReactI18next } from "react-i18next"; -import LanguageDetector from "i18next-browser-languagedetector"; -import Backend from "i18next-http-backend"; -import { getInitialNamespaces } from "remix-i18next/client"; - -async function hydrate() { - await i18next - .use(initReactI18next) // Tell i18next to use the react-i18next plugin - .use(LanguageDetector) // Set up a client-side language detector - .use(Backend) // Setup your backend - .init({ - ...i18n, // spread the configuration - // This function detects the namespaces your routes rendered while SSR use - ns: getInitialNamespaces(), - backend: { loadPath: "/locales/{{lng}}.json" }, - detection: { - // Here only enable htmlTag detection, we'll detect the language only - // server-side with remix-i18next, by using the `` attribute - // we can communicate to the client the language detected server-side - order: ["htmlTag"], - // Because we only use htmlTag, there's no reason to cache the language - // on the browser, so we disable it - caches: [], - }, - }); - - startTransition(() => { - hydrateRoot( - document, - - - - - , - ); - }); -} - -if (window.requestIdleCallback) { - window.requestIdleCallback(hydrate); -} else { - // Safari doesn't support requestIdleCallback - // https://caniuse.com/requestidlecallback - window.setTimeout(hydrate, 1); -} diff --git a/Foxnouns.Frontend/app/entry.server.tsx b/Foxnouns.Frontend/app/entry.server.tsx deleted file mode 100644 index 33fd4af..0000000 --- a/Foxnouns.Frontend/app/entry.server.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { PassThrough } from "stream"; -import { createReadableStreamFromReadable, type EntryContext } from "@remix-run/node"; -import { RemixServer } from "@remix-run/react"; -import { isbot } from "isbot"; -import { renderToPipeableStream } from "react-dom/server"; -import { createInstance } from "i18next"; -import i18next from "./i18next.server"; -import { I18nextProvider, initReactI18next } from "react-i18next"; -import Backend from "i18next-fs-backend"; -import i18n from "./i18n"; // your i18n configuration file -import { resolve } from "node:path"; - -const ABORT_DELAY = 5000; - -export default async function handleRequest( - request: Request, - responseStatusCode: number, - responseHeaders: Headers, - remixContext: EntryContext, -) { - const callbackName = isbot(request.headers.get("user-agent")) ? "onAllReady" : "onShellReady"; - - const instance = createInstance(); - const lng = await i18next.getLocale(request); - const ns = i18next.getRouteNamespaces(remixContext); - - await instance - .use(initReactI18next) // Tell our instance to use react-i18next - .use(Backend) // Set up our backend - .init({ - ...i18n, // spread the configuration - lng, // The locale we detected above - ns, // The namespaces the routes about to render wants to use - backend: { loadPath: resolve("./public/locales/{{lng}}.json") }, - }); - - return new Promise((resolve, reject) => { - let didError = false; - - const { pipe, abort } = renderToPipeableStream( - - - , - { - [callbackName]: () => { - const body = new PassThrough(); - const stream = createReadableStreamFromReadable(body); - responseHeaders.set("Content-Type", "text/html"); - - resolve( - new Response(stream, { - headers: responseHeaders, - status: didError ? 500 : responseStatusCode, - }), - ); - - pipe(body); - }, - onShellError(error: unknown) { - reject(error); - }, - onError(error: unknown) { - didError = true; - - console.error(error); - }, - }, - ); - - setTimeout(abort, ABORT_DELAY); - }); -} diff --git a/Foxnouns.Frontend/app/env.server.ts b/Foxnouns.Frontend/app/env.server.ts deleted file mode 100644 index 5e5e84b..0000000 --- a/Foxnouns.Frontend/app/env.server.ts +++ /dev/null @@ -1,6 +0,0 @@ -import "dotenv/config"; -import { env } from "node:process"; - -export const API_BASE = env.API_BASE || "https://pronouns.localhost/api"; -export const INTERNAL_API_BASE = env.INTERNAL_API_BASE || "https://localhost:5000/api"; -export const LANGUAGE = env.LANGUAGE || "en"; diff --git a/Foxnouns.Frontend/app/i18n.ts b/Foxnouns.Frontend/app/i18n.ts deleted file mode 100644 index 8ef310e..0000000 --- a/Foxnouns.Frontend/app/i18n.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - supportedLngs: ["en", "en-XX"], - fallbackLng: "en", - defaultNS: "common", -}; diff --git a/Foxnouns.Frontend/app/i18next.server.ts b/Foxnouns.Frontend/app/i18next.server.ts deleted file mode 100644 index 28fbbf0..0000000 --- a/Foxnouns.Frontend/app/i18next.server.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Backend from "i18next-fs-backend"; -import { resolve } from "node:path"; -import { RemixI18Next } from "remix-i18next/server"; -import i18n from "~/i18n"; -import { LANGUAGE } from "~/env.server"; - -const i18next = new RemixI18Next({ - detection: { - supportedLanguages: [LANGUAGE], - fallbackLanguage: LANGUAGE, - }, - // This is the configuration for i18next used - // when translating messages server-side only - i18next: { - ...i18n, - fallbackLng: LANGUAGE, - lng: LANGUAGE, - backend: { - loadPath: resolve("./public/locales/{{lng}}.json"), - }, - }, - // The i18next plugins you want RemixI18next to use for `i18n.getFixedT` inside loaders and actions. - // E.g. The Backend plugin for loading translations from the file system - // Tip: You could pass `resources` to the `i18next` configuration and avoid a backend here - plugins: [Backend], -}); - -export default i18next; diff --git a/Foxnouns.Frontend/app/lib/api/error.ts b/Foxnouns.Frontend/app/lib/api/error.ts deleted file mode 100644 index 1f0ee6b..0000000 --- a/Foxnouns.Frontend/app/lib/api/error.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -export type ApiError = { - 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", - LastAuthMetod = "LAST_AUTH_METHOD", -} - -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: ApiError, 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; -}; - -export enum ValidationErrorType { - LengthError = 0, - DisallowedValueError = 1, - GenericValidationError = 2, -} - -export const validationErrorType = (error: ValidationError) => { - if (error.min_length && error.max_length && error.actual_length) { - return ValidationErrorType.LengthError; - } - if (error.allowed_values && error.actual_value) { - return ValidationErrorType.DisallowedValueError; - } - return ValidationErrorType.GenericValidationError; -}; diff --git a/Foxnouns.Frontend/app/lib/request.server.ts b/Foxnouns.Frontend/app/lib/request.server.ts deleted file mode 100644 index 49f2d20..0000000 --- a/Foxnouns.Frontend/app/lib/request.server.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { parse as parseCookie, serialize as serializeCookie } from "cookie"; -import { API_BASE, INTERNAL_API_BASE } from "~/env.server"; -import { ApiError, ErrorCode } from "./api/error"; -import { tokenCookieName } from "~/lib/utils"; - -export type RequestParams = { - token?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - body?: any; - headers?: Record; - isInternal?: boolean; -}; - -export type RequestMethod = "GET" | "POST" | "PATCH" | "DELETE"; - -export async function baseRequest( - method: RequestMethod, - path: string, - params: RequestParams = {}, -): Promise { - // Internal requests, unauthenticated requests, and GET requests bypass the rate limiting proxy. - // All other requests go through the proxy, and are rate limited. - let base = params.isInternal || !params.token || method === "GET" ? INTERNAL_API_BASE : API_BASE; - base += params.isInternal ? "/internal" : "/v2"; - - const url = `${base}${path}`; - const resp = await fetch(url, { - method, - body: params.body ? JSON.stringify(params.body) : undefined, - headers: { - ...params.headers, - ...(params.token ? { Authorization: params.token } : {}), - ...(params.body ? { "Content-Type": "application/json" } : {}), - }, - }); - - if (resp.headers.get("Content-Type")?.indexOf("application/json") === -1) { - // If we don't get a JSON response, the server almost certainly encountered an internal error it couldn't recover from - // (that, or the reverse proxy, which should also be treated as a 500 error) - throw { - status: 500, - code: ErrorCode.InternalServerError, - message: "Internal server error", - } as ApiError; - } - - if (resp.status < 200 || resp.status >= 400) throw (await resp.json()) as ApiError; - return resp; -} - -export async function fastRequest(method: RequestMethod, path: string, params: RequestParams = {}) { - await baseRequest(method, path, params); -} - -export default async function serverRequest( - method: RequestMethod, - path: string, - params: RequestParams = {}, -) { - const resp = await baseRequest(method, path, params); - return (await resp.json()) as T; -} - -export const getToken = (req: Request) => getCookie(req, tokenCookieName); - -export function getCookie(req: Request, cookieName: string): string | undefined { - const header = req.headers.get("Cookie"); - if (!header) return undefined; - - const cookie = parseCookie(header); - return cookieName in cookie ? cookie[cookieName] : undefined; -} - -const YEAR = 365 * 86400; - -export const writeCookie = (cookieName: string, value: string, maxAge: number | undefined = YEAR) => - serializeCookie(cookieName, value, { - maxAge, - path: "/", - sameSite: "lax", - httpOnly: true, - secure: true, - }); diff --git a/Foxnouns.Frontend/app/lib/settings.server.ts b/Foxnouns.Frontend/app/lib/settings.server.ts deleted file mode 100644 index c10096f..0000000 --- a/Foxnouns.Frontend/app/lib/settings.server.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { UserSettings } from "./api/user"; -import { getCookie } from "./request.server"; - -export default function getLocalSettings(req: Request): UserSettings { - const settings = { dark_mode: null } as UserSettings; - const theme = getCookie(req, "pronounscc-theme"); - - switch (theme) { - case "auto": - settings.dark_mode = null; - break; - case "light": - settings.dark_mode = false; - break; - case "dark": - settings.dark_mode = true; - break; - default: - break; - } - - return settings; -} diff --git a/Foxnouns.Frontend/app/lib/utils.ts b/Foxnouns.Frontend/app/lib/utils.ts deleted file mode 100644 index f91176d..0000000 --- a/Foxnouns.Frontend/app/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { DateTime } from "luxon"; - -export const defaultAvatarUrl = "https://pronouns.cc/default/512.webp"; -export const tokenCookieName = "__Host-pronounscc-token"; -export const idTimestamp = (id: string) => - DateTime.fromMillis(parseInt(id, 10) / (1 << 22) + 1_640_995_200_000); diff --git a/Foxnouns.Frontend/app/root.tsx b/Foxnouns.Frontend/app/root.tsx deleted file mode 100644 index 3fc3c67..0000000 --- a/Foxnouns.Frontend/app/root.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { - json, - Links, - Meta as MetaComponent, - Outlet, - Scripts, - ScrollRestoration, - useLoaderData, - useRouteError, - useRouteLoaderData, -} from "@remix-run/react"; -import { LoaderFunctionArgs } from "@remix-run/node"; -import { useChangeLanguage } from "remix-i18next/react"; -import { useTranslation } from "react-i18next"; - -import serverRequest, { getToken, writeCookie } from "./lib/request.server"; -import Meta from "./lib/api/meta"; -import Navbar from "./components/nav/Navbar"; -import { User, UserSettings } from "./lib/api/user"; -import { ApiError, ErrorCode } from "./lib/api/error"; - -import "./app.scss"; -import getLocalSettings from "./lib/settings.server"; -import { LANGUAGE } from "~/env.server"; -import { errorCodeDesc } from "./components/ErrorAlert"; -import { Container } from "react-bootstrap"; -import { ReactNode } from "react"; -import BaseNavbar from "~/components/nav/BaseNavbar"; -import { tokenCookieName } from "~/lib/utils"; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const meta = await serverRequest("GET", "/meta"); - - const token = getToken(request); - let setCookie = ""; - - let meUser: User | undefined; - let settings = getLocalSettings(request); - if (token) { - try { - meUser = await serverRequest("GET", "/users/@me", { token }); - - settings = await serverRequest("GET", "/users/@me/settings", { token }); - } catch (e) { - // If we get an unauthorized error, clear the token, as it's not valid anymore. - if ((e as ApiError).code === ErrorCode.AuthenticationRequired) { - setCookie = writeCookie(tokenCookieName, token, 0); - } - } - } - - return json( - { meta, meUser, settings, locale: LANGUAGE }, - { - headers: { "Set-Cookie": setCookie }, - }, - ); -}; - -export function Layout({ children }: { children: ReactNode }) { - const { locale } = useRouteLoaderData("root") || { - meta: { - users: { - total: 0, - active_month: 0, - active_week: 0, - active_day: 0, - }, - members: 0, - version: "", - hash: "", - }, - }; - const { i18n } = useTranslation(); - i18n.language = locale || "en"; - useChangeLanguage(locale || "en"); - - return ( - - - - - - - - - - {children} - - - - - ); -} - -export function ErrorBoundary() { - const data = useRouteLoaderData("root"); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const error: any = useRouteError(); - const { t } = useTranslation(); - - const errorElem = - "code" in error && "message" in error ? ( - - ) : ( - <>{t("error.errors.generic-error")} - ); - - return ( - - - {t("error.title")} - - - - - {data?.meUser && data?.meta ? ( - - ) : ( - - )} - {errorElem} - - - - ); -} - -function ApiErrorElem({ error }: { error: ApiError }) { - const { t } = useTranslation(); - const errorDesc = errorCodeDesc(t, error.code); - - return ( - <> -

    {t("error.heading")}

    -

    {errorDesc}

    -
    - {t("error.more-info")} -
    -					{JSON.stringify(error, null, "  ")}
    -				
    -
    - - ); -} - -export default function App() { - const { meta, meUser } = useLoaderData(); - - return ( - <> - - - - - - ); -} diff --git a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx b/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx deleted file mode 100644 index b112a08..0000000 --- a/Foxnouns.Frontend/app/routes/$username/MemberCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { - defaultPreferences, - mergePreferences, - PartialMember, - PartialUser, - Pronoun, -} from "~/lib/api/user"; -import { Link } from "@remix-run/react"; -import { defaultAvatarUrl } from "~/lib/utils"; -import { useTranslation } from "react-i18next"; -import { OverlayTrigger, Tooltip } from "react-bootstrap"; -import { Lock } from "react-bootstrap-icons"; -import AvatarImage from "~/components/profile/AvatarImage"; - -export default function MemberCard({ user, member }: { user: PartialUser; member: PartialMember }) { - const { t } = useTranslation(); - - const mergedPrefs = mergePreferences(user.custom_preferences); - const pronouns: Pronoun[] = []; - for (const pronoun of member.pronouns) { - const pref = - pronoun.status in mergedPrefs ? mergedPrefs[pronoun.status] : defaultPreferences.missing; - if (pref.favourite) pronouns.push(pronoun); - } - - const displayedPronouns = pronouns - .map((pronoun) => { - if (pronoun.display_text) { - return pronoun.display_text; - } else { - const split = pronoun.value.split("/"); - if (split.length === 5) return split.splice(0, 2).join("/"); - return pronoun.value; - } - }) - .join(", "); - - return ( -
    - - - -

    - - {member.display_name ?? member.name} - {member.unlisted === true && ( - <> - - {t("user.member-hidden")} - - } - > - - - - - - )} - - {displayedPronouns && <>{displayedPronouns}} -

    -
    - ); -} diff --git a/Foxnouns.Frontend/app/routes/$username/route.tsx b/Foxnouns.Frontend/app/routes/$username/route.tsx deleted file mode 100644 index c558ac5..0000000 --- a/Foxnouns.Frontend/app/routes/$username/route.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link, redirect, useLoaderData, useRouteLoaderData } from "@remix-run/react"; -import { UserWithMembers } from "~/lib/api/user"; -import serverRequest from "~/lib/request.server"; -import { loader as rootLoader } from "~/root"; -import { Alert, Button, Pagination } from "react-bootstrap"; -import { Trans, useTranslation } from "react-i18next"; -import { PersonPlusFill } from "react-bootstrap-icons"; -import MemberCard from "~/routes/$username/MemberCard"; -import { ReactNode } from "react"; -import BaseProfile from "~/components/profile/BaseProfile"; - -export const meta: MetaFunction = ({ data }) => { - const { user } = data!; - - return [{ title: `@${user.username} • pronouns.cc` }]; -}; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const url = new URL(request.url); - let memberPage = parseInt(url.searchParams.get("page") ?? "0", 10); - - let username = params.username!; - if (!username.startsWith("@")) throw redirect(`/@${username}`); - username = username.substring("@".length); - - const user = await serverRequest("GET", `/users/${username}`); - const pageCount = Math.ceil(user.members.length / 20); - let members = user.members.slice(memberPage * 20, (memberPage + 1) * 20); - if (members.length === 0) { - members = user.members.slice(0, 20); - memberPage = 0; - } - - return json({ user, members, currentPage: memberPage, pageCount }); -}; - -export default function UserPage() { - const { t } = useTranslation(); - const { user, members, currentPage, pageCount } = useLoaderData(); - const { meUser } = useRouteLoaderData("root") || { meUser: undefined }; - - const isMeUser = meUser && meUser.id === user.id; - - const paginationItems: ReactNode[] = []; - for (let i = 0; i < pageCount; i++) { - paginationItems.push( - - {i + 1} - , - ); - } - - const pagination = ( - - - {paginationItems} - - - ); - - return ( - <> - {isMeUser && ( - - - You are currently viewing your public profile. -
    - Edit your profile -
    -
    - )} - - {(members.length > 0 || isMeUser) && ( - <> -
    -

    - {user.member_title || t("user.heading.members")}{" "} - {isMeUser && ( - // @ts-expect-error using as=Link causes an error here, even though it runs completely fine - - )} -

    - {pageCount > 1 && pagination} -
    - {members.length === 0 ? ( -
    - - You don't have any members yet. -
    - Members are sub-profiles that can have their own avatar, names, pronouns, and - preferred terms. -
    - You can create a new member with the "Create member" button above.{" "} - (only you can see this) -
    -
    - ) : ( -
    - {members.map((member, i) => ( - - ))} -
    - )} -
    - {pageCount > 1 && pagination} - - )} - - ); -} diff --git a/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx b/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx deleted file mode 100644 index da72aed..0000000 --- a/Foxnouns.Frontend/app/routes/$username_.$member/route.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link, redirect, useLoaderData, useRouteLoaderData } from "@remix-run/react"; -import serverRequest from "~/lib/request.server"; -import { Member } from "~/lib/api/member"; -import BaseProfile from "~/components/profile/BaseProfile"; -import { loader as rootLoader } from "~/root"; -import { Alert, Button } from "react-bootstrap"; -import { Trans, useTranslation } from "react-i18next"; -import { ArrowLeft } from "react-bootstrap-icons"; - -export const meta: MetaFunction = ({ data }) => { - const { member } = data!; - - return [ - { title: `${member.display_name ?? member.name} • @${member.user.username} • pronouns.cc` }, - ]; -}; - -export const loader = async ({ params }: LoaderFunctionArgs) => { - let username = params.username!; - const memberName = params.member!; - if (!username.startsWith("@")) throw redirect(`/@${username}/${memberName}`); - username = username.substring("@".length); - - const member = await serverRequest("GET", `/users/${username}/members/${memberName}`); - return json({ member }); -}; - -export default function MemberPage() { - const { t } = useTranslation(); - const { member } = useLoaderData(); - const { meUser } = useRouteLoaderData("root") || { meUser: undefined }; - const isMeUser = meUser && meUser.id === member.user.id; - - const memberName = member.name; - - return ( - <> - {isMeUser && ( - - - You are currently viewing the public profile of {{ memberName }}. -
    - Edit profile -
    -
    - )} -
    - {/* @ts-expect-error using as=Link causes an error here, even though it runs completely fine */} - -
    - - - ); -} diff --git a/Foxnouns.Frontend/app/routes/_index.tsx b/Foxnouns.Frontend/app/routes/_index.tsx deleted file mode 100644 index 2a906d2..0000000 --- a/Foxnouns.Frontend/app/routes/_index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { MetaFunction } from "@remix-run/node"; - -export const meta: MetaFunction = () => { - return [{ title: "pronouns.cc" }]; -}; - -export default function Index() { - return ( -
    -

    pronouns.cc

    -
    - ); -} diff --git a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx deleted file mode 100644 index c34ac58..0000000 --- a/Foxnouns.Frontend/app/routes/auth.callback.discord/route.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { - ActionFunctionArgs, - json, - redirect, - LoaderFunctionArgs, - MetaFunction, -} from "@remix-run/node"; -import { type ApiError, ErrorCode } from "~/lib/api/error"; -import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; -import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; -import { - Form as RemixForm, - Link, - useActionData, - useLoaderData, - ShouldRevalidateFunction, - useNavigate, -} from "@remix-run/react"; -import { Trans, useTranslation } from "react-i18next"; -import { Form, Button } from "react-bootstrap"; -import i18n from "~/i18next.server"; -import { tokenCookieName } from "~/lib/utils"; -import { useEffect } from "react"; -import RegisterError from "~/components/RegisterError"; -import { AuthMethod } from "~/lib/api/user"; -import { errorCodeDesc } from "~/components/ErrorAlert"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; -}; - -export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { - return !actionResult; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const url = new URL(request.url); - - const code = url.searchParams.get("code"); - const state = url.searchParams.get("state"); - - const token = getToken(request); - - if (!code || !state) - throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code or state" } as ApiError; - - if (token) { - try { - const resp = await serverRequest("POST", "/auth/discord/add-account/callback", { - body: { code, state }, - token, - isInternal: true, - }); - - return json({ - isLinkRequest: true, - meta: { title: t("log-in.callback.title.discord-link") }, - error: null, - hasAccount: false, - user: null, - ticket: null, - remoteUser: null, - newAuthMethod: resp, - }); - } catch (e) { - return json({ - isLinkRequest: true, - meta: { title: t("log-in.callback.title.discord-link") }, - error: e as ApiError, - hasAccount: false, - user: null, - ticket: null, - remoteUser: null, - newAuthMethod: null, - }); - } - } - - const resp = await serverRequest("POST", "/auth/discord/callback", { - body: { code, state }, - isInternal: true, - }); - - if (resp.has_account) { - return json( - { - isLinkRequest: false, - meta: { title: t("log-in.callback.title.discord-success") }, - error: null, - hasAccount: true, - user: resp.user!, - ticket: null, - remoteUser: null, - newAuthMethod: null, - }, - { - headers: { - "Set-Cookie": writeCookie(tokenCookieName, resp.token!), - }, - }, - ); - } - - return json({ - isLinkRequest: false, - meta: { title: t("log-in.callback.title.discord-register") }, - error: null, - hasAccount: false, - user: null, - ticket: resp.ticket!, - remoteUser: resp.remote_username!, - newAuthMethod: null, - }); -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - 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 json({ - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid username or ticket", - } as ApiError, - user: null, - }); - - try { - const resp = await serverRequest("POST", "/auth/discord/register", { - body: { username, ticket }, - isInternal: true, - }); - - return redirect("/auth/welcome", { - headers: { - "Set-Cookie": writeCookie(tokenCookieName, resp.token), - }, - status: 303, - }); - } catch (e) { - JSON.stringify(e); - - return json({ error: e as ApiError }); - } -}; - -export default function DiscordCallbackPage() { - const { t } = useTranslation(); - const data = useLoaderData(); - const actionData = useActionData(); - const navigate = useNavigate(); - - useEffect(() => { - setTimeout(() => { - if (data.hasAccount) { - navigate(`/@${data.user!.username}`); - } - }, 2000); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (data.isLinkRequest) { - if (data.error) { - return ( - <> -

    {t("log-in.callback.link-error")}

    -

    {errorCodeDesc(t, data.error.code)}

    - - ); - } - - const authMethod = data.newAuthMethod!; - - return ( - <> -

    {t("log-in.callback.discord-link-success")}

    -

    - {t("log-in.callback.discord-link-success-hint", { - username: authMethod.remote_username ?? authMethod.remote_id, - })} -

    - - ); - } - - if (data.hasAccount) { - const username = data.user!.username; - - return ( - <> -

    {t("log-in.callback.success")}

    -

    - - {/* @ts-expect-error react-i18next handles interpolation here */} - Welcome back, @{{ username }}! - -
    - {t("log-in.callback.redirect-hint")} -

    - - ); - } - - return ( - -
    - {actionData?.error && } - - {t("log-in.callback.remote-username.discord")} - - - - {t("log-in.callback.username")} - - - - - -
    - ); -} diff --git a/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx b/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx deleted file mode 100644 index 8444bb5..0000000 --- a/Foxnouns.Frontend/app/routes/auth.callback.mastodon.$instance/route.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { - ActionFunctionArgs, - json, - LoaderFunctionArgs, - MetaFunction, - redirect, -} from "@remix-run/node"; -import i18n from "~/i18next.server"; -import { type ApiError, ErrorCode } from "~/lib/api/error"; -import serverRequest, { writeCookie } from "~/lib/request.server"; -import { AuthResponse, CallbackResponse } from "~/lib/api/auth"; -import { tokenCookieName } from "~/lib/utils"; -import { - Link, - ShouldRevalidateFunction, - useActionData, - useLoaderData, - useNavigate, -} from "@remix-run/react"; -import { Trans, useTranslation } from "react-i18next"; -import { useEffect } from "react"; -import { Form as RemixForm } from "@remix-run/react/dist/components"; -import { Button, Form } from "react-bootstrap"; -import RegisterError from "~/components/RegisterError"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; -}; - -export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { - return !actionResult; -}; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const url = new URL(request.url); - - const code = url.searchParams.get("code"); - if (!code) throw { status: 400, code: ErrorCode.BadRequest, message: "Missing code" } as ApiError; - - const resp = await serverRequest("POST", "/auth/fediverse/callback", { - body: { code, instance: params.instance! }, - isInternal: true, - }); - - if (resp.has_account) { - return json( - { - meta: { title: t("log-in.callback.title.fediverse-success") }, - hasAccount: true, - user: resp.user!, - ticket: null, - remoteUser: null, - }, - { - headers: { - "Set-Cookie": writeCookie(tokenCookieName, resp.token!), - }, - }, - ); - } - - return json({ - meta: { title: t("log-in.callback.title.fediverse-register") }, - hasAccount: false, - user: null, - instance: params.instance!, - ticket: resp.ticket!, - remoteUser: resp.remote_username!, - }); -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - 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 json({ - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid username or ticket", - } as ApiError, - user: null, - }); - - try { - const resp = await serverRequest("POST", "/auth/fediverse/register", { - body: { username, ticket }, - isInternal: true, - }); - - return redirect("/auth/welcome", { - headers: { - "Set-Cookie": writeCookie(tokenCookieName, resp.token), - }, - status: 303, - }); - } catch (e) { - JSON.stringify(e); - - return json({ error: e as ApiError }); - } -}; - -export default function FediverseCallbackPage() { - const { t } = useTranslation(); - const data = useLoaderData(); - const actionData = useActionData(); - const navigate = useNavigate(); - - useEffect(() => { - setTimeout(() => { - if (data.hasAccount) { - navigate(`/@${data.user!.username}`); - } - }, 2000); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (data.hasAccount) { - const username = data.user!.username; - - return ( - <> -

    {t("log-in.callback.success")}

    -

    - - {/* @ts-expect-error react-i18next handles interpolation here */} - Welcome back, @{{ username }}! - -
    - {t("log-in.callback.redirect-hint")} -

    - - ); - } - - return ( - -
    - {actionData?.error && } - - {t("log-in.callback.remote-username.fediverse")} - - - - {t("log-in.callback.username")} - - - - - -
    - ); -} diff --git a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx deleted file mode 100644 index dc3af98..0000000 --- a/Foxnouns.Frontend/app/routes/auth.log-in/route.tsx +++ /dev/null @@ -1,146 +0,0 @@ -import { - MetaFunction, - json, - LoaderFunctionArgs, - redirect, - ActionFunctionArgs, -} from "@remix-run/node"; -import { - Form as RemixForm, - ShouldRevalidateFunction, - useActionData, - useLoaderData, -} from "@remix-run/react"; -import { Form, Button, ButtonGroup, ListGroup } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; -import i18n from "~/i18next.server"; -import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; -import { AuthResponse, AuthUrls } from "~/lib/api/auth"; -import { ApiError, ErrorCode } from "~/lib/api/error"; -import ErrorAlert from "~/components/ErrorAlert"; -import { User } from "~/lib/api/user"; -import { tokenCookieName } from "~/lib/utils"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Log in"} • pronouns.cc` }]; -}; - -export const shouldRevalidate: ShouldRevalidateFunction = ({ actionResult }) => { - return !actionResult; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const token = getToken(request); - if (token) { - try { - await serverRequest("GET", "/users/@me", { token }); - return redirect("/?err=already-logged-in", 303); - } catch (e) { - // ignore - } - } - - const urls = await serverRequest("POST", "/auth/urls", { isInternal: true }); - - return json({ - meta: { title: t("log-in.title") }, - urls, - }); -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - 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 serverRequest("POST", "/auth/email/login", { - body: { email, password }, - isInternal: true, - }); - - return redirect("/", { - status: 303, - headers: { - "Set-Cookie": writeCookie(tokenCookieName, resp.token), - }, - }); - } catch (e) { - return json({ error: e as ApiError }); - } -}; - -export default function LoginPage() { - const { t } = useTranslation(); - const { urls } = useLoaderData(); - const actionData = useActionData(); - - return ( - <> -
    - {!urls.email_enabled &&
    } - {urls.email_enabled && ( -
    -

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

    - {actionData?.error && } - -
    - - {t("log-in.email")} - - - - {t("log-in.password")} - - - - - - - - -
    -
    - )} -
    -

    {t("log-in.3rd-party.title")}

    -

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

    - - {urls.discord && ( - - {t("log-in.3rd-party.discord")} - - )} - {urls.google && ( - - {t("log-in.3rd-party.google")} - - )} - {urls.tumblr && ( - - {t("log-in.3rd-party.tumblr")} - - )} - - {t("log-in.3rd-party.fediverse")} - - -
    - {!urls.email_enabled &&
    } -
    - - ); -} - -function LoginError({ error }: { error: ApiError }) { - const { t } = useTranslation(); - - if (error.code !== ErrorCode.UserNotFound) return ; - - return <>{t("log-in.invalid-credentials")}; -} diff --git a/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx deleted file mode 100644 index a7304f5..0000000 --- a/Foxnouns.Frontend/app/routes/auth.log-in_.fediverse/route.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import { - LoaderFunctionArgs, - json, - MetaFunction, - ActionFunctionArgs, - redirect, -} from "@remix-run/node"; -import i18n from "~/i18next.server"; -import { useTranslation } from "react-i18next"; -import { Form as RemixForm, useActionData } from "@remix-run/react"; -import { Button, Form } from "react-bootstrap"; -import serverRequest from "~/lib/request.server"; -import { ApiError, ErrorCode } from "~/lib/api/error"; -import ErrorAlert from "~/components/ErrorAlert"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Log in with a Fediverse account"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - - return json({ meta: { title: t("log-in.fediverse.choose-title") } }); -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - const body = await request.formData(); - const instance = body.get("instance") as string | null; - if (!instance) - return json({ - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid instance name", - } as ApiError, - }); - - try { - const resp = await serverRequest<{ url: string }>( - "GET", - `/auth/fediverse?instance=${encodeURIComponent(instance)}`, - { - isInternal: true, - }, - ); - - return redirect(resp.url); - } catch (e) { - return json({ error: e as ApiError }); - } -}; - -export default function AuthFediversePage() { - const { t } = useTranslation(); - - const data = useActionData(); - - return ( - <> -

    {t("log-in.fediverse.choose-form-title")}

    - {data?.error && } - -
    - - {t("log-in.fediverse-instance-label")} - - - - -
    - - ); -} diff --git a/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx b/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx deleted file mode 100644 index 8b89d8d..0000000 --- a/Foxnouns.Frontend/app/routes/auth.log-out/route.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ActionFunction } from "@remix-run/node"; -import { writeCookie } from "~/lib/request.server"; -import { tokenCookieName } from "~/lib/utils"; - -export const action: ActionFunction = async () => { - return new Response(null, { - headers: { - "Set-Cookie": writeCookie(tokenCookieName, "token", 0), - }, - status: 204, - }); -}; diff --git a/Foxnouns.Frontend/app/routes/auth.welcome/route.tsx b/Foxnouns.Frontend/app/routes/auth.welcome/route.tsx deleted file mode 100644 index 3925fed..0000000 --- a/Foxnouns.Frontend/app/routes/auth.welcome/route.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { LoaderFunctionArgs, redirect, json, MetaFunction } from "@remix-run/node"; -import i18n from "~/i18next.server"; -import serverRequest, { getToken } from "~/lib/request.server"; -import { User } from "~/lib/api/user"; -import { useTranslation } from "react-i18next"; -import { Link, useLoaderData } from "@remix-run/react"; -import { Button } from "react-bootstrap"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Welcome"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const token = getToken(request); - let user: User; - - if (token) { - try { - user = await serverRequest("GET", "/users/@me", { token }); - } catch (e) { - return redirect("/auth/log-in"); - } - } else { - return redirect("/auth/log-in"); - } - - return json({ meta: { title: t("welcome.title") }, user }); -}; - -export default function WelcomePage() { - const { t } = useTranslation(); - const { user } = useLoaderData(); - - return ( -
    -

    {t("welcome.header")}

    -

    {t("welcome.blurb")}

    -

    {t("welcome.customize-profile")}

    -

    {t("welcome.customize-profile-blurb")}

    -

    {t("welcome.create-members")}

    -

    {t("welcome.create-members-blurb")}

    -

    {t("welcome.custom-preferences")}

    -

    {t("welcome.custom-preferences-blurb")}

    - - - -
    - ); -} diff --git a/Foxnouns.Frontend/app/routes/dark-mode/route.tsx b/Foxnouns.Frontend/app/routes/dark-mode/route.tsx deleted file mode 100644 index c3c2b24..0000000 --- a/Foxnouns.Frontend/app/routes/dark-mode/route.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { ActionFunction } from "@remix-run/node"; -import { UserSettings } from "~/lib/api/user"; -import serverRequest, { getToken, writeCookie } from "~/lib/request.server"; - -// Handles theme switching -// Remix itself handles redirecting back to the original page after the setting is set -// -// Note: this function is currently unused. Bootstrap only lets us switch themes with either prefers-color-scheme -// *or* a programmatic switch using data-bs-theme, not both. -// If the Sec-CH-Prefers-Color-Scheme header (https://caniuse.com/mdn-http_headers_sec-ch-prefers-color-scheme) -// is added to Firefox and Safari, the dark mode setting should be reworked to use it instead. -// As it stands, using prefers-color-scheme is the only way -// to respect the operating system's dark mode setting without using JavaScript. -export const action: ActionFunction = async ({ request }) => { - const body = await request.formData(); - const theme = (body.get("theme") as string | null) || "auto"; - - const token = getToken(request); - if (token) { - await serverRequest("PATCH", "/users/@me/settings", { - token, - body: { - dark_mode: theme === "auto" ? null : theme === "dark", - }, - }); - - return new Response(null, { - status: 204, - }); - } - - return new Response(null, { - headers: { - "Set-Cookie": writeCookie("pronounscc-theme", theme), - }, - status: 204, - }); -}; diff --git a/Foxnouns.Frontend/app/routes/settings._index/route.tsx b/Foxnouns.Frontend/app/routes/settings._index/route.tsx deleted file mode 100644 index 2829098..0000000 --- a/Foxnouns.Frontend/app/routes/settings._index/route.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { Button, Form, InputGroup, Table } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; -import { - Form as RemixForm, - Link, - Outlet, - useActionData, - useRouteLoaderData, -} from "@remix-run/react"; -import { loader as settingsLoader } from "../settings/route"; -import { loader as rootLoader } from "../../root"; -import { DateTime } from "luxon"; -import { defaultAvatarUrl, idTimestamp } from "~/lib/utils"; -import { ExclamationTriangleFill, InfoCircleFill } from "react-bootstrap-icons"; -import AvatarImage from "~/components/profile/AvatarImage"; -import { ActionFunctionArgs, json } from "@remix-run/node"; -import { type ApiError, ErrorCode, firstErrorFor } from "~/lib/api/error"; -import serverRequest, { getToken } from "~/lib/request.server"; -import { MeUser } from "~/lib/api/user"; -import ErrorAlert from "~/components/ErrorAlert"; - -export const action = async ({ request }: ActionFunctionArgs) => { - const data = await request.formData(); - const username = data.get("username") as string | null; - const token = getToken(request); - - if (!username) { - return json({ - error: { - status: 403, - code: ErrorCode.BadRequest, - message: "Invalid username", - } as ApiError, - user: null, - }); - } - - try { - const resp = await serverRequest("PATCH", "/users/@me", { body: { username }, token }); - - return json({ user: resp, error: null }); - } catch (e) { - return json({ error: e as ApiError, user: null }); - } -}; - -export default function SettingsIndex() { - const { user } = useRouteLoaderData("routes/settings")!; - const actionData = useActionData(); - const { meta } = useRouteLoaderData("root")!; - const { t } = useTranslation(); - - const createdAt = idTimestamp(user.id); - - return ( - <> - -
    -
    - -
    - - {t("settings.general.username")} - - - - - - -
    - -

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

    - {actionData?.error && } -
    -
    - -
    -
    -
    -

    {t("settings.general.log-out-everywhere")}

    -

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

    - {/* @ts-expect-error as=Link */} - -
    -

    {t("settings.general.table-header")}

    -
    - - - - - - - - - - - - - - - - - - - - - - - - - - -
    {t("settings.general.id")} - {user.id} -
    {t("settings.general.created")}{createdAt.toLocaleString(DateTime.DATETIME_MED)}
    {t("settings.general.member-count")} - {user.members.length}/{meta.limits.member_count} -
    {t("settings.general.member-list-hidden")}{user.member_list_hidden ? t("yes") : t("no")}
    {t("settings.general.custom-preferences")} - {Object.keys(user.custom_preferences).length}/{meta.limits.custom_preferences} -
    {t("settings.general.role")} - {user.role} -
    - - ); -} - -function UsernameUpdateError({ error }: { error: ApiError }) { - const { t } = useTranslation(); - const usernameError = firstErrorFor(error, "username"); - if (!usernameError) { - return ; - } - - return ( -

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

    - ); -} diff --git a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth/route.tsx deleted file mode 100644 index 4954027..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth/route.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import i18n from "~/i18next.server"; -import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Link, useLoaderData, useRouteLoaderData } from "@remix-run/react"; -import { Button, ListGroup } from "react-bootstrap"; -import { loader as settingsLoader } from "~/routes/settings/route"; -import { useTranslation } from "react-i18next"; -import { AuthMethod, MeUser } from "~/lib/api/user"; -import serverRequest from "~/lib/request.server"; -import { AuthUrls } from "~/lib/api/auth"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const urls = await serverRequest("POST", "/auth/urls", { isInternal: true }); - - return { urls, meta: { title: t("settings.auth.title") } }; -}; - -export default function AuthSettings() { - const { urls } = useLoaderData(); - const { user } = useRouteLoaderData("routes/settings")!; - - return ( -
    - {urls.email_enabled && } - {urls.discord && } - -
    - ); -} - -function EmailSettings({ user }: { user: MeUser }) { - const { t } = useTranslation(); - const oneAuthMethod = user.auth_methods.length === 1; - const emails = user.auth_methods.filter((m) => m.type === "EMAIL"); - - return ( - <> -

    {t("settings.auth.email-addresses")}

    - {emails.length > 0 && ( - <> - - {emails.map((e) => ( - - ))} - - - )} - {emails.length < 3 && ( -

    - {/* @ts-expect-error as=Link */} - -

    - )} - - ); -} - -function EmailRow({ email, disabled }: { email: AuthMethod; disabled: boolean }) { - const { t } = useTranslation(); - - return ( - -
    -
    {email.remote_id}
    - {!disabled && ( -
    - - {t("settings.auth.remove-auth-method")} - -
    - )} -
    -
    - ); -} - -function DiscordSettings({ user }: { user: MeUser }) { - const { t } = useTranslation(); - const oneAuthMethod = user.auth_methods.length === 1; - const discordAccounts = user.auth_methods.filter((m) => m.type === "DISCORD"); - - return ( - <> -

    {t("settings.auth.discord-accounts")}

    - {discordAccounts.length > 0 && ( - <> - - {discordAccounts.map((a) => ( - - ))} - - - )} - {discordAccounts.length < 3 && ( -

    - {/* @ts-expect-error as=Link */} - -

    - )} - - ); -} - -function FediverseSettings({ user }: { user: MeUser }) { - const { t } = useTranslation(); - const oneAuthMethod = user.auth_methods.length === 1; - const fediAccounts = user.auth_methods.filter((m) => m.type === "FEDIVERSE"); - - return ( - <> -

    {t("settings.auth.fediverse-accounts")}

    - {fediAccounts.length > 0 && ( - <> - - {fediAccounts.map((a) => ( - - ))} - - - )} - {fediAccounts.length < 3 && ( -

    - {/* @ts-expect-error as=Link */} - -

    - )} - - ); -} - -function NonEmailRow({ account, disabled }: { account: AuthMethod; disabled: boolean }) { - const { t } = useTranslation(); - - return ( - -
    -
    - {account.remote_username} {account.type !== "FEDIVERSE" && <>({account.remote_id})} -
    - {!disabled && ( -
    - - {t("settings.auth.remove-auth-method")} - -
    - )} -
    -
    - ); -} diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx deleted file mode 100644 index 045fe85..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth_.add-discord-account/route.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { LoaderFunctionArgs, redirect, json } from "@remix-run/node"; -import serverRequest, { getToken } from "~/lib/request.server"; -import { ApiError } from "~/lib/api/error"; -import { useLoaderData } from "@remix-run/react"; -import ErrorAlert from "~/components/ErrorAlert"; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const token = getToken(request); - - try { - const { url } = await serverRequest<{ url: string }>("GET", "/auth/discord/add-account", { - isInternal: true, - token, - }); - - return redirect(url, 303); - } catch (e) { - return json({ error: e as ApiError }); - } -}; - -export default function AddDiscordAccountPage() { - const { error } = useLoaderData(); - - return ; -} diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.add-email/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.add-email/route.tsx deleted file mode 100644 index 40eed7f..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth_.add-email/route.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import i18n from "~/i18next.server"; -import { json, useActionData, useNavigate, useRouteLoaderData } from "@remix-run/react"; -import { loader as settingsLoader } from "~/routes/settings/route"; -import { useTranslation } from "react-i18next"; -import { useEffect } from "react"; -import { Button, Card, Form } from "react-bootstrap"; -import { Form as RemixForm } from "@remix-run/react/dist/components"; -import { ApiError, ErrorCode } from "~/lib/api/error"; -import { fastRequest, getToken } from "~/lib/request.server"; -import ErrorAlert from "~/components/ErrorAlert"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Authentication"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - return { meta: { title: t("settings.auth.title") } }; -}; - -export const action = async ({ request }: ActionFunctionArgs) => { - const token = getToken(request)!; - const body = await request.formData(); - const email = body.get("email") as string | null; - const password = body.get("password-1") as string | null; - const password2 = body.get("password-2") as string | null; - - if (!email || !password || !password2) { - return json({ - error: { - status: 400, - code: ErrorCode.BadRequest, - message: "One or more required fields are missing.", - } as ApiError, - ok: false, - }); - } - - if (password !== password2) { - return json({ - error: { - status: 400, - code: ErrorCode.BadRequest, - message: "Passwords do not match.", - } as ApiError, - ok: false, - }); - } - - await fastRequest("POST", "/auth/email/add", { - body: { email, password }, - token, - isInternal: true, - }); - - return json({ error: null, ok: true }); -}; - -export default function AddEmailPage() { - const { t } = useTranslation(); - const { user } = useRouteLoaderData("routes/settings")!; - const actionData = useActionData(); - const navigate = useNavigate(); - const emails = user.auth_methods.filter((m) => m.type === "EMAIL"); - - useEffect(() => { - if (emails.length >= 3) { - navigate("/settings/auth"); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - - - - {emails.length === 0 - ? t("settings.auth.form.add-first-email") - : t("settings.auth.form.add-extra-email")} - - {emails.length === 0 && !actionData?.ok &&

    {t("settings.auth.no-email")}

    } - {actionData?.ok &&

    {t("settings.auth.new-email-pending")}

    } - {actionData?.error && } -
    - - {t("settings.auth.form.email-address")} - - - - {t("settings.auth.form.password-1")} - - - - {t("settings.auth.form.password-2")} - - - -
    -
    -
    - ); -} diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.confirm-email.$code/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.confirm-email.$code/route.tsx deleted file mode 100644 index d7fa86e..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth_.confirm-email.$code/route.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { LoaderFunctionArgs, json } from "@remix-run/node"; -import { baseRequest } from "~/lib/request.server"; -import { useTranslation } from "react-i18next"; -import { useEffect } from "react"; -import { useNavigate } from "@remix-run/react"; - -export const loader = async ({ params }: LoaderFunctionArgs) => { - const state = params.code!; - - const resp = await baseRequest("POST", "/auth/email/callback", { - body: { state }, - isInternal: true, - }); - if (resp.status !== 204) { - // TODO: handle non-204 status (this indicates that the email was not linked to an account) - } - - return json({ ok: true }); -}; - -export default function ConfirmEmailPage() { - const { t } = useTranslation(); - const navigate = useNavigate(); - - useEffect(() => { - setTimeout(() => { - navigate("/settings/auth"); - }, 2000); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - <> -

    {t("settings.auth.email-link-success")}

    -

    {t("settings.auth.redirect-to-auth-hint")}

    - - ); -} diff --git a/Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx b/Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx deleted file mode 100644 index 922f28d..0000000 --- a/Foxnouns.Frontend/app/routes/settings.auth_.remove-method.$id/route.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { ActionFunctionArgs, json, LoaderFunctionArgs, redirect } from "@remix-run/node"; -import i18n from "~/i18next.server"; -import serverRequest, { fastRequest, getToken } from "~/lib/request.server"; -import { AuthMethod } from "~/lib/api/user"; -import { useTranslation } from "react-i18next"; -import { useLoaderData, Form } from "@remix-run/react"; -import { Button } from "react-bootstrap"; - -export const action = async ({ request }: ActionFunctionArgs) => { - const data = await request.formData(); - const token = getToken(request); - - const id = data.get("remove-id") as string; - - await fastRequest("DELETE", `/auth/methods/${id}`, { token, isInternal: true }); - - return redirect("/settings/auth", 303); -}; - -export const loader = async ({ request, params }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const token = getToken(request); - - const method = await serverRequest("GET", `/auth/methods/${params.id}`, { - token, - isInternal: true, - }); - return json({ method, meta: { title: t("settings.auth.remove-auth-method-title") } }); -}; - -export default function RemoveAuthMethodPage() { - const { t } = useTranslation(); - const { method } = useLoaderData(); - - let methodName; - switch (method.type) { - case "EMAIL": - methodName = "email"; - break; - case "DISCORD": - methodName = "Discord"; - break; - case "FEDIVERSE": - methodName = "Fediverse"; - break; - case "GOOGLE": - methodName = "Google"; - break; - case "TUMBLR": - methodName = "Tumblr"; - break; - } - - return ( - <> -

    {t("settings.auth.remove-auth-method-title")}

    -

    - {t("settings.auth.remove-auth-method-hint", { - username: method.remote_username || method.remote_id, - methodName, - })} -

    -

    -

    - - -
    -

    - - ); -} diff --git a/Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx b/Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx deleted file mode 100644 index 300af39..0000000 --- a/Foxnouns.Frontend/app/routes/settings.force-log-out/route.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ActionFunction, redirect } from "@remix-run/node"; -import { fastRequest, getToken, writeCookie } from "~/lib/request.server"; -import { tokenCookieName } from "~/lib/utils"; -import { Button, Form } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; -import { Form as RemixForm, Link } from "@remix-run/react"; - -export const action: ActionFunction = async ({ request }) => { - const token = getToken(request); - if (!token) - return redirect("/", { - status: 303, - headers: { "Set-Cookie": writeCookie(tokenCookieName, "token", 0) }, - }); - - await fastRequest("POST", "/auth/force-log-out", { token, isInternal: true }); - - return redirect("/", { - status: 303, - headers: { "Set-Cookie": writeCookie(tokenCookieName, "token", 0) }, - }); -}; - -export const loader = () => { - return null; -}; - -export default function ForceLogoutPage() { - const { t } = useTranslation(); - - return ( - <> -

    {t("settings.general.log-out-everywhere")}

    -

    {t("settings.general.log-out-everywhere-confirm")}

    - -
    - - {/* @ts-expect-error as=Link */} - -
    -
    - - ); -} diff --git a/Foxnouns.Frontend/app/routes/settings/route.tsx b/Foxnouns.Frontend/app/routes/settings/route.tsx deleted file mode 100644 index 9d8247a..0000000 --- a/Foxnouns.Frontend/app/routes/settings/route.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { LoaderFunctionArgs, json, redirect, MetaFunction } from "@remix-run/node"; -import i18n from "~/i18next.server"; -import serverRequest, { getToken } from "~/lib/request.server"; -import { MeUser } from "~/lib/api/user"; -import { Link, Outlet, useLocation } from "@remix-run/react"; -import { Nav } from "react-bootstrap"; -import { useTranslation } from "react-i18next"; - -export const meta: MetaFunction = ({ data }) => { - return [{ title: `${data?.meta.title || "Settings"} • pronouns.cc` }]; -}; - -export const loader = async ({ request }: LoaderFunctionArgs) => { - const t = await i18n.getFixedT(request); - const token = getToken(request); - - if (token) { - try { - const user = await serverRequest("GET", "/users/@me", { token }); - return json({ user, meta: { title: t("settings.title") } }); - } catch (e) { - return redirect("/auth/log-in"); - } - } - - return redirect("/auth/log-in"); -}; - -export default function SettingsLayout() { - const { t } = useTranslation(); - const { pathname } = useLocation(); - - const isActive = (matches: string[] | string, startsWith: boolean = false) => - startsWith - ? typeof matches === "string" - ? pathname.startsWith(matches) - : matches.some((m) => pathname.startsWith(m)) - : typeof matches === "string" - ? matches === pathname - : matches.includes(pathname); - - return ( - <> - -
    - -
    - - ); -} diff --git a/Foxnouns.Frontend/eslint.config.js b/Foxnouns.Frontend/eslint.config.js new file mode 100644 index 0000000..d676276 --- /dev/null +++ b/Foxnouns.Frontend/eslint.config.js @@ -0,0 +1,33 @@ +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/"], + }, +); diff --git a/Foxnouns.Frontend/i18next-parser.config.js b/Foxnouns.Frontend/i18next-parser.config.js deleted file mode 100644 index a1e6625..0000000 --- a/Foxnouns.Frontend/i18next-parser.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - locales: ["en"], -}; diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 6db78c8..1d5739a 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -1,74 +1,45 @@ { - "name": "foxnouns-fe", - "private": true, - "sideEffects": false, + "name": "foxnouns.frontend", + "version": "0.0.1", "type": "module", "scripts": { - "build": "remix vite:build", - "dev": "node ./server.js", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", - "start": "cross-env NODE_ENV=production node ./server.js", - "typecheck": "tsc", - "format": "prettier -w .", - "extract-translations": "i18next 'app/**/*.tsx' -o 'public/locales/$LOCALE.json'" - }, - "dependencies": { - "@remix-run/express": "^2.11.2", - "@remix-run/node": "^2.11.2", - "@remix-run/react": "^2.11.2", - "@remix-run/serve": "^2.11.2", - "bootstrap": "^5.3.3", - "change-case": "^5.4.4", - "classnames": "^2.5.1", - "compression": "^1.7.4", - "cookie": "^0.6.0", - "cross-env": "^7.0.3", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "i18next": "^23.15.1", - "i18next-browser-languagedetector": "^8.0.0", - "i18next-fs-backend": "^2.3.2", - "i18next-http-backend": "^2.6.1", - "isbot": "^4.1.0", - "luxon": "^3.5.0", - "markdown-it": "^14.1.0", - "morgan": "^1.10.0", - "react": "^18.2.0", - "react-bootstrap": "^2.10.4", - "react-bootstrap-icons": "^1.11.4", - "react-dom": "^18.2.0", - "react-i18next": "^15.0.1", - "remix-i18next": "^6.3.0", - "sanitize-html": "^2.13.0" + "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": { - "@fontsource/firago": "^5.0.11", - "@remix-run/dev": "^2.11.2", - "@types/compression": "^1.7.5", - "@types/cookie": "^0.6.0", - "@types/express": "^4.17.21", - "@types/luxon": "^3.4.2", + "@sveltejs/adapter-node": "^5.2.9", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltestrap/sveltestrap": "^6.2.7", + "@types/eslint": "^9.6.0", "@types/markdown-it": "^14.1.2", - "@types/morgan": "^1.9.9", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", "@types/sanitize-html": "^2.13.0", - "@typescript-eslint/eslint-plugin": "^6.7.4", - "@typescript-eslint/parser": "^6.7.4", - "eslint": "^8.38.0", - "eslint-import-resolver-typescript": "^3.6.1", - "eslint-plugin-import": "^2.28.1", - "eslint-plugin-jsx-a11y": "^6.7.1", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0", - "i18next-parser": "^9.0.2", - "prettier": "^3.3.3", - "sass": "1.77.6", - "typescript": "^5.1.6", - "vite": "^5.1.0", - "vite-tsconfig-paths": "^4.2.1" + "bootstrap": "^5.3.3", + "eslint": "^9.7.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-svelte": "^2.36.0", + "globals": "^15.0.0", + "prettier": "^3.3.2", + "prettier-plugin-svelte": "^3.2.6", + "sass": "^1.81.0", + "svelte": "^5.0.0", + "svelte-check": "^4.0.0", + "sveltekit-i18n": "^2.4.2", + "typescript": "^5.0.0", + "typescript-eslint": "^8.0.0", + "vite": "^5.0.3" }, - "engines": { - "node": ">=20.0.0" + "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee", + "dependencies": { + "@fontsource/firago": "^5.1.0", + "bootstrap-icons": "^1.11.3", + "markdown-it": "^14.1.0", + "sanitize-html": "^2.13.1", + "tslog": "^4.9.3" } } diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml new file mode 100644 index 0000000..ca6df0f --- /dev/null +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -0,0 +1,2620 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fontsource/firago': + specifier: ^5.1.0 + version: 5.1.0 + bootstrap-icons: + specifier: ^1.11.3 + version: 1.11.3 + markdown-it: + specifier: ^14.1.0 + version: 14.1.0 + sanitize-html: + specifier: ^2.13.1 + version: 2.13.1 + tslog: + specifier: ^4.9.3 + version: 4.9.3 + devDependencies: + '@sveltejs/adapter-node': + specifier: ^5.2.9 + version: 5.2.9(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))) + '@sveltejs/kit': + specifier: ^2.0.0 + version: 2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + '@sveltejs/vite-plugin-svelte': + specifier: ^4.0.0 + version: 4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + '@sveltestrap/sveltestrap': + specifier: ^6.2.7 + version: 6.2.7(svelte@5.2.2) + '@types/eslint': + specifier: ^9.6.0 + version: 9.6.1 + '@types/markdown-it': + specifier: ^14.1.2 + version: 14.1.2 + '@types/sanitize-html': + specifier: ^2.13.0 + version: 2.13.0 + bootstrap: + specifier: ^5.3.3 + version: 5.3.3(@popperjs/core@2.11.8) + eslint: + specifier: ^9.7.0 + version: 9.15.0 + eslint-config-prettier: + specifier: ^9.1.0 + version: 9.1.0(eslint@9.15.0) + eslint-plugin-svelte: + specifier: ^2.36.0 + version: 2.46.0(eslint@9.15.0)(svelte@5.2.2) + globals: + specifier: ^15.0.0 + version: 15.12.0 + prettier: + specifier: ^3.3.2 + version: 3.3.3 + prettier-plugin-svelte: + specifier: ^3.2.6 + version: 3.2.8(prettier@3.3.3)(svelte@5.2.2) + sass: + specifier: ^1.81.0 + version: 1.81.0 + svelte: + specifier: ^5.0.0 + version: 5.2.2 + svelte-check: + specifier: ^4.0.0 + version: 4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3) + sveltekit-i18n: + specifier: ^2.4.2 + version: 2.4.2(svelte@5.2.2) + typescript: + specifier: ^5.0.0 + version: 5.6.3 + typescript-eslint: + specifier: ^8.0.0 + version: 8.14.0(eslint@9.15.0)(typescript@5.6.3) + vite: + specifier: ^5.0.3 + version: 5.4.11(sass@1.81.0) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.4.1': + resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.19.0': + resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.9.0': + resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.2.0': + resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.15.0': + resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.4': + resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.2.3': + resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@fontsource/firago@5.1.0': + resolution: {integrity: sha512-ym1zCs5Zmp7J6vJ2AzU74H008bxebz53OBY2yPe+qD7+UXwu1S4bHpHYBbznu+HMxccW+JkEZg6UTyqc90V2ug==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.1': + resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.5': + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/set-array@1.2.1': + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + + '@jridgewell/trace-mapping@0.3.25': + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@parcel/watcher-android-arm64@2.5.0': + resolution: {integrity: sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.0': + resolution: {integrity: sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.0': + resolution: {integrity: sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.0': + resolution: {integrity: sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.0': + resolution: {integrity: sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.0': + resolution: {integrity: sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + resolution: {integrity: sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.0': + resolution: {integrity: sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.0': + resolution: {integrity: sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.0': + resolution: {integrity: sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.0': + resolution: {integrity: sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.0': + resolution: {integrity: sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.0': + resolution: {integrity: sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.0': + resolution: {integrity: sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==} + engines: {node: '>= 10.0.0'} + + '@polka/url@1.0.0-next.28': + resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rollup/plugin-commonjs@28.0.1': + resolution: {integrity: sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@15.3.0': + resolution: {integrity: sha512-9eO5McEICxMzJpDW9OnMYSv4Sta3hmt7VtBFz5zR9273suNOydOyq/FrGeGy+KsTRFm8w0SLVhzig2ILFT63Ag==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.1.3': + resolution: {integrity: sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.27.2': + resolution: {integrity: sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.27.2': + resolution: {integrity: sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.27.2': + resolution: {integrity: sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.27.2': + resolution: {integrity: sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.27.2': + resolution: {integrity: sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.27.2': + resolution: {integrity: sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.27.2': + resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.27.2': + resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.27.2': + resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.27.2': + resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': + resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.27.2': + resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.27.2': + resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.27.2': + resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.27.2': + resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.27.2': + resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.27.2': + resolution: {integrity: sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.27.2': + resolution: {integrity: sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==} + cpu: [x64] + os: [win32] + + '@sveltejs/adapter-node@5.2.9': + resolution: {integrity: sha512-51euNrx0AcaTu8//wDfVh7xmqQSVgU52rfinE/MwvGkJa4nHPJMHmzv6+OIpmxg7gZaF6+5NVlxnieCzxLD59g==} + peerDependencies: + '@sveltejs/kit': ^2.4.0 + + '@sveltejs/kit@2.8.1': + resolution: {integrity: sha512-uuOfFwZ4xvnfPsiTB6a4H1ljjTUksGhWnYq5X/Y9z4x5+3uM2Md8q/YVeHL+7w+mygAwoEFdgKZ8YkUuk+VKww==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 + + '@sveltejs/vite-plugin-svelte-inspector@3.0.1': + resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^4.0.0-next.0||^4.0.0 + svelte: ^5.0.0-next.96 || ^5.0.0 + vite: ^5.0.0 + + '@sveltejs/vite-plugin-svelte@4.0.1': + resolution: {integrity: sha512-prXoAE/GleD2C4pKgHa9vkdjpzdYwCSw/kmjw6adIyu0vk5YKCfqIztkLg10m+kOYnzZu3bb0NaPTxlWre2a9Q==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22} + peerDependencies: + svelte: ^5.0.0-next.96 || ^5.0.0 + vite: ^5.0.0 + + '@sveltekit-i18n/base@1.3.7': + resolution: {integrity: sha512-kg1kql1/ro/lIudwFiWrv949Q07gmweln87tflUZR51MNdXXzK4fiJQv5Mw50K/CdQ5BOk/dJ0WOH2vOtBI6yw==} + peerDependencies: + svelte: '>=3.49.0' + + '@sveltekit-i18n/parser-default@1.1.1': + resolution: {integrity: sha512-/gtzLlqm/sox7EoPKD56BxGZktK/syGc79EbJAPWY5KVitQD9SM0TP8yJCqDxTVPk7Lk0WJhrBGUE2Nn0f5M1w==} + + '@sveltestrap/sveltestrap@6.2.7': + resolution: {integrity: sha512-WwLLfAFUb42BGuRrf3Vbct30bQMzlEMMipN/MfxhjuLTmLQeW9muVJfPyvjtWS+mY+RjkSCoHvAp/ZobP1NLlQ==} + peerDependencies: + svelte: ^4.0.0 || ^5.0.0 || ^5.0.0-next.0 + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/sanitize-html@2.13.0': + resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==} + + '@typescript-eslint/eslint-plugin@8.14.0': + resolution: {integrity: sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/parser@8.14.0': + resolution: {integrity: sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/scope-manager@8.14.0': + resolution: {integrity: sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.14.0': + resolution: {integrity: sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/types@8.14.0': + resolution: {integrity: sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.14.0': + resolution: {integrity: sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@typescript-eslint/utils@8.14.0': + resolution: {integrity: sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + + '@typescript-eslint/visitor-keys@8.14.0': + resolution: {integrity: sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-typescript@1.4.13: + resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} + peerDependencies: + acorn: '>=8.9.0' + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bootstrap-icons@1.11.3: + resolution: {integrity: sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==} + + bootstrap@5.3.3: + resolution: {integrity: sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==} + peerDependencies: + '@popperjs/core': ^2.11.8 + + brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + + brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + + 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==} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + cross-spawn@7.0.5: + resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + devalue@5.1.1: + resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-compat-utils@0.5.1: + resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} + engines: {node: '>=12'} + peerDependencies: + eslint: '>=6.0.0' + + eslint-config-prettier@9.1.0: + resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-svelte@2.46.0: + resolution: {integrity: sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==} + engines: {node: ^14.17.0 || >=16.0.0} + 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 + + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.15.0: + resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + esm-env@1.1.4: + resolution: {integrity: sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==} + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrap@1.2.2: + resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.12.0: + resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + engines: {node: '>=18'} + + globalyzer@0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + immutable@5.0.2: + resolution: {integrity: sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==} + + import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + + import-meta-resolve@4.1.0: + resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-core-module@2.15.1: + resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + known-css-properties@0.35.0: + resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + postcss-load-config@3.1.4: + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + + postcss-safe-parser@6.0.0: + resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + + postcss-scss@4.0.9: + resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.4.29 + + postcss-selector-parser@6.1.2: + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} + engines: {node: '>=4'} + + postcss@8.4.49: + resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-plugin-svelte@3.2.8: + resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==} + peerDependencies: + prettier: ^3.0.0 + svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rollup@4.27.2: + resolution: {integrity: sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + sanitize-html@2.13.1: + resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==} + + sass@1.81.0: + resolution: {integrity: sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==} + engines: {node: '>=14.0.0'} + hasBin: true + + semver@7.6.3: + resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + sirv@3.0.0: + resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-check@4.0.9: + resolution: {integrity: sha512-SVNCz2L+9ZELGli7G0n3B3QE5kdf0u27RtKr2ZivWQhcWIXatZxwM4VrQ6AiA2k9zKp2mk5AxkEhdjbpjv7rEw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte-eslint-parser@0.43.0: + resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + svelte: + optional: true + + svelte@5.2.2: + resolution: {integrity: sha512-eHIJRcvA6iuXdRGMESTmBtWTQCcCiol4gyH9DA60ybS35W1x27cvtbndNvWDqX72blyf+AYeQ4gzZ0XGg3L8sw==} + engines: {node: '>=18'} + + sveltekit-i18n@2.4.2: + resolution: {integrity: sha512-hjRWn4V4DBL8JQKJoJa3MRvn6d32Zo+rWkoSP5bsQ/XIAguPdQUZJ8LMe6Nc1rST8WEVdu9+vZI3aFdKYGR3+Q==} + peerDependencies: + svelte: '>=3.49.0' + + tiny-glob@0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + ts-api-utils@1.4.0: + resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + + tslog@4.9.3: + resolution: {integrity: sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==} + engines: {node: '>=16'} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript-eslint@8.14.0: + resolution: {integrity: sha512-K8fBJHxVL3kxMmwByvz8hNdBJ8a0YqKzKDX6jRlrjMuNXyd5T2V02HIq37+OiWXvUUOXgOOGiSSOh26Mh8pC3w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vite@5.4.11: + resolution: {integrity: sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitefu@1.0.3: + resolution: {integrity: sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.15.0)': + dependencies: + eslint: 9.15.0 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.0': + dependencies: + '@eslint/object-schema': 2.1.4 + debug: 4.3.7 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.9.0': {} + + '@eslint/eslintrc@3.2.0': + dependencies: + ajv: 6.12.6 + debug: 4.3.7 + espree: 10.3.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.15.0': {} + + '@eslint/object-schema@2.1.4': {} + + '@eslint/plugin-kit@0.2.3': + dependencies: + levn: 0.4.1 + + '@fontsource/firago@5.1.0': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.1': {} + + '@jridgewell/gen-mapping@0.3.5': + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/set-array@1.2.1': {} + + '@jridgewell/sourcemap-codec@1.5.0': {} + + '@jridgewell/trace-mapping@0.3.25': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + + '@parcel/watcher-android-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.0': + optional: true + + '@parcel/watcher-darwin-x64@2.5.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.0': + optional: true + + '@parcel/watcher-win32-arm64@2.5.0': + optional: true + + '@parcel/watcher-win32-ia32@2.5.0': + optional: true + + '@parcel/watcher-win32-x64@2.5.0': + optional: true + + '@parcel/watcher@2.5.0': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.0 + '@parcel/watcher-darwin-arm64': 2.5.0 + '@parcel/watcher-darwin-x64': 2.5.0 + '@parcel/watcher-freebsd-x64': 2.5.0 + '@parcel/watcher-linux-arm-glibc': 2.5.0 + '@parcel/watcher-linux-arm-musl': 2.5.0 + '@parcel/watcher-linux-arm64-glibc': 2.5.0 + '@parcel/watcher-linux-arm64-musl': 2.5.0 + '@parcel/watcher-linux-x64-glibc': 2.5.0 + '@parcel/watcher-linux-x64-musl': 2.5.0 + '@parcel/watcher-win32-arm64': 2.5.0 + '@parcel/watcher-win32-ia32': 2.5.0 + '@parcel/watcher-win32-x64': 2.5.0 + optional: true + + '@polka/url@1.0.0-next.28': {} + + '@popperjs/core@2.11.8': {} + + '@rollup/plugin-commonjs@28.0.1(rollup@4.27.2)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.2) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.4.2(picomatch@4.0.2) + is-reference: 1.2.1 + magic-string: 0.30.12 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.27.2 + + '@rollup/plugin-json@6.1.0(rollup@4.27.2)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.2) + optionalDependencies: + rollup: 4.27.2 + + '@rollup/plugin-node-resolve@15.3.0(rollup@4.27.2)': + dependencies: + '@rollup/pluginutils': 5.1.3(rollup@4.27.2) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.8 + optionalDependencies: + rollup: 4.27.2 + + '@rollup/pluginutils@5.1.3(rollup@4.27.2)': + dependencies: + '@types/estree': 1.0.6 + estree-walker: 2.0.2 + picomatch: 4.0.2 + optionalDependencies: + rollup: 4.27.2 + + '@rollup/rollup-android-arm-eabi@4.27.2': + optional: true + + '@rollup/rollup-android-arm64@4.27.2': + optional: true + + '@rollup/rollup-darwin-arm64@4.27.2': + optional: true + + '@rollup/rollup-darwin-x64@4.27.2': + optional: true + + '@rollup/rollup-freebsd-arm64@4.27.2': + optional: true + + '@rollup/rollup-freebsd-x64@4.27.2': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.27.2': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.27.2': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.27.2': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.27.2': + optional: true + + '@rollup/rollup-linux-x64-musl@4.27.2': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.27.2': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.27.2': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.27.2': + optional: true + + '@sveltejs/adapter-node@5.2.9(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))': + dependencies: + '@rollup/plugin-commonjs': 28.0.1(rollup@4.27.2) + '@rollup/plugin-json': 6.1.0(rollup@4.27.2) + '@rollup/plugin-node-resolve': 15.3.0(rollup@4.27.2) + '@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + rollup: 4.27.2 + + '@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + '@types/cookie': 0.6.0 + cookie: 0.6.0 + devalue: 5.1.1 + esm-env: 1.1.4 + import-meta-resolve: 4.1.0 + kleur: 4.1.5 + magic-string: 0.30.12 + mrmime: 2.0.0 + sade: 1.8.1 + set-cookie-parser: 2.7.1 + sirv: 3.0.0 + svelte: 5.2.2 + tiny-glob: 0.2.9 + vite: 5.4.11(sass@1.81.0) + + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))': + dependencies: + '@sveltejs/vite-plugin-svelte': 4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + debug: 4.3.7 + svelte: 5.2.2 + vite: 5.4.11(sass@1.81.0) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + debug: 4.3.7 + deepmerge: 4.3.1 + kleur: 4.1.5 + magic-string: 0.30.12 + svelte: 5.2.2 + vite: 5.4.11(sass@1.81.0) + vitefu: 1.0.3(vite@5.4.11(sass@1.81.0)) + transitivePeerDependencies: + - supports-color + + '@sveltekit-i18n/base@1.3.7(svelte@5.2.2)': + dependencies: + svelte: 5.2.2 + + '@sveltekit-i18n/parser-default@1.1.1': {} + + '@sveltestrap/sveltestrap@6.2.7(svelte@5.2.2)': + dependencies: + '@popperjs/core': 2.11.8 + svelte: 5.2.2 + + '@types/cookie@0.6.0': {} + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.6': {} + + '@types/json-schema@7.0.15': {} + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + + '@types/resolve@1.20.2': {} + + '@types/sanitize-html@2.13.0': + dependencies: + htmlparser2: 8.0.2 + + '@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.15.0)(typescript@5.6.3))(eslint@9.15.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.14.0(eslint@9.15.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/type-utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.14.0 + eslint: 9.15.0 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.14.0(eslint@9.15.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.14.0 + debug: 4.3.7 + eslint: 9.15.0 + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.14.0': + dependencies: + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 + + '@typescript-eslint/type-utils@8.14.0(eslint@9.15.0)(typescript@5.6.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) + debug: 4.3.7 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + '@typescript-eslint/types@8.14.0': {} + + '@typescript-eslint/typescript-estree@8.14.0(typescript@5.6.3)': + dependencies: + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/visitor-keys': 8.14.0 + debug: 4.3.7 + fast-glob: 3.3.2 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.6.3 + ts-api-utils: 1.4.0(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.14.0(eslint@9.15.0)(typescript@5.6.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@typescript-eslint/scope-manager': 8.14.0 + '@typescript-eslint/types': 8.14.0 + '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) + eslint: 9.15.0 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/visitor-keys@8.14.0': + dependencies: + '@typescript-eslint/types': 8.14.0 + eslint-visitor-keys: 3.4.3 + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-typescript@1.4.13(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-query@5.3.2: {} + + axobject-query@4.1.0: {} + + balanced-match@1.0.2: {} + + bootstrap-icons@1.11.3: {} + + bootstrap@5.3.3(@popperjs/core@2.11.8): + dependencies: + '@popperjs/core': 2.11.8 + + brace-expansion@1.1.11: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.1: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commondir@1.0.1: {} + + concat-map@0.0.1: {} + + cookie@0.6.0: {} + + cross-spawn@7.0.5: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + deepmerge@4.3.1: {} + + detect-libc@1.0.3: + optional: true + + devalue@5.1.1: {} + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + entities@4.5.0: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escape-string-regexp@4.0.0: {} + + eslint-compat-utils@0.5.1(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + semver: 7.6.3 + + eslint-config-prettier@9.1.0(eslint@9.15.0): + dependencies: + eslint: 9.15.0 + + eslint-plugin-svelte@2.46.0(eslint@9.15.0)(svelte@5.2.2): + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@jridgewell/sourcemap-codec': 1.5.0 + eslint: 9.15.0 + eslint-compat-utils: 0.5.1(eslint@9.15.0) + esutils: 2.0.3 + known-css-properties: 0.35.0 + postcss: 8.4.49 + postcss-load-config: 3.1.4(postcss@8.4.49) + postcss-safe-parser: 6.0.0(postcss@8.4.49) + postcss-selector-parser: 6.1.2 + semver: 7.6.3 + svelte-eslint-parser: 0.43.0(svelte@5.2.2) + optionalDependencies: + svelte: 5.2.2 + transitivePeerDependencies: + - ts-node + + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@8.2.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.0: {} + + eslint@9.15.0: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.0 + '@eslint/core': 0.9.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.15.0 + '@eslint/plugin-kit': 0.2.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.1 + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.5 + debug: 4.3.7 + escape-string-regexp: 4.0.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + 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.4 + transitivePeerDependencies: + - supports-color + + esm-env@1.1.4: {} + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + espree@9.6.1: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 3.4.3 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrap@1.2.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.6 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-glob@3.3.2: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fastq@1.17.1: + dependencies: + reusify: 1.0.4 + + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + + flatted@3.3.1: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@15.12.0: {} + + globalyzer@0.1.0: {} + + globrex@0.1.2: {} + + graphemer@1.4.0: {} + + has-flag@4.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + + ignore@5.3.2: {} + + immutable@5.0.2: {} + + import-fresh@3.3.0: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-meta-resolve@4.1.0: {} + + imurmurhash@0.1.4: {} + + is-core-module@2.15.1: + dependencies: + hasown: 2.0.2 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-module@1.0.0: {} + + is-number@7.0.0: {} + + is-plain-object@5.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.6 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.6 + + isexe@2.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kleur@4.1.5: {} + + known-css-properties@0.35.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@2.1.0: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + locate-character@3.0.0: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + magic-string@0.30.12: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + mdurl@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + mri@1.2.0: {} + + mrmime@2.0.0: {} + + ms@2.1.3: {} + + nanoid@3.3.7: {} + + natural-compare@1.4.0: {} + + node-addon-api@7.1.1: + optional: true + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-srcset@1.0.2: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + postcss-load-config@3.1.4(postcss@8.4.49): + dependencies: + lilconfig: 2.1.0 + yaml: 1.10.2 + optionalDependencies: + postcss: 8.4.49 + + postcss-safe-parser@6.0.0(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-scss@4.0.9(postcss@8.4.49): + dependencies: + postcss: 8.4.49 + + postcss-selector-parser@6.1.2: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.4.49: + dependencies: + nanoid: 3.3.7 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier-plugin-svelte@3.2.8(prettier@3.3.3)(svelte@5.2.2): + dependencies: + prettier: 3.3.3 + svelte: 5.2.2 + + prettier@3.3.3: {} + + punycode.js@2.3.1: {} + + punycode@2.3.1: {} + + queue-microtask@1.2.3: {} + + readdirp@4.0.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.8: + dependencies: + is-core-module: 2.15.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.0.4: {} + + rollup@4.27.2: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.27.2 + '@rollup/rollup-android-arm64': 4.27.2 + '@rollup/rollup-darwin-arm64': 4.27.2 + '@rollup/rollup-darwin-x64': 4.27.2 + '@rollup/rollup-freebsd-arm64': 4.27.2 + '@rollup/rollup-freebsd-x64': 4.27.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.27.2 + '@rollup/rollup-linux-arm-musleabihf': 4.27.2 + '@rollup/rollup-linux-arm64-gnu': 4.27.2 + '@rollup/rollup-linux-arm64-musl': 4.27.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.27.2 + '@rollup/rollup-linux-riscv64-gnu': 4.27.2 + '@rollup/rollup-linux-s390x-gnu': 4.27.2 + '@rollup/rollup-linux-x64-gnu': 4.27.2 + '@rollup/rollup-linux-x64-musl': 4.27.2 + '@rollup/rollup-win32-arm64-msvc': 4.27.2 + '@rollup/rollup-win32-ia32-msvc': 4.27.2 + '@rollup/rollup-win32-x64-msvc': 4.27.2 + fsevents: 2.3.3 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + sanitize-html@2.13.1: + dependencies: + deepmerge: 4.3.1 + escape-string-regexp: 4.0.0 + htmlparser2: 8.0.2 + is-plain-object: 5.0.0 + parse-srcset: 1.0.2 + postcss: 8.4.49 + + sass@1.81.0: + dependencies: + chokidar: 4.0.1 + immutable: 5.0.2 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.0 + + semver@7.6.3: {} + + set-cookie-parser@2.7.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + sirv@3.0.0: + dependencies: + '@polka/url': 1.0.0-next.28 + mrmime: 2.0.0 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-check@4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + chokidar: 4.0.1 + fdir: 6.4.2(picomatch@4.0.2) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.2.2 + typescript: 5.6.3 + transitivePeerDependencies: + - picomatch + + svelte-eslint-parser@0.43.0(svelte@5.2.2): + dependencies: + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + postcss: 8.4.49 + postcss-scss: 4.0.9(postcss@8.4.49) + optionalDependencies: + svelte: 5.2.2 + + svelte@5.2.2: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.6 + acorn: 8.14.0 + acorn-typescript: 1.4.13(acorn@8.14.0) + aria-query: 5.3.2 + axobject-query: 4.1.0 + esm-env: 1.1.4 + esrap: 1.2.2 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.12 + zimmerframe: 1.1.2 + + sveltekit-i18n@2.4.2(svelte@5.2.2): + dependencies: + '@sveltekit-i18n/base': 1.3.7(svelte@5.2.2) + '@sveltekit-i18n/parser-default': 1.1.1 + svelte: 5.2.2 + + tiny-glob@0.2.9: + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + ts-api-utils@1.4.0(typescript@5.6.3): + dependencies: + typescript: 5.6.3 + + tslog@4.9.3: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript-eslint@8.14.0(eslint@9.15.0)(typescript@5.6.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.15.0)(typescript@5.6.3))(eslint@9.15.0)(typescript@5.6.3) + '@typescript-eslint/parser': 8.14.0(eslint@9.15.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) + optionalDependencies: + typescript: 5.6.3 + transitivePeerDependencies: + - eslint + - supports-color + + typescript@5.6.3: {} + + uc.micro@2.1.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + vite@5.4.11(sass@1.81.0): + dependencies: + esbuild: 0.21.5 + postcss: 8.4.49 + rollup: 4.27.2 + optionalDependencies: + fsevents: 2.3.3 + sass: 1.81.0 + + vitefu@1.0.3(vite@5.4.11(sass@1.81.0)): + optionalDependencies: + vite: 5.4.11(sass@1.81.0) + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yaml@1.10.2: {} + + yocto-queue@0.1.0: {} + + zimmerframe@1.1.2: {} diff --git a/Foxnouns.Frontend/public/favicon.svg b/Foxnouns.Frontend/public/favicon.svg deleted file mode 100644 index 11e664f..0000000 --- a/Foxnouns.Frontend/public/favicon.svg +++ /dev/null @@ -1,2 +0,0 @@ - -image/svg+xml \ No newline at end of file diff --git a/Foxnouns.Frontend/public/locales/en.json b/Foxnouns.Frontend/public/locales/en.json deleted file mode 100644 index a9aa060..0000000 --- a/Foxnouns.Frontend/public/locales/en.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "error": { - "heading": "An error occurred", - "validation": { - "too-long": "Value is too long, maximum length is {{maxLength}}, current length is {{actualLength}}.", - "too-short": "Value is too short, minimum length is {{minLength}}, current length is {{actualLength}}.", - "disallowed-value": "The value <1>{{actualValue}} is not allowed here. Allowed values are: <4>{{allowedValues}}", - "generic": "The value <1>{{actualValue}} is not allowed here. Reason: {{reason}}", - "generic-no-value": "The value you entered is not allowed here. Reason: {{reason}}" - }, - "errors": { - "authentication-error": "There was an error validating your credentials.", - "authentication-required": "You need to log in.", - "bad-request": "Server rejected your input, please check anything for errors.", - "forbidden": "You are not allowed to perform that action.", - "generic-error": "An unknown error occurred.", - "internal-server-error": "Server experienced an internal error, please try again later.", - "member-not-found": "Member not found, please check your spelling and try again.", - "user-not-found": "User 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." - }, - "title": "An error occurred", - "more-info": "Click here for a more detailed error" - }, - "navbar": { - "view-profile": "View profile", - "settings": "Settings", - "log-out": "Log out", - "log-in": "Log in or sign up" - }, - "user": { - "avatar-alt": "Avatar for @{{username}}", - "heading": { - "names": "Names", - "pronouns": "Pronouns", - "members": "Members" - }, - "member-avatar-alt": "Avatar for {{name}}", - "member-hidden": "This member is unlisted, and not shown in your public member list.", - "own-profile-alert": "You are currently viewing your <1>public profile.<3><4>Edit your profile", - "create-member-button": "Create member", - "no-members-blurb": "You don't have any members yet.<1>Members are sub-profiles that can have their own avatar, names, pronouns, and preferred terms.<3>You can create a new member with the \"Create member\" button above. <6>(only you can see this)" - }, - "member": { - "avatar-alt": "Avatar for {{name}}", - "own-profile-alert": "You are currently viewing the <1>public profile of {{memberName}}.<5><6>Edit profile", - "back": "Back to {{name}}" - }, - "log-in": { - "callback": { - "invalid-ticket": "Invalid ticket (it might have been too long since you logged in), please <2>try again.", - "invalid-username": "Invalid username", - "username-taken": "That username is already taken, please try something else.", - "title": { - "discord-link": "Link a new Discord account", - "discord-success": "Log in with Discord", - "discord-register": "Register with Discord", - "fediverse-success": "Log in with a Fediverse account", - "fediverse-register": "Register with a Fediverse account" - }, - "link-error": "Could not link account", - "discord-link-success": "Linked a new Discord account!", - "discord-link-success-hint": "Successfully linked the Discord account {{username}} with your pronouns.cc account. You can now close this page.", - "success": "Successfully logged in!", - "success-link": "Welcome back, <1>@{{username}}!", - "redirect-hint": "If you're not redirected to your profile in a few seconds, press the link above.", - "remote-username": { - "discord": "Your Discord username", - "fediverse": "Your Fediverse account" - }, - "username": "Username", - "sign-up-button": "Sign up" - }, - "fediverse": { - "choose-title": "Log in with a Fediverse account", - "choose-form-title": "Choose a Fediverse instance" - }, - "fediverse-instance-label": "Your Fediverse instance", - "fediverse-log-in-button": "Log in", - "title": "Log in", - "form-title": "Log in with email", - "email": "Email address", - "password": "Password", - "log-in-button": "Log in", - "register-with-email": "Register with email", - "3rd-party": { - "title": "Log in with another service", - "desc": "If you prefer, you can also log in with one of these services:", - "discord": "Log in with Discord", - "google": "Log in with Google", - "tumblr": "Log in with Tumblr", - "fediverse": "Log in with the Fediverse" - }, - "invalid-credentials": "Invalid email address or password, please check your spelling and try again." - }, - "welcome": { - "title": "Welcome", - "header": "Welcome to pronouns.cc!", - "blurb": "{welcome.blurb}", - "customize-profile": "Customize your profile", - "customize-profile-blurb": "{welcome.customize-profile-blurb}", - "create-members": "Create members", - "create-members-blurb": "{welcome.create-members-blurb}", - "custom-preferences": "Customize your preferences", - "custom-preferences-blurb": "{welcome.custom-preferences-blurb}", - "profile-button": "Go to your profile" - }, - "settings": { - "general": { - "username": "Username", - "change-username": "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.", - "log-out-everywhere": "Log out everywhere", - "log-out-everywhere-hint": "If you think one of your tokens might have been compromised, you can log out on all devices by clicking this button.", - "force-log-out-button": "Force log out", - "table-header": "General account information", - "id": "Your user ID", - "created": "Account created at", - "member-count": "Members", - "member-list-hidden": "Member list hidden?", - "custom-preferences": "Custom preferences", - "role": "Account role", - "username-update-error": "Could not update your username as the new username is invalid:\n{{message}}", - "log-out-everywhere-confirm": "Are you sure you want to log out everywhere?\nPlease double check your authentication methods before doing so, as it might lock you out of your account." - }, - "auth": { - "title": "Authentication", - "form": { - "add-first-email": "Set an email address", - "add-extra-email": "Add another email address", - "email-address": "Email address", - "password-1": "Password", - "password-2": "Confirm password", - "add-email-button": "Add email address", - "add-first-discord-account": "Link a Discord account", - "add-extra-discord-account": "Link another Discord account", - "add-first-fediverse-account": "Link a Fediverse account", - "add-extra-fediverse-account": "Link another Fediverse account" - }, - "no-email": "You haven't linked any email addresses yet. You can add one using this form.", - "new-email-pending": "Email address added! Click the link in your inbox to confirm.", - "email-link-success": "Email successfully linked", - "redirect-to-auth-hint": "You will be redirected back to the authentication page in a few seconds.", - "remove-auth-method-title": "Remove authentication method", - "remove-auth-method-hint": "Are you sure you want to remove {{username}} ({{methodName}}) from your account? You will no longer be able to log in using it.", - "remove-auth-method": "Remove", - "email-addresses": "Email addresses", - "discord-accounts": "Linked Discord accounts", - "fediverse-accounts": "Linked Fediverse accounts" - }, - "title": "Settings", - "nav": { - "general-information": "General information", - "profile": "Base profile", - "members": "Members", - "authentication": "Authentication", - "export": "Export your data" - } - }, - "yes": "Yes", - "no": "No" -} diff --git a/Foxnouns.Frontend/server.js b/Foxnouns.Frontend/server.js deleted file mode 100644 index 88ce3db..0000000 --- a/Foxnouns.Frontend/server.js +++ /dev/null @@ -1,51 +0,0 @@ -import { env } from "node:process"; -import { createRequestHandler } from "@remix-run/express"; -import compression from "compression"; -import express from "express"; -import morgan from "morgan"; - -const viteDevServer = - env.NODE_ENV === "production" - ? undefined - : await import("vite").then((vite) => - vite.createServer({ - server: { middlewareMode: true }, - }), - ); - -const remixHandler = createRequestHandler({ - build: viteDevServer - ? () => viteDevServer.ssrLoadModule("virtual:remix/server-build") - : await import("./build/server/index.js"), -}); - -const app = express(); - -app.use(compression()); - -// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header -app.disable("x-powered-by"); - -// handle asset requests -if (viteDevServer) { - app.use(viteDevServer.middlewares); -} else { - // Vite fingerprints its assets so we can cache forever. - app.use("/assets", express.static("build/client/assets", { immutable: true, maxAge: "1y" })); -} - -// Only cache locales for a minute, as they can change without the filename changing -// TODO: figure out how to change the filenames on update? -app.use("/locales", express.static("build/client/locales", { maxAge: "1m" })); - -// Everything else (like favicon.ico) is cached for an hour. You may want to be -// more aggressive with this caching. -app.use(express.static("build/client", { maxAge: "1d" })); - -app.use(morgan("tiny")); - -// handle SSR requests -app.all("*", remixHandler); - -const port = env.PORT || 3000; -app.listen(port, () => console.log(`Express server listening at http://localhost:${port}`)); diff --git a/Foxnouns.Frontend/src/app.d.ts b/Foxnouns.Frontend/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/Foxnouns.Frontend/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // 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 new file mode 100644 index 0000000..77a5ff5 --- /dev/null +++ b/Foxnouns.Frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
    %sveltekit.body%
    + + diff --git a/Foxnouns.Frontend/app/app.scss b/Foxnouns.Frontend/src/app.scss similarity index 68% rename from Foxnouns.Frontend/app/app.scss rename to Foxnouns.Frontend/src/app.scss index 50b0ad0..252667f 100644 --- a/Foxnouns.Frontend/app/app.scss +++ b/Foxnouns.Frontend/src/app.scss @@ -18,17 +18,8 @@ ) ); +@import "bootstrap-icons/font/bootstrap-icons.css"; @import "@fontsource/firago/400.css"; @import "@fontsource/firago/400-italic.css"; @import "@fontsource/firago/700.css"; - -.pride-flag { - height: 1.5rem; - max-width: 200px; - border-radius: 3px; -} - -// This is necessary for line breaks in translation strings to show up. Don't ask me why -.text-has-newline { - white-space: pre-line; -} +@import "@fontsource/firago/700-italic.css"; diff --git a/Foxnouns.Frontend/src/hooks.server.ts b/Foxnouns.Frontend/src/hooks.server.ts new file mode 100644 index 0000000..e8ec723 --- /dev/null +++ b/Foxnouns.Frontend/src/hooks.server.ts @@ -0,0 +1,13 @@ +import { PRIVATE_API_HOST, PRIVATE_INTERNAL_API_HOST } from "$env/static/private"; +import { PUBLIC_API_BASE } from "$env/static/public"; +import type { HandleFetch } from "@sveltejs/kit"; + +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); +}; diff --git a/Foxnouns.Frontend/src/lib/api/error.ts b/Foxnouns.Frontend/src/lib/api/error.ts new file mode 100644 index 0000000..6b5d918 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/api/error.ts @@ -0,0 +1,54 @@ +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 { + status: this.raw?.status || 500, + code: this.code, + message: this.raw?.message || "Internal server error", + errors: this.raw?.errors, + }; + } +} + +export type RawApiError = { + 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", + // 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; +}; diff --git a/Foxnouns.Frontend/src/lib/api/index.ts b/Foxnouns.Frontend/src/lib/api/index.ts new file mode 100644 index 0000000..0c8293d --- /dev/null +++ b/Foxnouns.Frontend/src/lib/api/index.ts @@ -0,0 +1,92 @@ +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"; + +export type RequestArgs = { + token?: string; + isInternal?: boolean; + body?: any; + fetch?: typeof fetch; + 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 Promise 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 = `${PUBLIC_API_BASE}/${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(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(); + if ("code" in err) throw new ApiError(err); + else throw new ApiError(); + } + return (await resp.json()) as T; +} + +/** + * 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/app/lib/api/auth.ts b/Foxnouns.Frontend/src/lib/api/models/auth.ts similarity index 89% rename from Foxnouns.Frontend/app/lib/api/auth.ts rename to Foxnouns.Frontend/src/lib/api/models/auth.ts index 0f8ce27..3ea7cab 100644 --- a/Foxnouns.Frontend/app/lib/api/auth.ts +++ b/Foxnouns.Frontend/src/lib/api/models/auth.ts @@ -1,4 +1,4 @@ -import { User } from "~/lib/api/user"; +import type { User } from "./user"; export type AuthResponse = { user: User; diff --git a/Foxnouns.Frontend/src/lib/api/models/index.ts b/Foxnouns.Frontend/src/lib/api/models/index.ts new file mode 100644 index 0000000..cc8fd7e --- /dev/null +++ b/Foxnouns.Frontend/src/lib/api/models/index.ts @@ -0,0 +1,4 @@ +export * from "./meta"; +export * from "./user"; +export * from "./member"; +export * from "./auth"; diff --git a/Foxnouns.Frontend/app/lib/api/member.ts b/Foxnouns.Frontend/src/lib/api/models/member.ts similarity index 60% rename from Foxnouns.Frontend/app/lib/api/member.ts rename to Foxnouns.Frontend/src/lib/api/models/member.ts index 1719f04..26eab92 100644 --- a/Foxnouns.Frontend/app/lib/api/member.ts +++ b/Foxnouns.Frontend/src/lib/api/models/member.ts @@ -1,4 +1,4 @@ -import { Field, PartialMember, PartialUser, PrideFlag } from "~/lib/api/user"; +import type { Field, PartialMember, PartialUser, PrideFlag } from "./user"; export type Member = PartialMember & { fields: Field[]; diff --git a/Foxnouns.Frontend/app/lib/api/meta.ts b/Foxnouns.Frontend/src/lib/api/models/meta.ts similarity index 85% rename from Foxnouns.Frontend/app/lib/api/meta.ts rename to Foxnouns.Frontend/src/lib/api/models/meta.ts index 5f2bd11..f822478 100644 --- a/Foxnouns.Frontend/app/lib/api/meta.ts +++ b/Foxnouns.Frontend/src/lib/api/models/meta.ts @@ -1,4 +1,5 @@ -export default interface Meta { +export type Meta = { + repository: string; version: string; hash: string; users: { @@ -9,7 +10,7 @@ export default interface Meta { }; members: number; limits: Limits; -} +}; export type Limits = { member_count: number; diff --git a/Foxnouns.Frontend/app/lib/api/user.ts b/Foxnouns.Frontend/src/lib/api/models/user.ts similarity index 93% rename from Foxnouns.Frontend/app/lib/api/user.ts rename to Foxnouns.Frontend/src/lib/api/models/user.ts index 4b39502..715cf46 100644 --- a/Foxnouns.Frontend/app/lib/api/user.ts +++ b/Foxnouns.Frontend/src/lib/api/models/user.ts @@ -1,8 +1,8 @@ export type PartialUser = { id: string; username: string; - display_name?: string | null; - avatar_url?: string | null; + display_name: string | null; + avatar_url: string | null; custom_preferences: Record; }; @@ -39,7 +39,7 @@ export type UserSettings = { export type PartialMember = { id: string; name: string; - display_name: string | null; + display_name: string; bio: string | null; avatar_url: string | null; names: FieldEntry[]; @@ -87,7 +87,9 @@ export enum PreferenceSize { Small = "SMALL", } -export function mergePreferences(prefs: Record) { +export function mergePreferences( + prefs: Record, +): Record { return Object.assign({}, defaultPreferences, prefs); } diff --git a/Foxnouns.Frontend/src/lib/components/Avatar.svelte b/Foxnouns.Frontend/src/lib/components/Avatar.svelte new file mode 100644 index 0000000..1b116ea --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/Avatar.svelte @@ -0,0 +1,14 @@ + + + diff --git a/Foxnouns.Frontend/src/lib/components/Error.svelte b/Foxnouns.Frontend/src/lib/components/Error.svelte new file mode 100644 index 0000000..9ca2ff0 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/Error.svelte @@ -0,0 +1,32 @@ + + + + {#if error.code === ErrorCode.BadRequest} + {$t("error.bad-request-header")} + {:else} + {$t("error.generic-header")} + {/if} + +

    {errorDescription($t, error.code)}

    +{#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 new file mode 100644 index 0000000..a94d9ed --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/ErrorAlert.svelte @@ -0,0 +1,11 @@ + + + diff --git a/Foxnouns.Frontend/src/lib/components/Logo.svelte b/Foxnouns.Frontend/src/lib/components/Logo.svelte new file mode 100644 index 0000000..9da9d99 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/Logo.svelte @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + diff --git a/Foxnouns.Frontend/src/lib/components/Navbar.svelte b/Foxnouns.Frontend/src/lib/components/Navbar.svelte new file mode 100644 index 0000000..2661fc9 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/Navbar.svelte @@ -0,0 +1,69 @@ + + + + + + {#if meta.version.endsWith(".dirty")} + dev + {:else} + beta + {/if} + + + + + + + + diff --git a/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte b/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte new file mode 100644 index 0000000..d029158 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte @@ -0,0 +1,17 @@ + + + + + +{preference.tooltip}: +{preference.tooltip} diff --git a/Foxnouns.Frontend/src/lib/components/errors/KeyedValidationErrors.svelte b/Foxnouns.Frontend/src/lib/components/errors/KeyedValidationErrors.svelte new file mode 100644 index 0000000..86c21a9 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/errors/KeyedValidationErrors.svelte @@ -0,0 +1,16 @@ + + +
  • + {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 new file mode 100644 index 0000000..c840fb7 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/errors/RequestValidationError.svelte @@ -0,0 +1,41 @@ + + +{#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 new file mode 100644 index 0000000..9515628 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/OwnProfileNotice.svelte @@ -0,0 +1,16 @@ + + +
    + {#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/ProfileFields.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileFields.svelte new file mode 100644 index 0000000..d6594f8 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileFields.svelte @@ -0,0 +1,26 @@ + + +
    + {#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 new file mode 100644 index 0000000..5c042cc --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte @@ -0,0 +1,24 @@ + + + + {flag.description ?? flag.name} + {flag.description + {flag.name} + + + diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte new file mode 100644 index 0000000..d28a001 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte @@ -0,0 +1,69 @@ + + +
    +
    +
    + + + {#if profile.flags && profile.bio} +
    + {#each profile.flags as flag} + + {/each} +
    + {/if} +
    +
    + {#if profile.display_name} +
    +

    {profile.display_name}

    +

    {name}

    +
    + {:else} +

    {name}

    + {/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 new file mode 100644 index 0000000..d4672a8 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte @@ -0,0 +1,33 @@ + + +{#if isLink} + +
  • + + {displayLink} +
  • +
    +{:else} +
  • + + {displayLink} +
  • +{/if} diff --git a/Foxnouns.Frontend/src/lib/components/profile/field/ProfileField.svelte b/Foxnouns.Frontend/src/lib/components/profile/field/ProfileField.svelte new file mode 100644 index 0000000..01c998d --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/field/ProfileField.svelte @@ -0,0 +1,30 @@ + + +
    +

    {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 new file mode 100644 index 0000000..5773757 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/field/ProfileFieldEntry.svelte @@ -0,0 +1,28 @@ + + + + + {@render children?.()} + diff --git a/Foxnouns.Frontend/src/lib/components/profile/field/PronounLink.svelte b/Foxnouns.Frontend/src/lib/components/profile/field/PronounLink.svelte new file mode 100644 index 0000000..cae11ec --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/field/PronounLink.svelte @@ -0,0 +1,41 @@ + + +{#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 new file mode 100644 index 0000000..7d1bdc7 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte @@ -0,0 +1,49 @@ + + +
    + + + +

    + + {member.name} + + {#if pronouns} +
    + {pronouns} + {/if} +

    +
    diff --git a/Foxnouns.Frontend/src/lib/errorCodes.svelte.ts b/Foxnouns.Frontend/src/lib/errorCodes.svelte.ts new file mode 100644 index 0000000..b9c3d9a --- /dev/null +++ b/Foxnouns.Frontend/src/lib/errorCodes.svelte.ts @@ -0,0 +1,36 @@ +import { ErrorCode } from "$api/error"; +import type { Modifier } from "sveltekit-i18n"; + +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.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 new file mode 100644 index 0000000..858f2bd --- /dev/null +++ b/Foxnouns.Frontend/src/lib/i18n/index.ts @@ -0,0 +1,24 @@ +import { PUBLIC_LANGUAGE } from "$env/static/public"; +import i18n, { type Config } from "sveltekit-i18n"; + +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 new file mode 100644 index 0000000..2cba2a5 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en-PR.json @@ -0,0 +1,28 @@ +{ + "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 new file mode 100644 index 0000000..5d603d7 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -0,0 +1,63 @@ +{ + "hello": "Hello, {{name}}!", + "nav": { + "log-in": "Log in or sign up", + "settings": "Settings" + }, + "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" + }, + "title": { + "log-in": "Log in", + "welcome": "Welcome" + }, + "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-mastodon": "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" + }, + "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 long, 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" + } +} diff --git a/Foxnouns.Frontend/src/lib/index.ts b/Foxnouns.Frontend/src/lib/index.ts new file mode 100644 index 0000000..6b65464 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/index.ts @@ -0,0 +1,12 @@ +// place files you want to import through the `$lib` alias in this folder. + +import type { Cookies } from "@sveltejs/kit"; + +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: "/" }); + +// TODO: change this to something we actually clearly have the rights to use +export const DEFAULT_AVATAR = "https://pronouns.cc/default/512.webp"; diff --git a/Foxnouns.Frontend/src/lib/log.ts b/Foxnouns.Frontend/src/lib/log.ts new file mode 100644 index 0000000..f8995c6 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/log.ts @@ -0,0 +1,4 @@ +import { Logger } from "tslog"; + +const log = new Logger(); +export default log; diff --git a/Foxnouns.Frontend/app/lib/markdown.ts b/Foxnouns.Frontend/src/lib/markdown.ts similarity index 60% rename from Foxnouns.Frontend/app/lib/markdown.ts rename to Foxnouns.Frontend/src/lib/markdown.ts index 37dbec2..94a1a05 100644 --- a/Foxnouns.Frontend/app/lib/markdown.ts +++ b/Foxnouns.Frontend/src/lib/markdown.ts @@ -13,10 +13,6 @@ const unsafeMd = new MarkdownIt({ linkify: true, }); -export function renderMarkdown(src: string | null) { - return src ? sanitize(md.render(src)) : null; -} +export const renderMarkdown = (src: string | null) => (src ? sanitize(md.render(src)) : null); -export function renderUnsafeMarkdown(src: string) { - return sanitize(unsafeMd.render(src)); -} +export const renderUnsafeMarkdown = (src: string) => sanitize(unsafeMd.render(src)); diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts new file mode 100644 index 0000000..e73ca7d --- /dev/null +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -0,0 +1,21 @@ +import { clearToken, TOKEN_COOKIE_NAME } from "$lib"; +import { apiRequest } from "$api"; +import ApiError, { ErrorCode } from "$api/error"; +import type { Meta, User } from "$api/models"; +import log from "$lib/log"; +import type { LayoutServerLoad } from "./$types"; + +export const load = (async ({ fetch, cookies }) => { + let meUser: User | null = null; + if (cookies.get(TOKEN_COOKIE_NAME)) { + try { + meUser = await apiRequest("GET", "/users/@me", { fetch, cookies }); + } 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 }; +}) satisfies LayoutServerLoad; diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..ceff270 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/+layout.svelte @@ -0,0 +1,13 @@ + + + + +{@render children?.()} diff --git a/Foxnouns.Frontend/src/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte new file mode 100644 index 0000000..47ab0e5 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/+page.svelte @@ -0,0 +1,21 @@ + + + + pronouns.cc + + +
    +

    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 new file mode 100644 index 0000000..330bd21 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts @@ -0,0 +1,20 @@ +import { apiRequest } from "$api"; +import type { UserWithMembers } from "$api/models"; + +export const load = async ({ params, fetch, cookies, url }) => { + const user = await apiRequest("GET", `/users/${params.username}`, { + fetch, + cookies, + }); + + // Paginate members on the server side + let currentPage = Number(url.searchParams.get("page") || "0"); + const pageCount = Math.ceil(user.members.length / 20); + let members = user.members.slice(currentPage * 20, (currentPage + 1) * 20); + if (members.length === 0) { + members = user.members.slice(0, 20); + currentPage = 0; + } + + return { user, members, currentPage, pageCount }; +}; diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte new file mode 100644 index 0000000..0ed36cc --- /dev/null +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte @@ -0,0 +1,60 @@ + + + + @{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]/Paginator.svelte b/Foxnouns.Frontend/src/routes/@[username]/Paginator.svelte new file mode 100644 index 0000000..cf5aa54 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/@[username]/Paginator.svelte @@ -0,0 +1,31 @@ + + +{#if pageCount > 1} + + + + + + + + + {currentPage + 1} + + + + + + + + +{/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 new file mode 100644 index 0000000..ce145d2 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts @@ -0,0 +1,62 @@ +import { apiRequest } from "$api"; +import ApiError, { ErrorCode, type RawApiError } from "$api/error"; +import type { AuthResponse, CallbackResponse } from "$api/models/auth.js"; +import { setToken } from "$lib"; +import log from "$lib/log.js"; +import { isRedirect, redirect } from "@sveltejs/kit"; + +export const load = async ({ parent, params, url, fetch, cookies }) => { + const { meUser } = await parent(); + if (meUser) redirect(303, `/@${meUser.username}`); + + 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; + + const resp = await apiRequest("POST", "/auth/fediverse/callback", { + body: { code, state, instance: params.instance }, + isInternal: true, + fetch, + }); + + if (resp.has_account) { + setToken(cookies, resp.token!); + redirect(303, `/@${resp.user!.username}`); + } + + return { + hasAccount: false, + instance: params.instance, + ticket: resp.ticket!, + remoteUser: resp.remote_username!, + }; +}; + +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; + + if (!username || !ticket) + return { + error: { message: "Bad request", code: ErrorCode.BadRequest, status: 403 } as RawApiError, + }; + + try { + const resp = await apiRequest("POST", "/auth/fediverse/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/mastodon/[instance]/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte new file mode 100644 index 0000000..c68235f --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte @@ -0,0 +1,35 @@ + + + + {$t("auth.register-with-mastodon")} • pronouns.cc + + +
    +

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

    + + {#if form?.error} + + {/if} + +
    +
    + + +
    +
    + + +
    + + +
    +
    diff --git a/Foxnouns.Frontend/src/routes/auth/log-in/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/log-in/+page.server.ts new file mode 100644 index 0000000..6b4dfa6 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/log-in/+page.server.ts @@ -0,0 +1,79 @@ +import { isRedirect, redirect } from "@sveltejs/kit"; + +import { apiRequest } from "$api"; +import type { AuthResponse, AuthUrls } from "$api/models/auth"; +import { setToken } from "$lib"; +import ApiError, { ErrorCode } from "$api/error"; + +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 }); + return { urls }; +}; + +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)}&forceRefresh=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 new file mode 100644 index 0000000..834985d --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte @@ -0,0 +1,88 @@ + + + + {$t("title.log-in")} • pronouns.cc + + +
    +
    + {#if form?.error} + + {/if} +
    +
    + {#if data.urls.email_enabled} +
    +

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

    +
    +
    + + +
    +
    + + +
    + + + + {$t("auth.register-with-email-button")} + + +
    +
    + {:else} +
    + {/if} +
    +

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

    +

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

    +
    +
    + {#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/welcome/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/welcome/+page.server.ts new file mode 100644 index 0000000..88baf97 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/welcome/+page.server.ts @@ -0,0 +1,6 @@ +import { redirect } from "@sveltejs/kit"; + +export const load = async ({ parent }) => { + const { meUser } = await parent(); + if (!meUser) redirect(303, "/auth/log-in"); +}; diff --git a/Foxnouns.Frontend/src/routes/auth/welcome/+page.svelte b/Foxnouns.Frontend/src/routes/auth/welcome/+page.svelte new file mode 100644 index 0000000..4dd7dd4 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/welcome/+page.svelte @@ -0,0 +1,34 @@ + + + + {$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/static/favicon.png b/Foxnouns.Frontend/static/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..825b9e65af7c104cfb07089bb28659393b4f2097 GIT binary patch literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH=16.9.11", "@types/react@^18.2.20": - version "18.3.5" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f" - integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - -"@types/sanitize-html@^2.13.0": - version "2.13.0" - resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e" - integrity sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ== - dependencies: - htmlparser2 "^8.0.0" - -"@types/semver@^7.5.0": - version "7.5.8" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" - integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== - -"@types/send@*": - version "0.17.4" - resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" - integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -"@types/serve-static@*": - version "1.15.7" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" - integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== - dependencies: - "@types/http-errors" "*" - "@types/node" "*" - "@types/send" "*" - -"@types/symlink-or-copy@^1.2.0": - version "1.2.2" - resolved "https://registry.yarnpkg.com/@types/symlink-or-copy/-/symlink-or-copy-1.2.2.tgz#51b1c00b516a5774ada5d611e65eb123f988ef8d" - integrity sha512-MQ1AnmTLOncwEf9IVU+B2e4Hchrku5N67NkgcAHW0p3sdzPe0FNMANxEm6OJUzPniEQGkeT3OROLlCwZJLWFZA== - -"@types/unist@^2", "@types/unist@^2.0.0": - version "2.0.11" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.11.tgz#11af57b127e32487774841f7a4e54eab166d03c4" - integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA== - -"@types/warning@^3.0.0": - version "3.0.3" - resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.3.tgz#d1884c8cc4a426d1ac117ca2611bf333834c6798" - integrity sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q== - -"@typescript-eslint/eslint-plugin@^6.7.4": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz#30830c1ca81fd5f3c2714e524c4303e0194f9cd3" - integrity sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/type-utils" "6.21.0" - "@typescript-eslint/utils" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - graphemer "^1.4.0" - ignore "^5.2.4" - natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/parser@^6.7.4": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" - integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== - dependencies: - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - -"@typescript-eslint/scope-manager@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" - integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - -"@typescript-eslint/type-utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz#6473281cfed4dacabe8004e8521cee0bd9d4c01e" - integrity sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag== - dependencies: - "@typescript-eslint/typescript-estree" "6.21.0" - "@typescript-eslint/utils" "6.21.0" - debug "^4.3.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/types@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" - integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== - -"@typescript-eslint/typescript-estree@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" - integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== - dependencies: - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/visitor-keys" "6.21.0" - debug "^4.3.4" - globby "^11.1.0" - is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" - -"@typescript-eslint/utils@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" - integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== - dependencies: - "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "6.21.0" - "@typescript-eslint/types" "6.21.0" - "@typescript-eslint/typescript-estree" "6.21.0" - semver "^7.5.4" - -"@typescript-eslint/visitor-keys@6.21.0": - version "6.21.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" - integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== - dependencies: - "@typescript-eslint/types" "6.21.0" - eslint-visitor-keys "^3.4.1" - -"@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - -"@vanilla-extract/babel-plugin-debug-ids@^1.0.4": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.6.tgz#e9033b5fb97c1b13066cec701f42e753373c2516" - integrity sha512-C188vUEYmw41yxg3QooTs8r1IdbDQQ2mH7L5RkORBnHx74QlmsNfqVmKwAVTgrlYt8JoRaWMtPfGm/Ql0BNQrA== - dependencies: - "@babel/core" "^7.23.9" - -"@vanilla-extract/css@^1.14.0": - version "1.15.5" - resolved "https://registry.yarnpkg.com/@vanilla-extract/css/-/css-1.15.5.tgz#06782b98b4d1478baec578fb06c223bde589d4b3" - integrity sha512-N1nQebRWnXvlcmu9fXKVUs145EVwmWtMD95bpiEKtvehHDpUhmO1l2bauS7FGYKbi3dU1IurJbGpQhBclTr1ng== - dependencies: - "@emotion/hash" "^0.9.0" - "@vanilla-extract/private" "^1.0.6" - css-what "^6.1.0" - cssesc "^3.0.0" - csstype "^3.0.7" - dedent "^1.5.3" - deep-object-diff "^1.1.9" - deepmerge "^4.2.2" - lru-cache "^10.4.3" - media-query-parser "^2.0.2" - modern-ahocorasick "^1.0.0" - picocolors "^1.0.0" - -"@vanilla-extract/integration@^6.2.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@vanilla-extract/integration/-/integration-6.5.0.tgz#613407565b07dc60b123ca9080ea3f47cd2ce7bb" - integrity sha512-E2YcfO8vA+vs+ua+gpvy1HRqvgWbI+MTlUpxA8FvatOvybuNcWAY0CKwQ/Gpj7rswYKtC6C7+xw33emM6/ImdQ== - dependencies: - "@babel/core" "^7.20.7" - "@babel/plugin-syntax-typescript" "^7.20.0" - "@vanilla-extract/babel-plugin-debug-ids" "^1.0.4" - "@vanilla-extract/css" "^1.14.0" - esbuild "npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0" - eval "0.1.8" - find-up "^5.0.0" - javascript-stringify "^2.0.1" - lodash "^4.17.21" - mlly "^1.4.2" - outdent "^0.8.0" - vite "^5.0.11" - vite-node "^1.2.0" - -"@vanilla-extract/private@^1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@vanilla-extract/private/-/private-1.0.6.tgz#f10bbf3189f7b827d0bd7f804a6219dd03ddbdd4" - integrity sha512-ytsG/JLweEjw7DBuZ/0JCN4WAQgM9erfSTdS1NQY778hFQSZ6cfCDEZZ0sgVm4k54uNz6ImKB33AYvSR//fjxw== - -"@web3-storage/multipart-parser@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@web3-storage/multipart-parser/-/multipart-parser-1.0.0.tgz#6b69dc2a32a5b207ba43e556c25cc136a56659c4" - integrity sha512-BEO6al7BYqcnfX15W2cnGR+Q566ACXAT9UQykORCWW80lmkpWsnEob6zJS1ZVBKsSJC8+7vJkHwlp+lXG1UCdw== - -"@zxing/text-encoding@0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b" - integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -accepts@~1.3.5, accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -acorn-jsx@^5.0.0, acorn-jsx@^5.3.2: - version "5.3.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" - integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== - -acorn@^8.0.0, acorn@^8.11.3, acorn@^8.9.0: - version "8.12.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" - integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== - -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - -ajv@^6.12.4: - version "6.12.6" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" - integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== - 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" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -anymatch@^3.1.3, anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -arg@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -aria-query@~5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" - integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ== - dependencies: - deep-equal "^2.0.5" - -array-buffer-byte-length@^1.0.0, array-buffer-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" - integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== - dependencies: - call-bind "^1.0.5" - is-array-buffer "^3.0.4" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== - -array-includes@^3.1.6, array-includes@^3.1.8: - version "3.1.8" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" - integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - is-string "^1.0.7" - -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - -array.prototype.findlast@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" - integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-shim-unscopables "^1.0.2" - -array.prototype.findlastindex@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" - integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-shim-unscopables "^1.0.2" - -array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" - integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.flatmap@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" - integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - es-shim-unscopables "^1.0.0" - -array.prototype.tosorted@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" - integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.3" - es-errors "^1.3.0" - es-shim-unscopables "^1.0.2" - -arraybuffer.prototype.slice@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" - integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== - dependencies: - array-buffer-byte-length "^1.0.1" - call-bind "^1.0.5" - define-properties "^1.2.1" - es-abstract "^1.22.3" - es-errors "^1.2.1" - get-intrinsic "^1.2.3" - is-array-buffer "^3.0.4" - is-shared-array-buffer "^1.0.2" - -ast-types-flow@^0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" - integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== - -astring@^1.8.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/astring/-/astring-1.9.0.tgz#cc73e6062a7eb03e7d19c22d8b0b3451fd9bfeef" - integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg== - -available-typed-arrays@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" - integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== - dependencies: - possible-typed-array-names "^1.0.0" - -axe-core@^4.10.0: - version "4.10.0" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.10.0.tgz#d9e56ab0147278272739a000880196cdfe113b59" - integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== - -axobject-query@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-4.1.0.tgz#28768c76d0e3cff21bc62a9e2d0b6ac30042a1ee" - integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== - -b4a@^1.6.4: - version "1.6.6" - resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.6.tgz#a4cc349a3851987c3c4ac2d7785c18744f6da9ba" - integrity sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg== - -bail@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" - integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -bare-events@^2.2.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.4.2.tgz#3140cca7a0e11d49b3edc5041ab560659fd8e1f8" - integrity sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -basic-auth@~2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" - integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== - dependencies: - safe-buffer "5.1.2" - -binary-extensions@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" - integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== - -bl@^4.0.3, bl@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bl@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" - integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== - dependencies: - buffer "^6.0.3" - inherits "^2.0.4" - readable-stream "^3.4.0" - -body-parser@1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -bootstrap@^5.3.3: - version "5.3.3" - resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.3.tgz#de35e1a765c897ac940021900fcbb831602bac38" - integrity sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.3, braces@~3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" - integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== - dependencies: - fill-range "^7.1.1" - -broccoli-node-api@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/broccoli-node-api/-/broccoli-node-api-1.7.0.tgz#391aa6edecd2a42c63c111b4162956b2fa288cb6" - integrity sha512-QIqLSVJWJUVOhclmkmypJJH9u9s/aWH4+FH6Q6Ju5l+Io4dtwqdPUNmDfw40o6sxhbZHhqGujDJuHTML1wG8Yw== - -broccoli-node-info@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/broccoli-node-info/-/broccoli-node-info-2.2.0.tgz#feb01c13020792f429e01d7f7845dc5b3a7932b3" - integrity sha512-VabSGRpKIzpmC+r+tJueCE5h8k6vON7EIMMWu6d/FyPdtijwLQ7QvzShEw+m3mHoDzUaj/kiZsDYrS8X2adsBg== - -broccoli-output-wrapper@^3.2.5: - version "3.2.5" - resolved "https://registry.yarnpkg.com/broccoli-output-wrapper/-/broccoli-output-wrapper-3.2.5.tgz#514b17801c92922a2c2f87fd145df2a25a11bc5f" - integrity sha512-bQAtwjSrF4Nu0CK0JOy5OZqw9t5U0zzv2555EA/cF8/a8SLDTIetk9UgrtMVw7qKLKdSpOZ2liZNeZZDaKgayw== - dependencies: - fs-extra "^8.1.0" - heimdalljs-logger "^0.1.10" - symlink-or-copy "^1.2.0" - -broccoli-plugin@^4.0.7: - version "4.0.7" - resolved "https://registry.yarnpkg.com/broccoli-plugin/-/broccoli-plugin-4.0.7.tgz#dd176a85efe915ed557d913744b181abe05047db" - integrity sha512-a4zUsWtA1uns1K7p9rExYVYG99rdKeGRymW0qOCNkvDPHQxVi3yVyJHhQbM3EZwdt2E0mnhr5e0c/bPpJ7p3Wg== - dependencies: - broccoli-node-api "^1.7.0" - broccoli-output-wrapper "^3.2.5" - fs-merger "^3.2.1" - promise-map-series "^0.3.0" - quick-temp "^0.1.8" - rimraf "^3.0.2" - symlink-or-copy "^1.3.1" - -browserify-zlib@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.1.4.tgz#bb35f8a519f600e0fa6b8485241c979d0141fb2d" - integrity sha512-19OEpq7vWgsH6WkvkBJQDFvJS1uPcbFOQ4v9CU839dO+ZZXUZO6XpE6hNCqvlIIj+4fZvRiJ6DsAQ382GwiyTQ== - dependencies: - pako "~0.2.0" - -browserslist@^4.23.1: - version "4.23.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" - integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== - dependencies: - caniuse-lite "^1.0.30001646" - electron-to-chromium "^1.5.4" - node-releases "^2.0.18" - update-browserslist-db "^1.1.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" - integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cac@^6.7.14: - version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" - integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== - -cacache@^17.1.3: - version "17.1.4" - resolved "https://registry.yarnpkg.com/cacache/-/cacache-17.1.4.tgz#b3ff381580b47e85c6e64f801101508e26604b35" - integrity sha512-/aJwG2l3ZMJ1xNAnqbMpA40of9dj/pIH3QfiuQSqjfPJF747VR0J/bHn+/KdNnHKc6XQcWt/AfRSBft82W1d2A== - dependencies: - "@npmcli/fs" "^3.1.0" - fs-minipass "^3.0.0" - glob "^10.2.2" - lru-cache "^7.7.1" - minipass "^7.0.3" - minipass-collect "^1.0.2" - minipass-flush "^1.0.5" - minipass-pipeline "^1.2.4" - p-map "^4.0.0" - ssri "^10.0.0" - tar "^6.1.11" - unique-filename "^3.0.0" - -call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" - integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - set-function-length "^1.2.1" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -caniuse-lite@^1.0.30001646: - version "1.0.30001655" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001655.tgz#0ce881f5a19a2dcfda2ecd927df4d5c1684b982f" - integrity sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg== - -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -change-case@^5.4.4: - version "5.4.4" - resolved "https://registry.yarnpkg.com/change-case/-/change-case-5.4.4.tgz#0d52b507d8fb8f204343432381d1a6d7bff97a02" - integrity sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w== - -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - -character-entities@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" - integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== - -character-reference-invalid@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" - integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== - -cheerio-select@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cheerio-select/-/cheerio-select-2.1.0.tgz#4d8673286b8126ca2a8e42740d5e3c4884ae21b4" - integrity sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g== - dependencies: - boolbase "^1.0.0" - css-select "^5.1.0" - css-what "^6.1.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - -cheerio@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0.tgz#1ede4895a82f26e8af71009f961a9b8cb60d6a81" - integrity sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww== - dependencies: - cheerio-select "^2.1.0" - dom-serializer "^2.0.0" - domhandler "^5.0.3" - domutils "^3.1.0" - encoding-sniffer "^0.2.0" - htmlparser2 "^9.1.0" - parse5 "^7.1.2" - parse5-htmlparser2-tree-adapter "^7.0.0" - parse5-parser-stream "^7.1.2" - undici "^6.19.5" - whatwg-mimetype "^4.0.0" - -"chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.1, chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - 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" - optionalDependencies: - fsevents "~2.3.2" - -chownr@^1.1.1: - version "1.1.4" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" - integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -classnames@^2.3.2, classnames@^2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" - integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== - -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - -cli-spinners@^2.5.0: - version "2.9.2" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" - integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== - -clone-stats@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680" - integrity sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag== - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - -clone@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f" - integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -colors@1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" - integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== - -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - -commander@~12.1.0: - version "12.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-12.1.0.tgz#01423b36f501259fdaac4d0e4d60c96c991585d3" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - -compressible@~2.0.16: - version "2.0.18" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" - integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== - dependencies: - mime-db ">= 1.43.0 < 2" - -compression@^1.7.4: - version "1.7.4" - resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" - integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== - dependencies: - accepts "~1.3.5" - bytes "3.0.0" - compressible "~2.0.16" - debug "2.6.9" - on-headers "~1.0.2" - safe-buffer "5.1.2" - vary "~1.1.2" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -confbox@^0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.7.tgz#ccfc0a2bcae36a84838e83a3b7f770fb17d6c579" - integrity sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA== - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4, content-type@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" - integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== - -cookie-signature@^1.1.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.1.tgz#790dea2cce64638c7ae04d9fabed193bd7ccf3b4" - integrity sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw== - -cookie@0.6.0, cookie@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" - integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - -cross-fetch@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" - integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== - dependencies: - node-fetch "^2.6.12" - -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -css-select@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-5.1.0.tgz#b8ebd6554c3637ccc76688804ad3f6a6fdaea8a6" - integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== - dependencies: - boolbase "^1.0.0" - css-what "^6.1.0" - domhandler "^5.0.2" - domutils "^3.0.1" - nth-check "^2.0.1" - -css-what@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -csstype@^3.0.2, csstype@^3.0.7: - version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== - -damerau-levenshtein@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" - integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== - -data-uri-to-buffer@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz#594b8973938c5bc2c33046535785341abc4f3636" - integrity sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og== - -data-view-buffer@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" - integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" - integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -data-view-byte-offset@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" - integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-data-view "^1.0.1" - -debug@2.6.9, debug@^2.2.0: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: - version "4.3.6" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" - integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== - dependencies: - ms "2.1.2" - -decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== - dependencies: - character-entities "^2.0.0" - -dedent@^1.5.3: - version "1.5.3" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" - integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== - -deep-equal@^2.0.5: - version "2.2.3" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.2.3.tgz#af89dafb23a396c7da3e862abc0be27cf51d56e1" - integrity sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA== - dependencies: - array-buffer-byte-length "^1.0.0" - call-bind "^1.0.5" - es-get-iterator "^1.1.3" - get-intrinsic "^1.2.2" - is-arguments "^1.1.1" - is-array-buffer "^3.0.2" - is-date-object "^1.0.5" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.2" - isarray "^2.0.5" - object-is "^1.1.5" - object-keys "^1.1.1" - object.assign "^4.1.4" - regexp.prototype.flags "^1.5.1" - side-channel "^1.0.4" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.1" - which-typed-array "^1.1.13" - -deep-is@^0.1.3: - version "0.1.4" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" - integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== - -deep-object-diff@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/deep-object-diff/-/deep-object-diff-1.1.9.tgz#6df7ef035ad6a0caa44479c536ed7b02570f4595" - integrity sha512-Rn+RuwkmkDwCi2/oXOFS9Gsr5lJZu/yTGpK7wAaAIE75CC+LCGEZHpY6VQJa/RoJcrmaA/docWJZvYohlNkWPA== - -deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - -defaults@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" - integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== - dependencies: - clone "^1.0.2" - -define-data-property@^1.0.1, define-data-property@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" - integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== - dependencies: - es-define-property "^1.0.0" - es-errors "^1.3.0" - gopd "^1.0.1" - -define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" - integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== - dependencies: - define-data-property "^1.0.1" - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -depd@2.0.0, depd@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -dequal@^2.0.0, dequal@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -diff@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -doctrine@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" - integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== - dependencies: - esutils "^2.0.2" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -dom-helpers@^5.0.1, dom-helpers@^5.2.0, dom-helpers@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902" - integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== - dependencies: - "@babel/runtime" "^7.8.7" - csstype "^3.0.2" - -dom-serializer@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-2.0.0.tgz#e41b802e1eedf9f6cae183ce5e622d789d7d8e53" - integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.2" - entities "^4.2.0" - -domelementtype@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^5.0.2, domhandler@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-5.0.3.tgz#cc385f7f751f1d1fc650c21374804254538c7d31" - integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== - dependencies: - domelementtype "^2.3.0" - -domutils@^3.0.1, domutils@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-3.1.0.tgz#c47f551278d3dc4b0b1ab8cbb42d751a6f0d824e" - integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== - dependencies: - dom-serializer "^2.0.0" - domelementtype "^2.3.0" - domhandler "^5.0.3" - -dotenv@^16.0.0, dotenv@^16.4.5: - version "16.4.5" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" - integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== - -duplexify@^3.5.0, duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" - integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== - dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" - stream-shift "^1.0.0" - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.5.4: - version "1.5.13" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" - integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -encoding-sniffer@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz#799569d66d443babe82af18c9f403498365ef1d5" - integrity sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg== - dependencies: - iconv-lite "^0.6.3" - whatwg-encoding "^3.1.1" - -end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -enhanced-resolve@^5.15.0: - version "5.17.1" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" - integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -ensure-posix-path@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ensure-posix-path/-/ensure-posix-path-1.1.1.tgz#3c62bdb19fa4681544289edb2b382adc029179ce" - integrity sha512-VWU0/zXzVbeJNXvME/5EmLuEj2TauvoaTz6aFYK1Z92JCBlDlZ3Gu0tuGR42kpW1754ywTs+QB0g5TP0oj9Zaw== - -entities@^4.2.0, entities@^4.4.0, entities@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== - -eol@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd" - integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg== - -err-code@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" - integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== - -es-abstract@^1.17.5, es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: - version "1.23.3" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" - integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== - dependencies: - array-buffer-byte-length "^1.0.1" - arraybuffer.prototype.slice "^1.0.3" - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - data-view-buffer "^1.0.1" - data-view-byte-length "^1.0.1" - data-view-byte-offset "^1.0.0" - es-define-property "^1.0.0" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - es-set-tostringtag "^2.0.3" - es-to-primitive "^1.2.1" - function.prototype.name "^1.1.6" - get-intrinsic "^1.2.4" - get-symbol-description "^1.0.2" - globalthis "^1.0.3" - gopd "^1.0.1" - has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - hasown "^2.0.2" - internal-slot "^1.0.7" - is-array-buffer "^3.0.4" - is-callable "^1.2.7" - is-data-view "^1.0.1" - is-negative-zero "^2.0.3" - is-regex "^1.1.4" - is-shared-array-buffer "^1.0.3" - is-string "^1.0.7" - is-typed-array "^1.1.13" - is-weakref "^1.0.2" - object-inspect "^1.13.1" - object-keys "^1.1.1" - object.assign "^4.1.5" - regexp.prototype.flags "^1.5.2" - safe-array-concat "^1.1.2" - safe-regex-test "^1.0.3" - string.prototype.trim "^1.2.9" - string.prototype.trimend "^1.0.8" - string.prototype.trimstart "^1.0.8" - typed-array-buffer "^1.0.2" - typed-array-byte-length "^1.0.1" - typed-array-byte-offset "^1.0.2" - typed-array-length "^1.0.6" - unbox-primitive "^1.0.2" - which-typed-array "^1.1.15" - -es-define-property@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" - integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== - dependencies: - get-intrinsic "^1.2.4" - -es-errors@^1.2.1, es-errors@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" - integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== - -es-get-iterator@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.3.tgz#3ef87523c5d464d41084b2c3c9c214f1199763d6" - integrity sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.1.3" - has-symbols "^1.0.3" - is-arguments "^1.1.1" - is-map "^2.0.2" - is-set "^2.0.2" - is-string "^1.0.7" - isarray "^2.0.5" - stop-iteration-iterator "^1.0.0" - -es-iterator-helpers@^1.0.19: - version "1.0.19" - resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" - integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.3" - es-errors "^1.3.0" - es-set-tostringtag "^2.0.3" - function-bind "^1.1.2" - get-intrinsic "^1.2.4" - globalthis "^1.0.3" - has-property-descriptors "^1.0.2" - has-proto "^1.0.3" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - iterator.prototype "^1.1.2" - safe-array-concat "^1.1.2" - -es-module-lexer@^1.3.1: - version "1.5.4" - resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" - integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== - -es-object-atoms@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" - integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== - dependencies: - es-errors "^1.3.0" - -es-set-tostringtag@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" - integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== - dependencies: - get-intrinsic "^1.2.4" - has-tostringtag "^1.0.2" - hasown "^2.0.1" - -es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" - integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== - dependencies: - hasown "^2.0.0" - -es-to-primitive@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" - integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -esbuild-plugins-node-modules-polyfill@^1.6.0: - version "1.6.6" - resolved "https://registry.yarnpkg.com/esbuild-plugins-node-modules-polyfill/-/esbuild-plugins-node-modules-polyfill-1.6.6.tgz#acdfbd32443a1667a029b930b15a5ae767a7ed25" - integrity sha512-0wDvliv65SCaaGtmoITnmXqqiUzU+ggFupnOgkEo2B9cQ+CUt58ql2+EY6dYoEsoqiHRu2NuTrFUJGMJEgMmLw== - dependencies: - "@jspm/core" "^2.0.1" - local-pkg "^0.5.0" - resolve.exports "^2.0.2" - -esbuild@0.17.6: - version "0.17.6" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.6.tgz#bbccd4433629deb6e0a83860b3b61da120ba4e01" - integrity sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q== - optionalDependencies: - "@esbuild/android-arm" "0.17.6" - "@esbuild/android-arm64" "0.17.6" - "@esbuild/android-x64" "0.17.6" - "@esbuild/darwin-arm64" "0.17.6" - "@esbuild/darwin-x64" "0.17.6" - "@esbuild/freebsd-arm64" "0.17.6" - "@esbuild/freebsd-x64" "0.17.6" - "@esbuild/linux-arm" "0.17.6" - "@esbuild/linux-arm64" "0.17.6" - "@esbuild/linux-ia32" "0.17.6" - "@esbuild/linux-loong64" "0.17.6" - "@esbuild/linux-mips64el" "0.17.6" - "@esbuild/linux-ppc64" "0.17.6" - "@esbuild/linux-riscv64" "0.17.6" - "@esbuild/linux-s390x" "0.17.6" - "@esbuild/linux-x64" "0.17.6" - "@esbuild/netbsd-x64" "0.17.6" - "@esbuild/openbsd-x64" "0.17.6" - "@esbuild/sunos-x64" "0.17.6" - "@esbuild/win32-arm64" "0.17.6" - "@esbuild/win32-ia32" "0.17.6" - "@esbuild/win32-x64" "0.17.6" - -esbuild@^0.21.3: - version "0.21.5" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" - integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== - optionalDependencies: - "@esbuild/aix-ppc64" "0.21.5" - "@esbuild/android-arm" "0.21.5" - "@esbuild/android-arm64" "0.21.5" - "@esbuild/android-x64" "0.21.5" - "@esbuild/darwin-arm64" "0.21.5" - "@esbuild/darwin-x64" "0.21.5" - "@esbuild/freebsd-arm64" "0.21.5" - "@esbuild/freebsd-x64" "0.21.5" - "@esbuild/linux-arm" "0.21.5" - "@esbuild/linux-arm64" "0.21.5" - "@esbuild/linux-ia32" "0.21.5" - "@esbuild/linux-loong64" "0.21.5" - "@esbuild/linux-mips64el" "0.21.5" - "@esbuild/linux-ppc64" "0.21.5" - "@esbuild/linux-riscv64" "0.21.5" - "@esbuild/linux-s390x" "0.21.5" - "@esbuild/linux-x64" "0.21.5" - "@esbuild/netbsd-x64" "0.21.5" - "@esbuild/openbsd-x64" "0.21.5" - "@esbuild/sunos-x64" "0.21.5" - "@esbuild/win32-arm64" "0.21.5" - "@esbuild/win32-ia32" "0.21.5" - "@esbuild/win32-x64" "0.21.5" - -esbuild@^0.23.0: - version "0.23.1" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.23.1.tgz#40fdc3f9265ec0beae6f59824ade1bd3d3d2dab8" - integrity sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.23.1" - "@esbuild/android-arm" "0.23.1" - "@esbuild/android-arm64" "0.23.1" - "@esbuild/android-x64" "0.23.1" - "@esbuild/darwin-arm64" "0.23.1" - "@esbuild/darwin-x64" "0.23.1" - "@esbuild/freebsd-arm64" "0.23.1" - "@esbuild/freebsd-x64" "0.23.1" - "@esbuild/linux-arm" "0.23.1" - "@esbuild/linux-arm64" "0.23.1" - "@esbuild/linux-ia32" "0.23.1" - "@esbuild/linux-loong64" "0.23.1" - "@esbuild/linux-mips64el" "0.23.1" - "@esbuild/linux-ppc64" "0.23.1" - "@esbuild/linux-riscv64" "0.23.1" - "@esbuild/linux-s390x" "0.23.1" - "@esbuild/linux-x64" "0.23.1" - "@esbuild/netbsd-x64" "0.23.1" - "@esbuild/openbsd-arm64" "0.23.1" - "@esbuild/openbsd-x64" "0.23.1" - "@esbuild/sunos-x64" "0.23.1" - "@esbuild/win32-arm64" "0.23.1" - "@esbuild/win32-ia32" "0.23.1" - "@esbuild/win32-x64" "0.23.1" - -"esbuild@npm:esbuild@~0.17.6 || ~0.18.0 || ~0.19.0": - version "0.19.12" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.12.tgz#dc82ee5dc79e82f5a5c3b4323a2a641827db3e04" - integrity sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg== - optionalDependencies: - "@esbuild/aix-ppc64" "0.19.12" - "@esbuild/android-arm" "0.19.12" - "@esbuild/android-arm64" "0.19.12" - "@esbuild/android-x64" "0.19.12" - "@esbuild/darwin-arm64" "0.19.12" - "@esbuild/darwin-x64" "0.19.12" - "@esbuild/freebsd-arm64" "0.19.12" - "@esbuild/freebsd-x64" "0.19.12" - "@esbuild/linux-arm" "0.19.12" - "@esbuild/linux-arm64" "0.19.12" - "@esbuild/linux-ia32" "0.19.12" - "@esbuild/linux-loong64" "0.19.12" - "@esbuild/linux-mips64el" "0.19.12" - "@esbuild/linux-ppc64" "0.19.12" - "@esbuild/linux-riscv64" "0.19.12" - "@esbuild/linux-s390x" "0.19.12" - "@esbuild/linux-x64" "0.19.12" - "@esbuild/netbsd-x64" "0.19.12" - "@esbuild/openbsd-x64" "0.19.12" - "@esbuild/sunos-x64" "0.19.12" - "@esbuild/win32-arm64" "0.19.12" - "@esbuild/win32-ia32" "0.19.12" - "@esbuild/win32-x64" "0.19.12" - -escalade@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" - integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== - -eslint-import-resolver-node@^0.3.9: - version "0.3.9" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" - integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== - dependencies: - debug "^3.2.7" - is-core-module "^2.13.0" - resolve "^1.22.4" - -eslint-import-resolver-typescript@^3.6.1: - version "3.6.3" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz#bb8e388f6afc0f940ce5d2c5fd4a3d147f038d9e" - integrity sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA== - dependencies: - "@nolyfill/is-core-module" "1.0.39" - debug "^4.3.5" - enhanced-resolve "^5.15.0" - eslint-module-utils "^2.8.1" - fast-glob "^3.3.2" - get-tsconfig "^4.7.5" - is-bun-module "^1.0.2" - is-glob "^4.0.3" - -eslint-module-utils@^2.8.1, eslint-module-utils@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.9.0.tgz#95d4ac038a68cd3f63482659dffe0883900eb342" - integrity sha512-McVbYmwA3NEKwRQY5g4aWMdcZE5xZxV8i8l7CqJSrameuGSQJtSWaL/LxTEzSKKaCcOhlpDR8XEfYXWPrdo/ZQ== - dependencies: - debug "^3.2.7" - -eslint-plugin-import@^2.28.1: - version "2.30.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz#21ceea0fc462657195989dd780e50c92fe95f449" - integrity sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw== - dependencies: - "@rtsao/scc" "^1.1.0" - array-includes "^3.1.8" - array.prototype.findlastindex "^1.2.5" - array.prototype.flat "^1.3.2" - array.prototype.flatmap "^1.3.2" - debug "^3.2.7" - doctrine "^2.1.0" - eslint-import-resolver-node "^0.3.9" - eslint-module-utils "^2.9.0" - hasown "^2.0.2" - is-core-module "^2.15.1" - is-glob "^4.0.3" - minimatch "^3.1.2" - object.fromentries "^2.0.8" - object.groupby "^1.0.3" - object.values "^1.2.0" - semver "^6.3.1" - tsconfig-paths "^3.15.0" - -eslint-plugin-jsx-a11y@^6.7.1: - version "6.10.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz#36fb9dead91cafd085ddbe3829602fb10ef28339" - integrity sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg== - dependencies: - aria-query "~5.1.3" - array-includes "^3.1.8" - array.prototype.flatmap "^1.3.2" - ast-types-flow "^0.0.8" - axe-core "^4.10.0" - axobject-query "^4.1.0" - damerau-levenshtein "^1.0.8" - emoji-regex "^9.2.2" - es-iterator-helpers "^1.0.19" - hasown "^2.0.2" - jsx-ast-utils "^3.3.5" - language-tags "^1.0.9" - minimatch "^3.1.2" - object.fromentries "^2.0.8" - safe-regex-test "^1.0.3" - string.prototype.includes "^2.0.0" - -eslint-plugin-react-hooks@^4.6.0: - version "4.6.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" - integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== - -eslint-plugin-react@^7.33.2: - version "7.35.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz#d32500d3ec268656d5071918bfec78cfd8b070ed" - integrity sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ== - dependencies: - array-includes "^3.1.8" - array.prototype.findlast "^1.2.5" - array.prototype.flatmap "^1.3.2" - array.prototype.tosorted "^1.1.4" - doctrine "^2.1.0" - es-iterator-helpers "^1.0.19" - estraverse "^5.3.0" - hasown "^2.0.2" - jsx-ast-utils "^2.4.1 || ^3.0.0" - minimatch "^3.1.2" - object.entries "^1.1.8" - object.fromentries "^2.0.8" - object.values "^1.2.0" - prop-types "^15.8.1" - resolve "^2.0.0-next.5" - semver "^6.3.1" - string.prototype.matchall "^4.0.11" - string.prototype.repeat "^1.0.0" - -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== - dependencies: - esrecurse "^4.3.0" - estraverse "^5.2.0" - -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: - version "3.4.3" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" - integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== - -eslint@^8.38.0: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== - dependencies: - "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" - "@humanwhocodes/module-importer" "^1.0.1" - "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" - ajv "^6.12.4" - chalk "^4.0.0" - cross-spawn "^7.0.2" - debug "^4.3.2" - doctrine "^3.0.0" - escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" - esquery "^1.4.2" - esutils "^2.0.2" - fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" - find-up "^5.0.0" - glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" - ignore "^5.2.0" - imurmurhash "^0.1.4" - is-glob "^4.0.0" - is-path-inside "^3.0.3" - js-yaml "^4.1.0" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.4.1" - lodash.merge "^4.6.2" - minimatch "^3.1.2" - natural-compare "^1.4.0" - optionator "^0.9.3" - strip-ansi "^6.0.1" - text-table "^0.2.0" - -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== - dependencies: - acorn "^8.9.0" - acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" - -esquery@^1.4.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" - integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== - dependencies: - estraverse "^5.1.0" - -esrecurse@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" - integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== - dependencies: - estraverse "^5.2.0" - -estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" - integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== - -estree-util-attach-comments@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/estree-util-attach-comments/-/estree-util-attach-comments-2.1.1.tgz#ee44f4ff6890ee7dfb3237ac7810154c94c63f84" - integrity sha512-+5Ba/xGGS6mnwFbXIuQiDPTbuTxuMCooq3arVv7gPZtYpjp+VXH/NkHAP35OOefPhNG/UGqU3vt/LTABwcHX0w== - dependencies: - "@types/estree" "^1.0.0" - -estree-util-build-jsx@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/estree-util-build-jsx/-/estree-util-build-jsx-2.2.2.tgz#32f8a239fb40dc3f3dca75bb5dcf77a831e4e47b" - integrity sha512-m56vOXcOBuaF+Igpb9OPAy7f9w9OIkb5yhjsZuaPm7HoGi4oTOQi0h2+yZ+AtKklYFZ+rPC4n0wYCJCEU1ONqg== - dependencies: - "@types/estree-jsx" "^1.0.0" - estree-util-is-identifier-name "^2.0.0" - estree-walker "^3.0.0" - -estree-util-is-identifier-name@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-1.1.0.tgz#2e3488ea06d9ea2face116058864f6370b37456d" - integrity sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ== - -estree-util-is-identifier-name@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-2.1.0.tgz#fb70a432dcb19045e77b05c8e732f1364b4b49b2" - integrity sha512-bEN9VHRyXAUOjkKVQVvArFym08BTWB0aJPppZZr0UNyAqWsLaVfAqP7hbaTJjzHifmB5ebnR8Wm7r7yGN/HonQ== - -estree-util-to-js@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/estree-util-to-js/-/estree-util-to-js-1.2.0.tgz#0f80d42443e3b13bd32f7012fffa6f93603f4a36" - integrity sha512-IzU74r1PK5IMMGZXUVZbmiu4A1uhiPgW5hm1GjcOfr4ZzHaMPpLNJjR7HjXiIOzi25nZDrgFTobHTkV5Q6ITjA== - dependencies: - "@types/estree-jsx" "^1.0.0" - astring "^1.8.0" - source-map "^0.7.0" - -estree-util-value-to-estree@^1.0.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/estree-util-value-to-estree/-/estree-util-value-to-estree-1.3.0.tgz#1d3125594b4d6680f666644491e7ac1745a3df49" - integrity sha512-Y+ughcF9jSUJvncXwqRageavjrNPAI+1M/L3BI3PyLp1nmgYTGUXU6t5z1Y7OWuThoDdhPME07bQU+d5LxdJqw== - dependencies: - is-plain-obj "^3.0.0" - -estree-util-visit@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/estree-util-visit/-/estree-util-visit-1.2.1.tgz#8bc2bc09f25b00827294703835aabee1cc9ec69d" - integrity sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/unist" "^2.0.0" - -estree-walker@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" - integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== - dependencies: - "@types/estree" "^1.0.0" - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eval@0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/eval/-/eval-0.1.8.tgz#2b903473b8cc1d1989b83a1e7923f883eb357f85" - integrity sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw== - dependencies: - "@types/node" "*" - require-like ">= 0.1.1" - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -execa@5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -exit-hook@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-2.2.1.tgz#007b2d92c6428eda2b76e7016a34351586934593" - integrity sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw== - -express@^4.19.2: - version "4.19.2" - resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" - integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.2" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.6.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.11.0" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" - integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== - -fast-fifo@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" - integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== - -fast-glob@^3.2.9, fast-glob@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" - integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== - 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.4" - -fast-json-stable-stringify@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fast-levenshtein@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== - -fastq@^1.13.0, fastq@^1.6.0: - version "1.17.1" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" - integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== - dependencies: - reusify "^1.0.4" - -fault@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fault/-/fault-2.0.1.tgz#d47ca9f37ca26e4bd38374a7c500b5a384755b6c" - integrity sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ== - dependencies: - format "^0.2.0" - -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== - dependencies: - flat-cache "^3.0.4" - -fill-range@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" - integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== - dependencies: - flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" - -flatted@^3.2.9: - version "3.3.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" - integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -foreground-child@^3.1.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" - integrity sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg== - dependencies: - cross-spawn "^7.0.0" - signal-exit "^4.0.1" - -format@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" - integrity sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww== - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^11.1.0: - version "11.2.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" - integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^8.0.1, fs-extra@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" - integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^4.0.0" - universalify "^0.1.0" - -fs-merger@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/fs-merger/-/fs-merger-3.2.1.tgz#a225b11ae530426138294b8fbb19e82e3d4e0b3b" - integrity sha512-AN6sX12liy0JE7C2evclwoo0aCG3PFulLjrTLsJpWh/2mM+DinhpSGqYLbHBBbIW1PLRNcFhJG8Axtz8mQW3ug== - dependencies: - broccoli-node-api "^1.7.0" - broccoli-node-info "^2.1.0" - fs-extra "^8.0.1" - fs-tree-diff "^2.0.1" - walk-sync "^2.2.0" - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs-minipass@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" - integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== - dependencies: - minipass "^7.0.3" - -fs-mkdirp-stream@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fs-mkdirp-stream/-/fs-mkdirp-stream-2.0.1.tgz#1e82575c4023929ad35cf69269f84f1a8c973aa7" - integrity sha512-UTOY+59K6IA94tec8Wjqm0FSh5OVudGNB0NL/P6fB3HiE3bYOY3VYBGijsnOHNkQSwC1FKkU77pmq7xp9CskLw== - dependencies: - graceful-fs "^4.2.8" - streamx "^2.12.0" - -fs-tree-diff@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fs-tree-diff/-/fs-tree-diff-2.0.1.tgz#343e4745ab435ec39ebac5f9059ad919cd034afa" - integrity sha512-x+CfAZ/lJHQqwlD64pYM5QxWjzWhSjroaVsr8PW831zOApL55qPibed0c+xebaLWVr2BnHFoHdrwOv8pzt8R5A== - dependencies: - "@types/symlink-or-copy" "^1.2.0" - heimdalljs-logger "^0.1.7" - object-assign "^4.1.0" - path-posix "^1.0.0" - symlink-or-copy "^1.1.8" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@~2.3.2, fsevents@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" - integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== - -function-bind@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" - integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== - -function.prototype.name@^1.1.6: - version "1.1.6" - resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" - integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== - dependencies: - call-bind "^1.0.2" - define-properties "^1.2.0" - es-abstract "^1.22.1" - functions-have-names "^1.2.3" - -functions-have-names@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" - integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== - -generic-names@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/generic-names/-/generic-names-4.0.0.tgz#0bd8a2fd23fe8ea16cbd0a279acd69c06933d9a3" - integrity sha512-ySFolZQfw9FoDb3ed9d80Cm9f0+r7qj+HJkWjeD9RBfpxEVTlVhol+gvaQB/78WbwYfbnNh8nWHHBSlg072y6A== - dependencies: - loader-utils "^3.2.0" - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" - integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== - dependencies: - es-errors "^1.3.0" - function-bind "^1.1.2" - has-proto "^1.0.1" - has-symbols "^1.0.3" - hasown "^2.0.0" - -get-port@5.1.1, get-port@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" - integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -get-symbol-description@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" - integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== - dependencies: - call-bind "^1.0.5" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - -get-tsconfig@^4.7.5: - version "4.8.0" - resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.8.0.tgz#125dc13a316f61650a12b20c97c11b8fd996fedd" - integrity sha512-Pgba6TExTZ0FJAn1qkJAjIeKoDJ3CsI2ChuLohJnZl/tTU8MVrq3b+2t5UOPfRa4RMsorClBjJALkJUMjG1PAw== - dependencies: - resolve-pkg-maps "^1.0.0" - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob-stream@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-8.0.2.tgz#09e5818e41c16dd85274d72c7a7158d307426313" - integrity sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw== - dependencies: - "@gulpjs/to-absolute-glob" "^4.0.0" - anymatch "^3.1.3" - fastq "^1.13.0" - glob-parent "^6.0.2" - is-glob "^4.0.3" - is-negated-glob "^1.0.0" - normalize-path "^3.0.0" - streamx "^2.12.5" - -glob@^10.2.2: - version "10.4.5" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.5.tgz#f4d9f0b90ffdbab09c9d77f5f29b4262517b0956" - integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg== - dependencies: - foreground-child "^3.1.0" - jackspeak "^3.1.2" - minimatch "^9.0.4" - minipass "^7.1.2" - package-json-from-dist "^1.0.0" - path-scurry "^1.11.1" - -glob@^7.1.3: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" - -globalthis@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" - integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== - dependencies: - define-properties "^1.2.1" - gopd "^1.0.1" - -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - -globrex@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" - integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.10, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.8: - version "4.2.11" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" - integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== - -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - -gulp-sort@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/gulp-sort/-/gulp-sort-2.0.0.tgz#c6762a2f1f0de0a3fc595a21599d3fac8dba1aca" - integrity sha512-MyTel3FXOdh1qhw1yKhpimQrAmur9q1X0ZigLmCOxouQD+BD3za9/89O+HfbgBQvvh4igEbp0/PUWO+VqGYG1g== - dependencies: - through2 "^2.0.1" - -gunzip-maybe@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac" - integrity sha512-4haO1M4mLO91PW57BMsDFf75UmwoRX0GkdD+Faw+Lr+r/OZrOCS0pIBwOL1xCKQqnQzbNFGgK2V2CpBUPeFNTw== - dependencies: - browserify-zlib "^0.1.4" - is-deflate "^1.0.0" - is-gzip "^1.0.0" - peek-stream "^1.1.0" - pumpify "^1.3.3" - through2 "^2.0.3" - -has-bigints@^1.0.1, has-bigints@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" - integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" - integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== - dependencies: - es-define-property "^1.0.0" - -has-proto@^1.0.1, has-proto@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" - integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" - integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== - dependencies: - has-symbols "^1.0.3" - -hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" - integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== - dependencies: - function-bind "^1.1.2" - -hast-util-to-estree@^2.0.0: - version "2.3.3" - resolved "https://registry.yarnpkg.com/hast-util-to-estree/-/hast-util-to-estree-2.3.3.tgz#da60142ffe19a6296923ec222aba73339c8bf470" - integrity sha512-ihhPIUPxN0v0w6M5+IiAZZrn0LH2uZomeWwhn7uP7avZC6TE7lIiEh2yBMPr5+zi1aUCXq6VoYRgs2Bw9xmycQ== - dependencies: - "@types/estree" "^1.0.0" - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" - comma-separated-tokens "^2.0.0" - estree-util-attach-comments "^2.0.0" - estree-util-is-identifier-name "^2.0.0" - hast-util-whitespace "^2.0.0" - mdast-util-mdx-expression "^1.0.0" - mdast-util-mdxjs-esm "^1.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - style-to-object "^0.4.1" - unist-util-position "^4.0.0" - zwitch "^2.0.0" - -hast-util-whitespace@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-2.0.1.tgz#0ec64e257e6fc216c7d14c8a1b74d27d650b4557" - integrity sha512-nAxA0v8+vXSBDt3AnRUNjyRIQ0rD+ntpbAp4LnPkumc5M9yUbSMa4XDU9Q6etY4f1Wp4bNgvc1yjiZtsTTrSng== - -heimdalljs-logger@^0.1.10, heimdalljs-logger@^0.1.7: - version "0.1.10" - resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz#90cad58aabb1590a3c7e640ddc6a4cd3a43faaf7" - integrity sha512-pO++cJbhIufVI/fmB/u2Yty3KJD0TqNPecehFae0/eps0hkZ3b4Zc/PezUMOpYuHFQbA7FxHZxa305EhmjLj4g== - dependencies: - debug "^2.2.0" - heimdalljs "^0.2.6" - -heimdalljs@^0.2.6: - version "0.2.6" - resolved "https://registry.yarnpkg.com/heimdalljs/-/heimdalljs-0.2.6.tgz#b0eebabc412813aeb9542f9cc622cb58dbdcd9fe" - integrity sha512-o9bd30+5vLBvBtzCPwwGqpry2+n0Hi6H1+qwt6y+0kwRHGGF8TFIhJPmnuM0xO97zaKrDZMwO/V56fAnn8m/tA== - dependencies: - rsvp "~3.2.1" - -hosted-git-info@^6.0.0, hosted-git-info@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" - integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== - dependencies: - lru-cache "^7.5.1" - -html-parse-stringify@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" - integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== - dependencies: - void-elements "3.1.0" - -htmlparser2@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21" - integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.0.1" - entities "^4.4.0" - -htmlparser2@^9.1.0: - version "9.1.0" - resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-9.1.0.tgz#cdb498d8a75a51f739b61d3f718136c369bc8c23" - integrity sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ== - dependencies: - domelementtype "^2.3.0" - domhandler "^5.0.3" - domutils "^3.1.0" - entities "^4.5.0" - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -i18next-browser-languagedetector@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.0.0.tgz#b6fdd9b43af67c47f2c26c9ba27710a1eaf31e2f" - integrity sha512-zhXdJXTTCoG39QsrOCiOabnWj2jecouOqbchu3EfhtSHxIB5Uugnm9JaizenOy39h7ne3+fLikIjeW88+rgszw== - dependencies: - "@babel/runtime" "^7.23.2" - -i18next-fs-backend@^2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/i18next-fs-backend/-/i18next-fs-backend-2.3.2.tgz#580b91c9a306b452112e0a1ad3b07e9fd266e567" - integrity sha512-LIwUlkqDZnUI8lnUxBnEj8K/FrHQTT/Sc+1rvDm9E8YvvY5YxzoEAASNx+W5M9DfD5s77lI5vSAFWeTp26B/3Q== - -i18next-http-backend@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.6.1.tgz#186c3a1359e10245c9119a13129f9b5bf328c9a7" - integrity sha512-rCilMAnlEQNeKOZY1+x8wLM5IpYOj10guGvEpeC59tNjj6MMreLIjIW8D1RclhD3ifLwn6d/Y9HEM1RUE6DSog== - dependencies: - cross-fetch "4.0.0" - -i18next-parser@^9.0.2: - version "9.0.2" - resolved "https://registry.yarnpkg.com/i18next-parser/-/i18next-parser-9.0.2.tgz#f9d627422d33c352967556c8724975d58f1f5a95" - integrity sha512-Q1yTZljBp1DcVAQD7LxduEqFRpjIeZc+5VnQ+gU8qG9WvY3U5rqK0IVONRWNtngh3orb197bfy1Sz4wlwcplxg== - dependencies: - "@babel/runtime" "^7.23.2" - broccoli-plugin "^4.0.7" - cheerio "^1.0.0" - colors "1.4.0" - commander "~12.1.0" - eol "^0.9.1" - esbuild "^0.23.0" - fs-extra "^11.1.0" - gulp-sort "^2.0.0" - i18next "^23.5.1" - js-yaml "4.1.0" - lilconfig "^3.0.0" - rsvp "^4.8.2" - sort-keys "^5.0.0" - typescript "^5.0.4" - vinyl "~3.0.0" - vinyl-fs "^4.0.0" - -i18next@^23.15.1, i18next@^23.5.1: - version "23.15.1" - resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.15.1.tgz#c50de337bf12ca5195e697cc0fbe5f32304871d9" - integrity sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA== - dependencies: - "@babel/runtime" "^7.23.2" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@0.6.3, iconv-lite@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -icss-utils@^5.0.0, icss-utils@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" - integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== - -ieee754@^1.1.13, ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^5.2.0, ignore@^5.2.4: - version "5.3.2" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" - integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== - -immutable@^4.0.0: - version "4.3.7" - resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.7.tgz#c70145fc90d89fb02021e65c84eb0226e4e5a381" - integrity sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw== - -import-fresh@^3.2.1: - version "3.3.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" - integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inline-style-parser@0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/inline-style-parser/-/inline-style-parser-0.1.1.tgz#ec8a3b429274e9c0a1f1c4ffa9453a7fef72cea1" - integrity sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q== - -internal-slot@^1.0.4, internal-slot@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" - integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== - dependencies: - es-errors "^1.3.0" - hasown "^2.0.0" - side-channel "^1.0.4" - -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-alphabetical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" - integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== - -is-alphanumerical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" - integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== - dependencies: - is-alphabetical "^2.0.0" - is-decimal "^2.0.0" - -is-arguments@^1.0.4, is-arguments@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-array-buffer@^3.0.2, is-array-buffer@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" - integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== - dependencies: - call-bind "^1.0.2" - get-intrinsic "^1.2.1" - -is-async-function@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" - integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== - dependencies: - has-tostringtag "^1.0.0" - -is-bigint@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" - integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== - dependencies: - has-bigints "^1.0.1" - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-boolean-object@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" - integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - -is-bun-module@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-bun-module/-/is-bun-module-1.1.0.tgz#a66b9830869437f6cdad440ba49ab6e4dc837269" - integrity sha512-4mTAVPlrXpaN3jtF0lsnPCMGnq4+qZjVIKq0HCpfcqf8OC1SM5oATCIAPM5V5FN05qp2NNnFndphmdZS9CV3hA== - dependencies: - semver "^7.6.3" - -is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-core-module@^2.13.0, is-core-module@^2.15.1, is-core-module@^2.8.1: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== - dependencies: - hasown "^2.0.2" - -is-data-view@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" - integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== - dependencies: - is-typed-array "^1.1.13" - -is-date-object@^1.0.1, is-date-object@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" - integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== - dependencies: - has-tostringtag "^1.0.0" - -is-decimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" - integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== - -is-deflate@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-deflate/-/is-deflate-1.0.0.tgz#c862901c3c161fb09dac7cdc7e784f80e98f2f14" - integrity sha512-YDoFpuZWu1VRXlsnlYMzKyVRITXj7Ej/V9gXQ2/pAe7X1J7M/RNOqaIYi6qUn+B7nGyB9pDXrv02dsB58d2ZAQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-finalizationregistry@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" - integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== - dependencies: - call-bind "^1.0.2" - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-function@^1.0.10, is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-gzip@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-gzip/-/is-gzip-1.0.0.tgz#6ca8b07b99c77998025900e555ced8ed80879a83" - integrity sha512-rcfALRIb1YewtnksfRIHGcIY93QnK8BIQ/2c9yDYcG/Y6+vRoJuTWBmmSEbyLLYtXm7q35pHOHbZFQBaLrhlWQ== - -is-hexadecimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" - integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== - -is-interactive@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" - integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== - -is-map@^2.0.2, is-map@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" - integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== - -is-negated-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2" - integrity sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug== - -is-negative-zero@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" - integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== - -is-number-object@^1.0.4: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" - integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== - dependencies: - has-tostringtag "^1.0.0" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-path-inside@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" - integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== - -is-plain-obj@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" - integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== - -is-plain-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-plain-object@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" - integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== - -is-reference@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-3.0.2.tgz#154747a01f45cd962404ee89d43837af2cba247c" - integrity sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg== - dependencies: - "@types/estree" "*" - -is-regex@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" - integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-set@^2.0.2, is-set@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" - integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== - -is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" - integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== - dependencies: - call-bind "^1.0.7" - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-string@^1.0.5, is-string@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" - integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== - dependencies: - has-tostringtag "^1.0.0" - -is-symbol@^1.0.2, is-symbol@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" - integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== - dependencies: - has-symbols "^1.0.2" - -is-typed-array@^1.1.13, is-typed-array@^1.1.3: - version "1.1.13" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" - integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== - dependencies: - which-typed-array "^1.1.14" - -is-unicode-supported@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" - integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== - -is-valid-glob@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa" - integrity sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA== - -is-weakmap@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" - integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== - -is-weakref@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" - integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== - dependencies: - call-bind "^1.0.2" - -is-weakset@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" - integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== - dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - -isarray@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" - integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isbot@^4.1.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/isbot/-/isbot-4.4.0.tgz#897ce9f2e498de6181027660ca80de8734d1ef81" - integrity sha512-8ZvOWUA68kyJO4hHJdWjyreq7TYNWTS9y15IzeqVdKxR9pPr3P/3r9AHcoIv9M0Rllkao5qWz2v1lmcyKIVCzQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -iterator.prototype@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" - integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== - dependencies: - define-properties "^1.2.1" - get-intrinsic "^1.2.1" - has-symbols "^1.0.3" - reflect.getprototypeof "^1.0.4" - set-function-name "^2.0.1" - -jackspeak@^3.1.2: - version "3.4.3" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" - integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== - dependencies: - "@isaacs/cliui" "^8.0.2" - optionalDependencies: - "@pkgjs/parseargs" "^0.11.0" - -javascript-stringify@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/javascript-stringify/-/javascript-stringify-2.1.0.tgz#27c76539be14d8bd128219a2d731b09337904e79" - integrity sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg== - -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@4.1.0, js-yaml@^4.0.0, js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz#b43d35e89c0f3be6b5fbbe9dc6c82467b30c28da" - integrity sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ== - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== - -json5@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" - integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== - dependencies: - minimist "^1.2.0" - -json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonfile@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" - integrity sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg== - optionalDependencies: - graceful-fs "^4.1.6" - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: - version "3.3.5" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" - integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== - dependencies: - array-includes "^3.1.6" - array.prototype.flat "^1.3.1" - object.assign "^4.1.4" - object.values "^1.1.6" - -keyv@^4.5.3: - version "4.5.4" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" - integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== - dependencies: - json-buffer "3.0.1" - -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - -language-subtag-registry@^0.3.20: - version "0.3.23" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz#23529e04d9e3b74679d70142df3fd2eb6ec572e7" - integrity sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== - -language-tags@^1.0.9: - version "1.0.9" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" - integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== - dependencies: - language-subtag-registry "^0.3.20" - -lead@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/lead/-/lead-4.0.0.tgz#5317a49effb0e7ec3a0c8fb9c1b24fb716aab939" - integrity sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg== - -levn@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" - integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== - dependencies: - prelude-ls "^1.2.1" - type-check "~0.4.0" - -lilconfig@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.2.tgz#e4a7c3cb549e3a606c8dcc32e5ae1005e62c05cb" - integrity sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow== - -linkify-it@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" - integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== - dependencies: - uc.micro "^2.0.0" - -loader-utils@^3.2.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.3.1.tgz#735b9a19fd63648ca7adbd31c2327dfe281304e5" - integrity sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg== - -local-pkg@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" - integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== - dependencies: - mlly "^1.4.2" - pkg-types "^1.0.3" - -locate-path@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" - integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== - dependencies: - p-locate "^5.0.0" - -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.merge@^4.6.2: - version "4.6.2" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" - integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" - integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== - dependencies: - chalk "^4.1.0" - is-unicode-supported "^0.1.0" - -longest-streak@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" - integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== - -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - -lru-cache@^10.2.0, lru-cache@^10.4.3: - version "10.4.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" - integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^7.4.4, lru-cache@^7.5.1, lru-cache@^7.7.1: - version "7.18.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" - integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== - -luxon@^3.5.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.5.0.tgz#6b6f65c5cd1d61d1fd19dbf07ee87a50bf4b8e20" - integrity sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ== - -markdown-extensions@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/markdown-extensions/-/markdown-extensions-1.1.1.tgz#fea03b539faeaee9b4ef02a3769b455b189f7fc3" - integrity sha512-WWC0ZuMzCyDHYCasEGs4IPvLyTGftYwh6wIEOULOF0HXcqZlhwRzrK0w2VUlxWA98xnvb/jszw4ZSkJ6ADpM6Q== - -markdown-it@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" - integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== - 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" - -matcher-collection@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/matcher-collection/-/matcher-collection-2.0.1.tgz#90be1a4cf58d6f2949864f65bb3b0f3e41303b29" - integrity sha512-daE62nS2ZQsDg9raM0IlZzLmI2u+7ZapXBwdoeBUKAYERPDDIc0qNqA8E0Rp2D+gspKR7BgIFP52GeujaGXWeQ== - dependencies: - "@types/minimatch" "^3.0.3" - minimatch "^3.0.2" - -mdast-util-definitions@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" - integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" - -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.1.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" - integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" - -mdast-util-frontmatter@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-frontmatter/-/mdast-util-frontmatter-1.0.1.tgz#79c46d7414eb9d3acabe801ee4a70a70b75e5af1" - integrity sha512-JjA2OjxRqAa8wEG8hloD0uTU0kdn8kbtOWpPP94NBkfAlbxn4S8gCGf/9DwFtEeGPXrDcNXdiDjVaRdUFqYokw== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - micromark-extension-frontmatter "^1.0.0" - -mdast-util-mdx-expression@^1.0.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-expression/-/mdast-util-mdx-expression-1.3.2.tgz#d027789e67524d541d6de543f36d51ae2586f220" - integrity sha512-xIPmR5ReJDu/DHH1OoIT1HkuybIfRGYRywC+gJtI7qHjCJp/M9jrmBEJW22O8lskDWm562BX2W8TiAwRTb0rKA== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-mdx-jsx@^2.0.0: - version "2.1.4" - resolved "https://registry.yarnpkg.com/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-2.1.4.tgz#7c1f07f10751a78963cfabee38017cbc8b7786d1" - integrity sha512-DtMn9CmVhVzZx3f+optVDF8yFgQVt7FghCRNdlIaS3X5Bnym3hZwPbg/XW86vdpKjlc1PVj26SpnLGeJBXD3JA== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - ccount "^2.0.0" - mdast-util-from-markdown "^1.1.0" - mdast-util-to-markdown "^1.3.0" - parse-entities "^4.0.0" - stringify-entities "^4.0.0" - unist-util-remove-position "^4.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - -mdast-util-mdx@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdx/-/mdast-util-mdx-2.0.1.tgz#49b6e70819b99bb615d7223c088d295e53bb810f" - integrity sha512-38w5y+r8nyKlGvNjSEqWrhG0w5PmnRA+wnBvm+ulYCct7nsGYhFVb0lljS9bQav4psDAS1eGkP2LMVcZBi/aqw== - dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-mdx-expression "^1.0.0" - mdast-util-mdx-jsx "^2.0.0" - mdast-util-mdxjs-esm "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-mdxjs-esm@^1.0.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-1.3.1.tgz#645d02cd607a227b49721d146fd81796b2e2d15b" - integrity sha512-SXqglS0HrEvSdUEfoXFtcg7DRl7S2cwOXc7jkuusG472Mmjag34DUDeOJUZtl+BVnyeO1frIgVpHlNRWc2gk/w== - dependencies: - "@types/estree-jsx" "^1.0.0" - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-phrasing@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" - integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-is "^5.0.0" - -mdast-util-to-hast@^12.1.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" - integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-definitions "^5.0.0" - micromark-util-sanitize-uri "^1.1.0" - trim-lines "^3.0.0" - unist-util-generated "^2.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.0.0" - -mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" - integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^3.0.0" - mdast-util-to-string "^3.0.0" - micromark-util-decode-string "^1.0.0" - unist-util-visit "^4.0.0" - zwitch "^2.0.0" - -mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" - integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== - dependencies: - "@types/mdast" "^3.0.0" - -mdurl@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0" - integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w== - -media-query-parser@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/media-query-parser/-/media-query-parser-2.0.2.tgz#ff79e56cee92615a304a1c2fa4f2bd056c0a1d29" - integrity sha512-1N4qp+jE0pL5Xv4uEcwVUhIkwdUO3S/9gML90nqKA7v7FcOS5vUtatfzok9S9U1EJU8dHWlcv95WLnKmmxZI9w== - dependencies: - "@babel/runtime" "^7.12.5" - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== - -micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" - integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - -micromark-extension-frontmatter@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/micromark-extension-frontmatter/-/micromark-extension-frontmatter-1.1.1.tgz#2946643938e491374145d0c9aacc3249e38a865f" - integrity sha512-m2UH9a7n3W8VAH9JO9y01APpPKmNNNs71P0RbknEmYSaZU5Ghogv38BYO94AI5Xw6OYfxZRdHZZ2nYjs/Z+SZQ== - dependencies: - fault "^2.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-extension-mdx-expression@^1.0.0: - version "1.0.8" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-1.0.8.tgz#5bc1f5fd90388e8293b3ef4f7c6f06c24aff6314" - integrity sha512-zZpeQtc5wfWKdzDsHRBY003H2Smg+PUi2REhqgIhdzAa5xonhP03FcXxqFSerFiNUr5AWmHpaNPQTBVOS4lrXw== - dependencies: - "@types/estree" "^1.0.0" - micromark-factory-mdx-expression "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-mdx-jsx@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-1.0.5.tgz#e72d24b7754a30d20fb797ece11e2c4e2cae9e82" - integrity sha512-gPH+9ZdmDflbu19Xkb8+gheqEDqkSpdCEubQyxuz/Hn8DOXiXvrXeikOoBA71+e8Pfi0/UYmU3wW3H58kr7akA== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - estree-util-is-identifier-name "^2.0.0" - micromark-factory-mdx-expression "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-extension-mdx-md@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-1.0.1.tgz#595d4b2f692b134080dca92c12272ab5b74c6d1a" - integrity sha512-7MSuj2S7xjOQXAjjkbjBsHkMtb+mDGVW6uI2dBL9snOBCbZmoNgDAeZ0nSn9j3T42UE/g2xVNMn18PJxZvkBEA== - dependencies: - micromark-util-types "^1.0.0" - -micromark-extension-mdxjs-esm@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-1.0.5.tgz#e4f8be9c14c324a80833d8d3a227419e2b25dec1" - integrity sha512-xNRBw4aoURcyz/S69B19WnZAkWJMxHMT5hE36GtDAyhoyn/8TuAeqjFJQlwk+MKQsUD7b3l7kFX+vlfVWgcX1w== - dependencies: - "@types/estree" "^1.0.0" - micromark-core-commonmark "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-position-from-estree "^1.1.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-extension-mdxjs@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-1.0.1.tgz#f78d4671678d16395efeda85170c520ee795ded8" - integrity sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q== - dependencies: - acorn "^8.0.0" - acorn-jsx "^5.0.0" - micromark-extension-mdx-expression "^1.0.0" - micromark-extension-mdx-jsx "^1.0.0" - micromark-extension-mdx-md "^1.0.0" - micromark-extension-mdxjs-esm "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-destination@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" - integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-label@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" - integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-factory-mdx-expression@^1.0.0: - version "1.0.9" - resolved "https://registry.yarnpkg.com/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-1.0.9.tgz#57ba4571b69a867a1530f34741011c71c73a4976" - integrity sha512-jGIWzSmNfdnkJq05c7b0+Wv0Kfz3NJ3N4cBjnbO4zjXIlxJr+f8lk+5ZmwFvqdAbUy2q6B5rCY//g0QAAaXDWA== - dependencies: - "@types/estree" "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-events-to-acorn "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-position-from-estree "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-title@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" - integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-whitespace@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" - integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-character@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-chunked@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" - integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-classify-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" - integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-combine-extensions@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" - integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" - integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-decode-string@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" - integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" - integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== - -micromark-util-events-to-acorn@^1.0.0: - version "1.2.3" - resolved "https://registry.yarnpkg.com/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-1.2.3.tgz#a4ab157f57a380e646670e49ddee97a72b58b557" - integrity sha512-ij4X7Wuc4fED6UoLWkmo0xJQhsktfNh1J0m8g4PbIMPlx+ek/4YdW5mvbye8z/aZvAPUoxgXHrwVlXAPKMRp1w== - dependencies: - "@types/acorn" "^4.0.0" - "@types/estree" "^1.0.0" - "@types/unist" "^2.0.0" - estree-util-visit "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - vfile-message "^3.0.0" - -micromark-util-html-tag-name@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" - integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== - -micromark-util-normalize-identifier@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" - integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-resolve-all@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" - integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== - dependencies: - micromark-util-types "^1.0.0" - -micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" - integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-subtokenize@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" - integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-util-symbol@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - -micromark@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" - integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - -micromatch@^4.0.4: - version "4.0.8" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" - integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== - dependencies: - braces "^3.0.3" - picomatch "^2.3.1" - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -"mime-db@>= 1.43.0 < 2": - version "1.53.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" - integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== - -mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimatch@9.0.3: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^9.0.0, minimatch@^9.0.4: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - -minimist@^1.2.0, minimist@^1.2.6: - version "1.2.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" - integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== - -minipass-collect@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" - integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== - dependencies: - minipass "^3.0.0" - -minipass-flush@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" - integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== - dependencies: - minipass "^3.0.0" - -minipass-pipeline@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" - integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== - dependencies: - minipass "^3.0.0" - -minipass@^3.0.0: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" - integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== - -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.3, minipass@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" - integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mkdirp-classic@^0.5.2: - version "0.5.3" - resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" - integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== - -mkdirp@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mktemp@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/mktemp/-/mktemp-0.4.0.tgz#6d0515611c8a8c84e484aa2000129b98e981ff0b" - integrity sha512-IXnMcJ6ZyTuhRmJSjzvHSRhlVPiN9Jwc6e59V0bEJ0ba6OBeX2L0E+mRN1QseeOF4mM+F1Rit6Nh7o+rl2Yn/A== - -mlly@^1.4.2, mlly@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.1.tgz#e0336429bb0731b6a8e887b438cbdae522c8f32f" - integrity sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA== - dependencies: - acorn "^8.11.3" - pathe "^1.1.2" - pkg-types "^1.1.1" - ufo "^1.5.3" - -modern-ahocorasick@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/modern-ahocorasick/-/modern-ahocorasick-1.0.1.tgz#dec373444f51b5458ac05216a8ec376e126dd283" - integrity sha512-yoe+JbhTClckZ67b2itRtistFKf8yPYelHLc7e5xAwtNAXxM6wJTUx2C7QeVSJFDzKT7bCIFyBVybPMKvmB9AA== - -morgan@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" - integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== - dependencies: - basic-auth "~2.0.1" - debug "2.6.9" - depd "~2.0.0" - on-finished "~2.3.0" - on-headers "~1.0.2" - -mri@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - -mrmime@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" - integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -node-fetch@^2.6.12: - version "2.7.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" - integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== - dependencies: - whatwg-url "^5.0.0" - -node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== - -normalize-package-data@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-5.0.0.tgz#abcb8d7e724c40d88462b84982f7cbf6859b4588" - integrity sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q== - dependencies: - hosted-git-info "^6.0.0" - is-core-module "^2.8.1" - semver "^7.3.5" - validate-npm-package-license "^3.0.4" - -normalize-path@3.0.0, normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -now-and-later@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/now-and-later/-/now-and-later-3.0.0.tgz#cdc045dc5b894b35793cf276cc3206077bb7302d" - integrity sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg== - dependencies: - once "^1.4.0" - -npm-install-checks@^6.0.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/npm-install-checks/-/npm-install-checks-6.3.0.tgz#046552d8920e801fa9f919cad569545d60e826fe" - integrity sha512-W29RiK/xtpCGqn6f3ixfRYGk+zRyr+Ew9F2E20BfXxT5/euLdA/Nm7fO7OeTGuAmTs30cpgInyJ0cYe708YTZw== - dependencies: - semver "^7.1.1" - -npm-normalize-package-bin@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz#25447e32a9a7de1f51362c61a559233b89947832" - integrity sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ== - -npm-package-arg@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-10.1.0.tgz#827d1260a683806685d17193073cc152d3c7e9b1" - integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== - dependencies: - hosted-git-info "^6.0.0" - proc-log "^3.0.0" - semver "^7.3.5" - validate-npm-package-name "^5.0.0" - -npm-pick-manifest@^8.0.0: - version "8.0.2" - resolved "https://registry.yarnpkg.com/npm-pick-manifest/-/npm-pick-manifest-8.0.2.tgz#2159778d9c7360420c925c1a2287b5a884c713aa" - integrity sha512-1dKY+86/AIiq1tkKVD3l0WI+Gd3vkknVGAggsFeBkTvbhMQ1OND/LKkYv4JtXPKUJ8bOTCyLiqEg2P6QNdK+Gg== - dependencies: - npm-install-checks "^6.0.0" - npm-normalize-package-bin "^3.0.0" - npm-package-arg "^10.0.0" - semver "^7.3.5" - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -object-assign@^4.1.0, object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.13.1: - version "1.13.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" - integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== - -object-is@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" - integrity sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.assign@^4.1.4, object.assign@^4.1.5: - version "4.1.5" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" - integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== - dependencies: - call-bind "^1.0.5" - define-properties "^1.2.1" - has-symbols "^1.0.3" - object-keys "^1.1.1" - -object.entries@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" - integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -object.fromentries@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" - integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-object-atoms "^1.0.0" - -object.groupby@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" - integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - -object.values@^1.1.6, object.values@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" - integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -on-finished@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" - integrity sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww== - dependencies: - ee-first "1.1.1" - -on-headers@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" - integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -optionator@^0.9.3: - version "0.9.4" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" - integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== - 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" - -ora@^5.4.1: - version "5.4.1" - resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" - integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== - dependencies: - bl "^4.1.0" - chalk "^4.1.0" - cli-cursor "^3.1.0" - cli-spinners "^2.5.0" - is-interactive "^1.0.0" - is-unicode-supported "^0.1.0" - log-symbols "^4.1.0" - strip-ansi "^6.0.0" - wcwidth "^1.0.1" - -outdent@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.8.0.tgz#2ebc3e77bf49912543f1008100ff8e7f44428eb0" - integrity sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A== - -p-limit@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" - integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== - dependencies: - p-limit "^3.0.2" - -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - -package-json-from-dist@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz#e501cd3094b278495eb4258d4c9f6d5ac3019f00" - integrity sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw== - -pako@~0.2.0: - version "0.2.9" - resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75" - integrity sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-entities@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.1.tgz#4e2a01111fb1c986549b944af39eeda258fc9e4e" - integrity sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w== - dependencies: - "@types/unist" "^2.0.0" - character-entities "^2.0.0" - character-entities-legacy "^3.0.0" - character-reference-invalid "^2.0.0" - decode-named-character-reference "^1.0.0" - is-alphanumerical "^2.0.0" - is-decimal "^2.0.0" - is-hexadecimal "^2.0.0" - -parse-ms@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-2.1.0.tgz#348565a753d4391fa524029956b172cb7753097d" - integrity sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA== - -parse-srcset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" - integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q== - -parse5-htmlparser2-tree-adapter@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz#23c2cc233bcf09bb7beba8b8a69d46b08c62c2f1" - integrity sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g== - dependencies: - domhandler "^5.0.2" - parse5 "^7.0.0" - -parse5-parser-stream@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz#d7c20eadc37968d272e2c02660fff92dd27e60e1" - integrity sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow== - dependencies: - parse5 "^7.0.0" - -parse5@^7.0.0, parse5@^7.1.2: - version "7.1.2" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" - integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== - dependencies: - entities "^4.4.0" - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-posix@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/path-posix/-/path-posix-1.0.0.tgz#06b26113f56beab042545a23bfa88003ccac260f" - integrity sha512-1gJ0WpNIiYcQydgg3Ed8KzvIqTsDpNwq+cjBCssvBtuTWjEqY1AW+i+OepiEMqDCzyro9B2sLAe4RBPajMYFiA== - -path-scurry@^1.11.1: - version "1.11.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" - integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== - dependencies: - lru-cache "^10.2.0" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pathe@^1.1.1, pathe@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" - integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== - -peek-stream@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/peek-stream/-/peek-stream-1.1.3.tgz#3b35d84b7ccbbd262fff31dc10da56856ead6d67" - integrity sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA== - dependencies: - buffer-from "^1.0.0" - duplexify "^3.5.0" - through2 "^2.0.3" - -periscopic@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/periscopic/-/periscopic-3.1.0.tgz#7e9037bf51c5855bd33b48928828db4afa79d97a" - integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^3.0.0" - is-reference "^3.0.0" - -picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" - integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pidtree@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" - integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== - -pkg-types@^1.0.3, pkg-types@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.2.0.tgz#d0268e894e93acff11a6279de147e83354ebd42d" - integrity sha512-+ifYuSSqOQ8CqP4MbZA5hDpb97n3E8SVWdJe+Wms9kj745lmd3b7EZJiqvmLwAlmRfjrI7Hi5z3kdBJ93lFNPA== - dependencies: - confbox "^0.1.7" - mlly "^1.7.1" - pathe "^1.1.2" - -possible-typed-array-names@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" - integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== - -postcss-discard-duplicates@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" - integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== - -postcss-load-config@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" - integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== - dependencies: - lilconfig "^3.0.0" - yaml "^2.3.4" - -postcss-modules-extract-imports@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz#b4497cb85a9c0c4b5aabeb759bb25e8d89f15002" - integrity sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q== - -postcss-modules-local-by-default@^4.0.0: - version "4.0.5" - resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz#f1b9bd757a8edf4d8556e8d0f4f894260e3df78f" - integrity sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw== - dependencies: - icss-utils "^5.0.0" - postcss-selector-parser "^6.0.2" - postcss-value-parser "^4.1.0" - -postcss-modules-scope@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz#a43d28289a169ce2c15c00c4e64c0858e43457d5" - integrity sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ== - dependencies: - postcss-selector-parser "^6.0.4" - -postcss-modules-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" - integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== - dependencies: - icss-utils "^5.0.0" - -postcss-modules@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-modules/-/postcss-modules-6.0.0.tgz#cac283dbabbbdc2558c45391cbd0e2df9ec50118" - integrity sha512-7DGfnlyi/ju82BRzTIjWS5C4Tafmzl3R79YP/PASiocj+aa6yYphHhhKUOEoXQToId5rgyFgJ88+ccOUydjBXQ== - dependencies: - generic-names "^4.0.0" - icss-utils "^5.1.0" - lodash.camelcase "^4.3.0" - postcss-modules-extract-imports "^3.0.0" - postcss-modules-local-by-default "^4.0.0" - postcss-modules-scope "^3.0.0" - postcss-modules-values "^4.0.0" - string-hash "^1.1.1" - -postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: - version "6.1.2" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz#27ecb41fb0e3b6ba7a1ec84fff347f734c7929de" - integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-value-parser@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@^8.3.11: - version "8.4.47" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.47.tgz#5bf6c9a010f3e724c503bf03ef7947dcb0fea365" - integrity sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ== - dependencies: - nanoid "^3.3.7" - picocolors "^1.1.0" - source-map-js "^1.2.1" - -postcss@^8.4.19, postcss@^8.4.43: - version "8.4.45" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.45.tgz#538d13d89a16ef71edbf75d895284ae06b79e603" - integrity sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.1" - source-map-js "^1.2.0" - -prelude-ls@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" - integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== - -prettier@^2.7.1: - version "2.8.8" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" - integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== - -prettier@^3.3.3: - version "3.3.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.3.tgz#30c54fe0be0d8d12e6ae61dbb10109ea00d53105" - integrity sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew== - -pretty-ms@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-7.0.1.tgz#7d903eaab281f7d8e03c66f867e239dc32fb73e8" - integrity sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q== - dependencies: - parse-ms "^2.1.0" - -proc-log@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" - integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -promise-inflight@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" - integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== - -promise-map-series@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/promise-map-series/-/promise-map-series-0.3.0.tgz#41873ca3652bb7a042b387d538552da9b576f8a1" - integrity sha512-3npG2NGhTc8BWBolLLf8l/92OxMGaRLbqvIh9wjCHhDXNvk4zsxaTaCpiCunW09qWPrN2zeNSNwRLVBrQQtutA== - -promise-retry@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" - integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== - dependencies: - err-code "^2.0.2" - retry "^0.12.0" - -prop-types-extra@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" - integrity sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew== - dependencies: - react-is "^16.3.2" - warning "^4.0.0" - -prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: - version "15.8.1" - resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" - integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== - dependencies: - loose-envify "^1.4.0" - object-assign "^4.1.1" - react-is "^16.13.1" - -property-information@^6.0.0: - version "6.5.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec" - integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig== - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -pumpify@^1.3.3: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" - integrity sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ== - dependencies: - duplexify "^3.6.0" - inherits "^2.0.3" - pump "^2.0.0" - -punycode.js@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7" - integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA== - -punycode@^2.1.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" - integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== - -qs@6.11.0: - version "6.11.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" - integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== - dependencies: - side-channel "^1.0.4" - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -queue-tick@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.1.tgz#f6f07ac82c1fd60f82e098b417a80e52f1f4c142" - integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag== - -quick-temp@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/quick-temp/-/quick-temp-0.1.8.tgz#bab02a242ab8fb0dd758a3c9776b32f9a5d94408" - integrity sha512-YsmIFfD9j2zaFwJkzI6eMG7y0lQP7YeWzgtFgNl38pGWZBSXJooZbOWwkcRot7Vt0Fg9L23pX0tqWU3VvLDsiA== - dependencies: - mktemp "~0.4.0" - rimraf "^2.5.4" - underscore.string "~3.3.4" - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -react-bootstrap-icons@^1.11.4: - version "1.11.4" - resolved "https://registry.yarnpkg.com/react-bootstrap-icons/-/react-bootstrap-icons-1.11.4.tgz#f4d5a852af58b5e0523df7162758b77f6fef2eec" - integrity sha512-lnkOpNEZ/Zr7mNxvjA9efuarCPSgtOuGA55XiRj7ASJnBjb1wEAdtJOd2Aiv9t07r7FLI1IgyZPg9P6jqWD/IA== - dependencies: - prop-types "^15.7.2" - -react-bootstrap@^2.10.4: - version "2.10.4" - resolved "https://registry.yarnpkg.com/react-bootstrap/-/react-bootstrap-2.10.4.tgz#ed92f5f8225a44919a7707829bac879558b71b70" - integrity sha512-W3398nBM2CBfmGP2evneEO3ZZwEMPtHs72q++eNw60uDGDAdiGn0f9yNys91eo7/y8CTF5Ke1C0QO8JFVPU40Q== - dependencies: - "@babel/runtime" "^7.24.7" - "@restart/hooks" "^0.4.9" - "@restart/ui" "^1.6.9" - "@types/react-transition-group" "^4.4.6" - classnames "^2.3.2" - dom-helpers "^5.2.1" - invariant "^2.2.4" - prop-types "^15.8.1" - prop-types-extra "^1.1.0" - react-transition-group "^4.4.5" - uncontrollable "^7.2.1" - warning "^4.0.3" - -react-dom@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" - integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== - dependencies: - loose-envify "^1.1.0" - scheduler "^0.23.2" - -react-i18next@^15.0.1: - version "15.0.1" - resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-15.0.1.tgz#fc662d93829ecb39683fe2757a47ebfbc5c912a0" - integrity sha512-NwxLqNM6CLbeGA9xPsjits0EnXdKgCRSS6cgkgOdNcPXqL+1fYNl8fBg1wmnnHvFy812Bt4IWTPE9zjoPmFj3w== - dependencies: - "@babel/runtime" "^7.24.8" - html-parse-stringify "^3.0.1" - -react-is@^16.13.1, react-is@^16.3.2: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" - integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== - -react-lifecycles-compat@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" - integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== - -react-refresh@^0.14.0: - version "0.14.2" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" - integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== - -react-router-dom@6.26.1: - version "6.26.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.26.1.tgz#a408892b41767a49dc94b3564b0e7d8e3959f623" - integrity sha512-veut7m41S1fLql4pLhxeSW3jlqs+4MtjRLj0xvuCEXsxusJCbs6I8yn9BxzzDX2XDgafrccY6hwjmd/bL54tFw== - dependencies: - "@remix-run/router" "1.19.1" - react-router "6.26.1" - -react-router@6.26.1: - version "6.26.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.26.1.tgz#88c64837e05ffab6899a49df2a1484a22471e4ce" - integrity sha512-kIwJveZNwp7teQRI5QmwWo39A5bXRyqpH0COKKmPnyD2vBvDwgFXSqDUYtt1h+FEyfnE8eXr7oe0MxRzVwCcvQ== - dependencies: - "@remix-run/router" "1.19.1" - -react-transition-group@^4.4.5: - version "4.4.5" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" - integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== - dependencies: - "@babel/runtime" "^7.5.5" - dom-helpers "^5.0.1" - loose-envify "^1.4.0" - prop-types "^15.6.2" - -react@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" - -readable-stream@^2.0.0, readable-stream@~2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" - integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0: - version "3.6.2" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" - integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -reflect.getprototypeof@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" - integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.1" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - globalthis "^1.0.3" - which-builtin-type "^1.1.3" - -regenerator-runtime@^0.14.0: - version "0.14.1" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" - integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== - -regexp.prototype.flags@^1.5.1, regexp.prototype.flags@^1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" - integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== - dependencies: - call-bind "^1.0.6" - define-properties "^1.2.1" - es-errors "^1.3.0" - set-function-name "^2.0.1" - -remark-frontmatter@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/remark-frontmatter/-/remark-frontmatter-4.0.1.tgz#84560f7ccef114ef076d3d3735be6d69f8922309" - integrity sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-frontmatter "^1.0.0" - micromark-extension-frontmatter "^1.0.0" - unified "^10.0.0" - -remark-mdx-frontmatter@^1.0.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/remark-mdx-frontmatter/-/remark-mdx-frontmatter-1.1.1.tgz#54cfb3821fbb9cb6057673e0570ae2d645f6fe32" - integrity sha512-7teX9DW4tI2WZkXS4DBxneYSY7NHiXl4AKdWDO9LXVweULlCT8OPWsOjLEnMIXViN1j+QcY8mfbq3k0EK6x3uA== - dependencies: - estree-util-is-identifier-name "^1.0.0" - estree-util-value-to-estree "^1.0.0" - js-yaml "^4.0.0" - toml "^3.0.0" - -remark-mdx@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/remark-mdx/-/remark-mdx-2.3.0.tgz#efe678025a8c2726681bde8bf111af4a93943db4" - integrity sha512-g53hMkpM0I98MU266IzDFMrTD980gNF3BJnkyFcmN+dD873mQeD5rdMO3Y2X+x8umQfbSE0PcoEDl7ledSA+2g== - dependencies: - mdast-util-mdx "^2.0.0" - micromark-extension-mdxjs "^1.0.0" - -remark-parse@^10.0.0: - version "10.0.2" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.2.tgz#ca241fde8751c2158933f031a4e3efbaeb8bc262" - integrity sha512-3ydxgHa/ZQzG8LvC7jTXccARYDcRld3VfcgIIFs7bI6vbRSxJJmzgLEIIoYKyrfhaY+ujuWaf/PJiMZXoiCXgw== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - unified "^10.0.0" - -remark-rehype@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" - integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-to-hast "^12.1.0" - unified "^10.0.0" - -remix-i18next@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/remix-i18next/-/remix-i18next-6.3.0.tgz#1a086486ec0d6b13c262baf3b4420227fd4d8f8f" - integrity sha512-QisIBEv/XR29/FldR9NDwrQ712FRXceJlzstE+2dES2fG8K0TOcGan/bTOD+e+WLEwDqTf1lbBrqp5P7Ik/Eww== - -remove-trailing-separator@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" - integrity sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw== - -replace-ext@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-2.0.0.tgz#9471c213d22e1bcc26717cd6e50881d88f812b06" - integrity sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug== - -"require-like@>= 0.1.1": - version "0.1.2" - resolved "https://registry.yarnpkg.com/require-like/-/require-like-0.1.2.tgz#ad6f30c13becd797010c468afa775c0c0a6b47fa" - integrity sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve-options@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve-options/-/resolve-options-2.0.0.tgz#a1a57a9949db549dd075de3f5550675f02f1e4c5" - integrity sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A== - dependencies: - value-or-function "^4.0.0" - -resolve-pkg-maps@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" - integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== - -resolve.exports@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" - integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== - -resolve@^1.22.4: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -resolve@^2.0.0-next.5: - version "2.0.0-next.5" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" - integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -retry@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" - integrity sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow== - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^2.5.4: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -rollup@^4.20.0: - version "4.21.2" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.21.2.tgz#f41f277a448d6264e923dd1ea179f0a926aaf9b7" - integrity sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw== - dependencies: - "@types/estree" "1.0.5" - optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.21.2" - "@rollup/rollup-android-arm64" "4.21.2" - "@rollup/rollup-darwin-arm64" "4.21.2" - "@rollup/rollup-darwin-x64" "4.21.2" - "@rollup/rollup-linux-arm-gnueabihf" "4.21.2" - "@rollup/rollup-linux-arm-musleabihf" "4.21.2" - "@rollup/rollup-linux-arm64-gnu" "4.21.2" - "@rollup/rollup-linux-arm64-musl" "4.21.2" - "@rollup/rollup-linux-powerpc64le-gnu" "4.21.2" - "@rollup/rollup-linux-riscv64-gnu" "4.21.2" - "@rollup/rollup-linux-s390x-gnu" "4.21.2" - "@rollup/rollup-linux-x64-gnu" "4.21.2" - "@rollup/rollup-linux-x64-musl" "4.21.2" - "@rollup/rollup-win32-arm64-msvc" "4.21.2" - "@rollup/rollup-win32-ia32-msvc" "4.21.2" - "@rollup/rollup-win32-x64-msvc" "4.21.2" - fsevents "~2.3.2" - -rsvp@^4.8.2: - version "4.8.5" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" - integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== - -rsvp@~3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.2.1.tgz#07cb4a5df25add9e826ebc67dcc9fd89db27d84a" - integrity sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - -safe-array-concat@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" - integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== - dependencies: - call-bind "^1.0.7" - get-intrinsic "^1.2.4" - has-symbols "^1.0.3" - isarray "^2.0.5" - -safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -safe-buffer@5.2.1, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-regex-test@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" - integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== - dependencies: - call-bind "^1.0.6" - es-errors "^1.3.0" - is-regex "^1.1.4" - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sanitize-html@^2.13.0: - version "2.13.0" - resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.13.0.tgz#71aedcdb777897985a4ea1877bf4f895a1170dae" - integrity sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA== - 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" - -sass@1.77.6: - version "1.77.6" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.77.6.tgz#898845c1348078c2e6d1b64f9ee06b3f8bd489e4" - integrity sha512-ByXE1oLD79GVq9Ht1PeHWCPMPB8XHpBuz1r85oByKHjZY6qV6rWnQovQzXJXuQ/XyE1Oj3iPk3lo28uzaRA2/Q== - dependencies: - chokidar ">=3.0.0 <4.0.0" - immutable "^4.0.0" - source-map-js ">=0.6.2 <2.0.0" - -scheduler@^0.23.2: - version "0.23.2" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" - integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== - dependencies: - loose-envify "^1.1.0" - -semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver@^7.5.3, semver@^7.5.4, semver@^7.6.3: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -set-cookie-parser@^2.4.8, set-cookie-parser@^2.6.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz#ef5552b56dc01baae102acb5fc9fb8cd060c30f9" - integrity sha512-lXLOiqpkUumhRdFF3k1osNXCy9akgx/dyPZ5p8qAg9seJzXr5ZrlqZuWIMuY6ejOsVLE6flJ5/h3lsn57fQ/PQ== - -set-function-length@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" - integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== - 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" - -set-function-name@^2.0.1, set-function-name@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" - integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== - dependencies: - define-data-property "^1.1.4" - es-errors "^1.3.0" - functions-have-names "^1.2.3" - has-property-descriptors "^1.0.2" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -side-channel@^1.0.4, side-channel@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" - integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - get-intrinsic "^1.2.4" - object-inspect "^1.13.1" - -signal-exit@^3.0.2, signal-exit@^3.0.3: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -signal-exit@^4.0.1: - version "4.1.0" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" - integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -sort-keys@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-5.1.0.tgz#50a3f3d1ad3c5a76d043e0aeeba7299241e9aa5c" - integrity sha512-aSbHV0DaBcr7u0PVHXzM6NbZNAtrr9sF6+Qfs9UUVG7Ll3jQ6hHi8F/xqIIcn2rvIVbr0v/2zyjSdwSV47AgLQ== - dependencies: - is-plain-obj "^4.0.0" - -"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" - integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== - -source-map-js@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" - integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== - -source-map-support@^0.5.21: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@^0.7.0, source-map@^0.7.3: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - -spdx-correct@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" - integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" - integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.20" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" - integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== - -sprintf-js@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" - integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== - -ssri@^10.0.0: - version "10.0.6" - resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.6.tgz#a8aade2de60ba2bce8688e3fa349bad05c7dc1e5" - integrity sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ== - dependencies: - minipass "^7.0.3" - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -stop-iteration-iterator@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz#6a60be0b4ee757d1ed5254858ec66b10c49285e4" - integrity sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ== - dependencies: - internal-slot "^1.0.4" - -stream-composer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/stream-composer/-/stream-composer-1.0.2.tgz#7ee61ca1587bf5f31b2e29aa2093cbf11442d152" - integrity sha512-bnBselmwfX5K10AH6L4c8+S5lgZMWI7ZYrz2rvYjCPB2DIMC4Ig8OpxGpNJSxRZ58oti7y1IcNvjBAz9vW5m4w== - dependencies: - streamx "^2.13.2" - -stream-shift@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b" - integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ== - -stream-slice@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/stream-slice/-/stream-slice-0.1.2.tgz#2dc4f4e1b936fb13f3eb39a2def1932798d07a4b" - integrity sha512-QzQxpoacatkreL6jsxnVb7X5R/pGw9OUv2qWTYWnmLpg4NdN31snPy/f3TdQE1ZUXaThRvj1Zw4/OGg0ZkaLMA== - -streamx@^2.12.0, streamx@^2.12.5, streamx@^2.13.2, streamx@^2.14.0: - version "2.20.0" - resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.20.0.tgz#5f3608483499a9346852122b26042f964ceec931" - integrity sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ== - dependencies: - fast-fifo "^1.3.2" - queue-tick "^1.0.1" - text-decoder "^1.1.0" - optionalDependencies: - bare-events "^2.2.0" - -string-hash@^1.1.1: - version "1.1.3" - resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" - integrity sha512-kJUvRUFK49aub+a7T1nNE66EJbZBMnBgoC1UbCZ5n6bsZKBRga4KgBRTMn/pFkeCZSYtNeSyMxPDM0AXWELk2A== - -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string.prototype.includes@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.includes/-/string.prototype.includes-2.0.0.tgz#8986d57aee66d5460c144620a6d873778ad7289f" - integrity sha512-E34CkBgyeqNDcrbU76cDjL5JLcVrtSdYq0MEh/B10r17pRP4ciHLwTgnuLV8Ay6cgEMLkcBkFCKyFZ43YldYzg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.matchall@^4.0.11: - version "4.0.11" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" - integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.2" - es-errors "^1.3.0" - es-object-atoms "^1.0.0" - get-intrinsic "^1.2.4" - gopd "^1.0.1" - has-symbols "^1.0.3" - internal-slot "^1.0.7" - regexp.prototype.flags "^1.5.2" - set-function-name "^2.0.2" - side-channel "^1.0.6" - -string.prototype.repeat@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz#e90872ee0308b29435aa26275f6e1b762daee01a" - integrity sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - -string.prototype.trim@^1.2.9: - version "1.2.9" - resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" - integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-abstract "^1.23.0" - es-object-atoms "^1.0.0" - -string.prototype.trimend@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" - integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string.prototype.trimstart@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" - integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== - dependencies: - call-bind "^1.0.7" - define-properties "^1.2.1" - es-object-atoms "^1.0.0" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringify-entities@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" - integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.0.1" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -style-to-object@^0.4.1: - version "0.4.4" - resolved "https://registry.yarnpkg.com/style-to-object/-/style-to-object-0.4.4.tgz#266e3dfd56391a7eefb7770423612d043c3f33ec" - integrity sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg== - dependencies: - inline-style-parser "0.1.1" - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -symlink-or-copy@^1.1.8, symlink-or-copy@^1.2.0, symlink-or-copy@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/symlink-or-copy/-/symlink-or-copy-1.3.1.tgz#9506dd64d8e98fa21dcbf4018d1eab23e77f71fe" - integrity sha512-0K91MEXFpBUaywiwSSkmKjnGcasG/rVBXFLJz5DrgGabpYD6N+3yZrfD6uUIfpuTu65DZLHi7N8CizHc07BPZA== - -tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -tar-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784" - integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng== - dependencies: - chownr "^1.1.1" - mkdirp-classic "^0.5.2" - pump "^3.0.0" - tar-stream "^2.1.4" - -tar-stream@^2.1.4: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^6.1.11: - version "6.2.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" - integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^5.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -teex@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/teex/-/teex-1.0.1.tgz#b8fa7245ef8e8effa8078281946c85ab780a0b12" - integrity sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg== - dependencies: - streamx "^2.12.5" - -text-decoder@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/text-decoder/-/text-decoder-1.1.1.tgz#5df9c224cebac4a7977720b9f083f9efa1aefde8" - integrity sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA== - dependencies: - b4a "^1.6.4" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== - -through2@^2.0.1, through2@^2.0.3: - version "2.0.5" - resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" - integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== - dependencies: - readable-stream "~2.3.6" - xtend "~4.0.1" - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -to-through@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/to-through/-/to-through-3.0.0.tgz#bf4956eaca5a0476474850a53672bed6906ace54" - integrity sha512-y8MN937s/HVhEoBU1SxfHC+wxCHkV1a9gW8eAdTadYh/bGyesZIVcbjI+mSpFbSVwQici/XjBjuUyri1dnXwBw== - dependencies: - streamx "^2.12.5" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -toml@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" - integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - -trough@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/trough/-/trough-2.2.0.tgz#94a60bd6bd375c152c1df911a4b11d5b0256f50f" - integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw== - -ts-api-utils@^1.0.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" - integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== - -tsconfck@^3.0.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.3.tgz#a8202f51dab684c426314796cdb0bbd0fe0cdf80" - integrity sha512-ulNZP1SVpRDesxeMLON/LtWM8HIgAJEIVpVVhBM6gsmvQ8+Rh+ZG7FWGvHh7Ah3pRABwVJWklWCr/BTZSv0xnQ== - -tsconfig-paths@^3.15.0: - version "3.15.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" - integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== - dependencies: - "@types/json5" "^0.0.29" - json5 "^1.0.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tsconfig-paths@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" - integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== - dependencies: - json5 "^2.2.2" - minimist "^1.2.6" - strip-bom "^3.0.0" - -tslib@^2.4.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== - -turbo-stream@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/turbo-stream/-/turbo-stream-2.3.0.tgz#b9188351588dacb927b7094c63e95a711cfd63d0" - integrity sha512-PhEr9mdexoVv+rJkQ3c8TjrN3DUghX37GNJkSMksoPR4KrXIPnM2MnqRt07sViIqX9IdlhrgtTSyjoVOASq6cg== - -type-check@^0.4.0, type-check@~0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" - integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== - dependencies: - prelude-ls "^1.2.1" - -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typed-array-buffer@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" - integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== - dependencies: - call-bind "^1.0.7" - es-errors "^1.3.0" - is-typed-array "^1.1.13" - -typed-array-byte-length@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" - integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-byte-offset@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" - integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - -typed-array-length@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" - integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== - dependencies: - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-proto "^1.0.3" - is-typed-array "^1.1.13" - possible-typed-array-names "^1.0.0" - -typescript@^5.0.4: - version "5.6.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" - integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== - -typescript@^5.1.6: - version "5.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" - integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== - -uc.micro@^2.0.0, uc.micro@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" - integrity sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A== - -ufo@^1.5.3: - version "1.5.4" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.5.4.tgz#16d6949674ca0c9e0fbbae1fa20a71d7b1ded754" - integrity sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ== - -unbox-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" - integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== - dependencies: - call-bind "^1.0.2" - has-bigints "^1.0.2" - has-symbols "^1.0.3" - which-boxed-primitive "^1.0.2" - -uncontrollable@^7.2.1: - version "7.2.1" - resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.2.1.tgz#1fa70ba0c57a14d5f78905d533cf63916dc75738" - integrity sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ== - dependencies: - "@babel/runtime" "^7.6.3" - "@types/react" ">=16.9.11" - invariant "^2.2.4" - react-lifecycles-compat "^3.0.4" - -uncontrollable@^8.0.1: - version "8.0.4" - resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-8.0.4.tgz#a0a8307f638795162fafd0550f4a1efa0f8c5eb6" - integrity sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ== - -underscore.string@~3.3.4: - version "3.3.6" - resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.3.6.tgz#ad8cf23d7423cb3b53b898476117588f4e2f9159" - integrity sha512-VoC83HWXmCrF6rgkyxS9GHv8W9Q5nhMKho+OadDJGzL2oDYbYEppBaCMH6pFlwLeqj2QS+hhkw2kpXkSdD1JxQ== - dependencies: - sprintf-js "^1.1.1" - util-deprecate "^1.0.2" - -undici-types@~6.19.2: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" - integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== - -undici@^6.11.1, undici@^6.19.5: - version "6.19.8" - resolved "https://registry.yarnpkg.com/undici/-/undici-6.19.8.tgz#002d7c8a28f8cc3a44ff33c3d4be4d85e15d40e1" - integrity sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g== - -unified@^10.0.0: - version "10.1.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" - integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== - dependencies: - "@types/unist" "^2.0.0" - bail "^2.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^5.0.0" - -unique-filename@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" - integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== - dependencies: - unique-slug "^4.0.0" - -unique-slug@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" - integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== - dependencies: - imurmurhash "^0.1.4" - -unist-util-generated@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" - integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== - -unist-util-is@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" - integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-position-from-estree@^1.0.0, unist-util-position-from-estree@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/unist-util-position-from-estree/-/unist-util-position-from-estree-1.1.2.tgz#8ac2480027229de76512079e377afbcabcfcce22" - integrity sha512-poZa0eXpS+/XpoQwGwl79UUdea4ol2ZuCYguVaJS4qzIOMDzbqz8a3erUCOmubSZkaOuGamb3tX790iwOIROww== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-position@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" - integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-remove-position@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/unist-util-remove-position/-/unist-util-remove-position-4.0.2.tgz#a89be6ea72e23b1a402350832b02a91f6a9afe51" - integrity sha512-TkBb0HABNmxzAcfLf4qsIbFbaPDvMO6wa3b3j4VcEzFVaw1LBKwnW4/sRJ/atSLSzoIg41JWEdnE7N6DIhGDGQ== - dependencies: - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" - -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - -unist-util-visit@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" - -universalify@^0.1.0: - version "0.1.2" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== - -universalify@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" - integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== - -update-browserslist-db@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" - integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== - dependencies: - escalade "^3.1.2" - picocolors "^1.0.1" - -uri-js@^4.2.2: - version "4.4.1" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" - integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== - dependencies: - punycode "^2.1.0" - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -util@^0.12.3: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - 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" - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== - -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - -validate-npm-package-license@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -validate-npm-package-name@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz#a316573e9b49f3ccd90dbb6eb52b3f06c6d604e8" - integrity sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ== - -value-or-function@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/value-or-function/-/value-or-function-4.0.0.tgz#70836b6a876a010dc3a2b884e7902e9db064378d" - integrity sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg== - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== - -vfile-message@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" - integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - -vfile@^5.0.0: - version "5.3.7" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" - integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - -vinyl-contents@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/vinyl-contents/-/vinyl-contents-2.0.0.tgz#cc2ba4db3a36658d069249e9e36d9e2b41935d89" - integrity sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q== - dependencies: - bl "^5.0.0" - vinyl "^3.0.0" - -vinyl-fs@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/vinyl-fs/-/vinyl-fs-4.0.0.tgz#06cb36efc911c6e128452f230b96584a9133c3a1" - integrity sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw== - dependencies: - fs-mkdirp-stream "^2.0.1" - glob-stream "^8.0.0" - graceful-fs "^4.2.11" - iconv-lite "^0.6.3" - is-valid-glob "^1.0.0" - lead "^4.0.0" - normalize-path "3.0.0" - resolve-options "^2.0.0" - stream-composer "^1.0.2" - streamx "^2.14.0" - to-through "^3.0.0" - value-or-function "^4.0.0" - vinyl "^3.0.0" - vinyl-sourcemap "^2.0.0" - -vinyl-sourcemap@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz#422f410a0ea97cb54cebd698d56a06d7a22e0277" - integrity sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q== - dependencies: - convert-source-map "^2.0.0" - graceful-fs "^4.2.10" - now-and-later "^3.0.0" - streamx "^2.12.5" - vinyl "^3.0.0" - vinyl-contents "^2.0.0" - -vinyl@^3.0.0, vinyl@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-3.0.0.tgz#11e14732bf56e2faa98ffde5157fe6c13259ff30" - integrity sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g== - dependencies: - clone "^2.1.2" - clone-stats "^1.0.0" - remove-trailing-separator "^1.1.0" - replace-ext "^2.0.0" - teex "^1.0.1" - -vite-node@^1.2.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.6.0.tgz#2c7e61129bfecc759478fa592754fd9704aaba7f" - integrity sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw== - dependencies: - cac "^6.7.14" - debug "^4.3.4" - pathe "^1.1.1" - picocolors "^1.0.0" - vite "^5.0.0" - -vite-tsconfig-paths@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz#321f02e4b736a90ff62f9086467faf4e2da857a9" - integrity sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA== - dependencies: - debug "^4.1.1" - globrex "^0.1.2" - tsconfck "^3.0.3" - -vite@^5.0.0, vite@^5.0.11, vite@^5.1.0: - version "5.4.3" - resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.3.tgz#771c470e808cb6732f204e1ee96c2ed65b97a0eb" - integrity sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q== - dependencies: - esbuild "^0.21.3" - postcss "^8.4.43" - rollup "^4.20.0" - optionalDependencies: - fsevents "~2.3.3" - -void-elements@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" - integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== - -walk-sync@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-2.2.0.tgz#80786b0657fcc8c0e1c0b1a042a09eae2966387a" - integrity sha512-IC8sL7aB4/ZgFcGI2T1LczZeFWZ06b3zoHH7jBPyHxOtIIz1jppWHjjEXkOFvFojBVAK9pV7g47xOZ4LW3QLfg== - dependencies: - "@types/minimatch" "^3.0.3" - ensure-posix-path "^1.1.0" - matcher-collection "^2.0.0" - minimatch "^3.0.4" - -warning@^4.0.0, warning@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" - integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== - dependencies: - loose-envify "^1.0.0" - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== - dependencies: - defaults "^1.0.3" - -web-encoding@1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/web-encoding/-/web-encoding-1.1.5.tgz#fc810cf7667364a6335c939913f5051d3e0c4864" - integrity sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA== - dependencies: - util "^0.12.3" - optionalDependencies: - "@zxing/text-encoding" "0.9.0" - -web-streams-polyfill@^3.1.1: - version "3.3.3" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b" - integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-encoding@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" - integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== - dependencies: - iconv-lite "0.6.3" - -whatwg-mimetype@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" - integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-boxed-primitive@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" - integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== - dependencies: - is-bigint "^1.0.1" - is-boolean-object "^1.1.0" - is-number-object "^1.0.4" - is-string "^1.0.5" - is-symbol "^1.0.3" - -which-builtin-type@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.4.tgz#592796260602fc3514a1b5ee7fa29319b72380c3" - integrity sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w== - dependencies: - function.prototype.name "^1.1.6" - has-tostringtag "^1.0.2" - is-async-function "^2.0.0" - is-date-object "^1.0.5" - is-finalizationregistry "^1.0.2" - is-generator-function "^1.0.10" - is-regex "^1.1.4" - is-weakref "^1.0.2" - isarray "^2.0.5" - which-boxed-primitive "^1.0.2" - which-collection "^1.0.2" - which-typed-array "^1.1.15" - -which-collection@^1.0.1, which-collection@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" - integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== - dependencies: - is-map "^2.0.3" - is-set "^2.0.3" - is-weakmap "^2.0.2" - is-weakset "^2.0.3" - -which-typed-array@^1.1.13, which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.2: - version "1.1.15" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" - integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== - dependencies: - available-typed-arrays "^1.0.7" - call-bind "^1.0.7" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.2" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -which@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/which/-/which-3.0.1.tgz#89f1cd0c23f629a8105ffe69b8172791c87b4be1" - integrity sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg== - dependencies: - isexe "^2.0.0" - -word-wrap@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== - -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -ws@^7.4.5: - version "7.5.10" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.10.tgz#58b5c20dc281633f6c19113f39b349bd8bd558d9" - integrity sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ== - -xtend@~4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^2.3.4: - version "2.5.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.5.1.tgz#c9772aacf62cb7494a95b0c4f1fb065b563db130" - integrity sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q== - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zwitch@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/package.json b/package.json index 4c05b19..50681f6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "concurrently": "^9.0.1" }, "scripts": { - "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'cd Foxnouns.Backend && dotnet watch --no-hot-reload' 'cd Foxnouns.Frontend && yarn dev' 'cd rate && go run -v .'", - "format": "cd Foxnouns.Frontend && yarn format" - } + "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'cd Foxnouns.Backend && dotnet watch --no-hot-reload' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", + "format": "dotnet csharpier . && cd Foxnouns.Frontend && pnpm format" + }, + "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..c744fbb --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,211 @@ +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 diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 2e0d845..0000000 --- a/yarn.lock +++ /dev/null @@ -1,176 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -concurrently@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.0.1.tgz#01e171bf6c7af0c022eb85daef95bff04d8185aa" - integrity sha512-wYKvCd/f54sTXJMSfV6Ln/B8UrfLBKOYa+lzc6CHay3Qek+LorVSBdMVfyewFhRbH0Rbabsk4D+3PL/VjQ5gzg== - 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: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -escalade@^3.1.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" - integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -rxjs@^7.8.1: - version "7.8.1" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" - integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== - dependencies: - tslib "^2.1.0" - -shell-quote@^1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" - integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -tree-kill@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" - integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== - -tslib@^2.1.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" - integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.7.2: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" From 0c78cd25b054685f9b72c7a0580ef0b8b7a5a5d8 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 24 Nov 2024 16:01:40 +0100 Subject: [PATCH 122/261] fix(backend): use serilog theme that actually works with a light terminal --- Foxnouns.Backend/Extensions/WebApplicationExtensions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index c505f4d..da6f377 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -11,6 +11,7 @@ using NodaTime; using Prometheus; using Serilog; using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; using FediverseAuthService = Foxnouns.Backend.Services.Auth.FediverseAuthService; using IClock = NodaTime.IClock; @@ -38,7 +39,7 @@ public static class WebApplicationExtensions .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Mvc", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Warning) - .WriteTo.Console(); + .WriteTo.Console(theme: AnsiConsoleTheme.Sixteen); if (config.Logging.SeqLogUrl != null) { From c179669799080e1c4351e926fc272459f4835065 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 24 Nov 2024 17:36:02 +0100 Subject: [PATCH 123/261] feat(frontend): start settings --- Foxnouns.Frontend/package.json | 2 + Foxnouns.Frontend/pnpm-lock.yaml | 17 +++ Foxnouns.Frontend/src/app.scss | 17 +++ Foxnouns.Frontend/src/lib/api/error.ts | 12 ++ .../src/lib/components/Avatar.svelte | 6 +- .../src/lib/components/Error.svelte | 20 ++-- .../src/lib/i18n/locales/en.json | 32 ++++- Foxnouns.Frontend/src/lib/index.ts | 4 + .../src/routes/+layout.server.ts | 6 +- .../src/routes/settings/+layout.server.ts | 8 ++ .../src/routes/settings/+layout.svelte | 44 +++++++ .../src/routes/settings/+page.server.ts | 37 ++++++ .../src/routes/settings/+page.svelte | 113 ++++++++++++++++++ 13 files changed, 301 insertions(+), 17 deletions(-) create mode 100644 Foxnouns.Frontend/src/routes/settings/+layout.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/+layout.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/+page.svelte diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 1d5739a..69d4a1f 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -17,6 +17,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "@sveltestrap/sveltestrap": "^6.2.7", "@types/eslint": "^9.6.0", + "@types/luxon": "^3.4.2", "@types/markdown-it": "^14.1.2", "@types/sanitize-html": "^2.13.0", "bootstrap": "^5.3.3", @@ -38,6 +39,7 @@ "dependencies": { "@fontsource/firago": "^5.1.0", "bootstrap-icons": "^1.11.3", + "luxon": "^3.5.0", "markdown-it": "^14.1.0", "sanitize-html": "^2.13.1", "tslog": "^4.9.3" diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index ca6df0f..d9bd974 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: bootstrap-icons: specifier: ^1.11.3 version: 1.11.3 + luxon: + specifier: ^3.5.0 + version: 3.5.0 markdown-it: specifier: ^14.1.0 version: 14.1.0 @@ -39,6 +42,9 @@ importers: '@types/eslint': specifier: ^9.6.0 version: 9.6.1 + '@types/luxon': + specifier: ^3.4.2 + version: 3.4.2 '@types/markdown-it': specifier: ^14.1.2 version: 14.1.2 @@ -590,6 +596,9 @@ packages: '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/luxon@3.4.2': + resolution: {integrity: sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==} + '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} @@ -1063,6 +1072,10 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + luxon@3.5.0: + resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} + engines: {node: '>=12'} + magic-string@0.30.12: resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} @@ -1800,6 +1813,8 @@ snapshots: '@types/linkify-it@5.0.0': {} + '@types/luxon@3.4.2': {} + '@types/markdown-it@14.1.2': dependencies: '@types/linkify-it': 5.0.0 @@ -2302,6 +2317,8 @@ snapshots: lodash.merge@4.6.2: {} + luxon@3.5.0: {} + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 diff --git a/Foxnouns.Frontend/src/app.scss b/Foxnouns.Frontend/src/app.scss index 252667f..4a9d5dd 100644 --- a/Foxnouns.Frontend/src/app.scss +++ b/Foxnouns.Frontend/src/app.scss @@ -23,3 +23,20 @@ @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; +} + +// 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; + } + } + } +} diff --git a/Foxnouns.Frontend/src/lib/api/error.ts b/Foxnouns.Frontend/src/lib/api/error.ts index 6b5d918..eb93884 100644 --- a/Foxnouns.Frontend/src/lib/api/error.ts +++ b/Foxnouns.Frontend/src/lib/api/error.ts @@ -52,3 +52,15 @@ export type ValidationError = { 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/components/Avatar.svelte b/Foxnouns.Frontend/src/lib/components/Avatar.svelte index 1b116ea..99a5608 100644 --- a/Foxnouns.Frontend/src/lib/components/Avatar.svelte +++ b/Foxnouns.Frontend/src/lib/components/Avatar.svelte @@ -1,14 +1,14 @@ diff --git a/Foxnouns.Frontend/src/lib/components/Error.svelte b/Foxnouns.Frontend/src/lib/components/Error.svelte index 9ca2ff0..09337a9 100644 --- a/Foxnouns.Frontend/src/lib/components/Error.svelte +++ b/Foxnouns.Frontend/src/lib/components/Error.svelte @@ -4,17 +4,19 @@ import { t } from "$lib/i18n"; import KeyedValidationErrors from "./errors/KeyedValidationErrors.svelte"; - type Props = { headerElem?: string; error: RawApiError }; - let { headerElem, error }: Props = $props(); + type Props = { showHeader?: boolean; headerElem?: string; error: RawApiError }; + let { showHeader, headerElem, error }: Props = $props(); - - {#if error.code === ErrorCode.BadRequest} - {$t("error.bad-request-header")} - {:else} - {$t("error.generic-header")} - {/if} - +{#if showHeader !== false} + + {#if error.code === ErrorCode.BadRequest} + {$t("error.bad-request-header")} + {:else} + {$t("error.generic-header")} + {/if} + +{/if}

    {errorDescription($t, error.code)}

    {#if error.errors}
    diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 5d603d7..abc4d85 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -16,7 +16,8 @@ }, "title": { "log-in": "Log in", - "welcome": "Welcome" + "welcome": "Welcome", + "settings": "Settings" }, "auth": { "log-in-form-title": "Log in with email", @@ -59,5 +60,32 @@ "validation-reason": "Reason", "validation-generic": "The value you entered is not allowed here. Reason", "extra-info-header": "Extra error information" - } + }, + "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" + }, + "yes": "Yes", + "no": "No" } diff --git a/Foxnouns.Frontend/src/lib/index.ts b/Foxnouns.Frontend/src/lib/index.ts index 6b65464..7105327 100644 --- a/Foxnouns.Frontend/src/lib/index.ts +++ b/Foxnouns.Frontend/src/lib/index.ts @@ -1,6 +1,7 @@ // 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"; @@ -10,3 +11,6 @@ export const clearToken = (cookies: Cookies) => cookies.delete(TOKEN_COOKIE_NAME // TODO: change this to something we actually clearly have the rights to use export const DEFAULT_AVATAR = "https://pronouns.cc/default/512.webp"; + +export const idTimestamp = (id: string) => + DateTime.fromMillis(parseInt(id, 10) / (1 << 22) + 1_640_995_200_000); diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts index e73ca7d..00c3ef3 100644 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -1,15 +1,15 @@ import { clearToken, TOKEN_COOKIE_NAME } from "$lib"; import { apiRequest } from "$api"; import ApiError, { ErrorCode } from "$api/error"; -import type { Meta, User } from "$api/models"; +import type { Meta, MeUser } from "$api/models"; import log from "$lib/log"; import type { LayoutServerLoad } from "./$types"; export const load = (async ({ fetch, cookies }) => { - let meUser: User | null = null; + let meUser: MeUser | null = null; if (cookies.get(TOKEN_COOKIE_NAME)) { try { - meUser = await apiRequest("GET", "/users/@me", { fetch, cookies }); + meUser = await apiRequest("GET", "/users/@me", { fetch, cookies }); } 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); diff --git a/Foxnouns.Frontend/src/routes/settings/+layout.server.ts b/Foxnouns.Frontend/src/routes/settings/+layout.server.ts new file mode 100644 index 0000000..a1ac93c --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/+layout.server.ts @@ -0,0 +1,8 @@ +import { redirect } from "@sveltejs/kit"; + +export const load = async ({ parent }) => { + const data = await parent(); + if (!data.meUser) redirect(303, "/auth/log-in"); + + return { user: data.meUser! }; +}; diff --git a/Foxnouns.Frontend/src/routes/settings/+layout.svelte b/Foxnouns.Frontend/src/routes/settings/+layout.svelte new file mode 100644 index 0000000..0b0ac53 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/+layout.svelte @@ -0,0 +1,44 @@ + + + + {$t("title.settings")} • pronouns.cc + + +
    + + + {@render children?.()} +
    diff --git a/Foxnouns.Frontend/src/routes/settings/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/+page.server.ts new file mode 100644 index 0000000..9e35bda --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/+page.server.ts @@ -0,0 +1,37 @@ +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 new file mode 100644 index 0000000..062d9e6 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/+page.svelte @@ -0,0 +1,113 @@ + + +

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

    + +
    +
    +
    Change your username
    +
    + + + + + + + {#if form?.ok} +

    + Successfully changed your username! +

    + {:else if usernameError} +

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

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

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

    +
    + +
    + +
    +

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

    {$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} +
    +
    From 261435c252c02183bed19a7505506fdacffe53f7 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 24 Nov 2024 22:19:53 +0100 Subject: [PATCH 124/261] feat: so much more frontend stuff --- .../Controllers/UsersController.cs | 40 ++++ .../20241124201309_AddUserTimezone.cs | 30 +++ .../DatabaseContextModelSnapshot.cs | 4 + Foxnouns.Backend/Database/Models/User.cs | 1 + .../Properties/launchSettings.json | 2 + .../Services/UserRendererService.cs | 16 +- Foxnouns.Frontend/.env.example | 2 + Foxnouns.Frontend/package.json | 2 + Foxnouns.Frontend/pnpm-lock.yaml | 18 ++ Foxnouns.Frontend/src/lib/api/models/user.ts | 7 +- .../src/lib/components/Avatar.svelte | 14 +- .../lib/components/editor/AvatarEditor.svelte | 77 +++++++ .../components/editor/FormStatusMarker.svelte | 18 ++ .../src/lib/i18n/locales/en.json | 205 ++++++++++-------- .../src/routes/+layout.server.ts | 4 +- .../src/routes/@[username]/+page.server.ts | 19 +- .../src/routes/settings/+layout.server.ts | 2 +- .../src/routes/settings/+page.svelte | 5 +- .../routes/settings/profile/+layout.svelte | 42 ++++ .../routes/settings/profile/+page.server.ts | 29 +++ .../src/routes/settings/profile/+page.svelte | 190 ++++++++++++++++ .../settings/profile/bio/+page.server.ts | 19 ++ .../routes/settings/profile/bio/+page.svelte | 40 ++++ package.json | 3 +- 24 files changed, 682 insertions(+), 107 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20241124201309_AddUserTimezone.cs create mode 100644 Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/+layout.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/bio/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 1d85c77..33c38d6 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,4 +1,5 @@ using System.Diagnostics.CodeAnalysis; +using Coravel.Mailer.Mail.Helpers; using Coravel.Queuing.Interfaces; using EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; @@ -116,6 +117,42 @@ public class UsersController( if (req.HasProperty(nameof(req.Avatar))) errors.Add(("avatar", ValidationUtils.ValidateAvatar(req.Avatar))); + if (req.HasProperty(nameof(req.MemberTitle))) + { + if (string.IsNullOrEmpty(req.MemberTitle)) + { + user.MemberTitle = null; + } + else + { + errors.Add(("member_title", ValidationUtils.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) @@ -253,6 +290,9 @@ public class UsersController( public Pronoun[]? Pronouns { get; init; } public Field[]? Fields { get; init; } public Snowflake[]? Flags { get; init; } + public string? MemberTitle { get; init; } + public bool? MemberListHidden { get; init; } + public string? Timezone { get; init; } } [HttpGet("@me/settings")] diff --git a/Foxnouns.Backend/Database/Migrations/20241124201309_AddUserTimezone.cs b/Foxnouns.Backend/Database/Migrations/20241124201309_AddUserTimezone.cs new file mode 100644 index 0000000..e317f65 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20241124201309_AddUserTimezone.cs @@ -0,0 +1,30 @@ +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index e1e05c2..d012fe0 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -434,6 +434,10 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnName("sid") .HasDefaultValueSql("find_free_user_sid()"); + b.Property("Timezone") + .HasColumnType("text") + .HasColumnName("timezone"); + b.Property("Username") .IsRequired() .HasColumnType("text") diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index c8c12c5..367e293 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -15,6 +15,7 @@ public class User : BaseModel 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; } = []; diff --git a/Foxnouns.Backend/Properties/launchSettings.json b/Foxnouns.Backend/Properties/launchSettings.json index b680651..b9e2ace 100644 --- a/Foxnouns.Backend/Properties/launchSettings.json +++ b/Foxnouns.Backend/Properties/launchSettings.json @@ -4,6 +4,7 @@ "Development": { "commandName": "Project", "dotnetRunMessages": true, + "hotReloadEnabled": false, "launchBrowser": false, "externalUrlConfiguration": true, "environmentVariables": { @@ -13,6 +14,7 @@ "Production": { "commandName": "Project", "dotnetRunMessages": true, + "hotReloadEnabled": false, "launchBrowser": false, "externalUrlConfiguration": true, "environmentVariables": { diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index c6f9e5b..ceeba94 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -4,6 +4,7 @@ using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using NodaTime; +using Org.BouncyCastle.Ocsp; namespace Foxnouns.Backend.Services; @@ -49,6 +50,13 @@ public class UserRendererService( .ToListAsync(ct) : []; + int? utcOffset = null; + if ( + user.Timezone != null + && TimeZoneInfo.TryFindSystemTimeZoneById(user.Timezone, out var tz) + ) + utcOffset = (int)tz.GetUtcOffset(DateTimeOffset.UtcNow).TotalSeconds; + return new UserResponse( user.Id, user.Sid, @@ -63,6 +71,7 @@ public class UserRendererService( user.Fields, user.CustomPreferences, flags.Select(f => RenderPrideFlag(f.PrideFlag)), + utcOffset, user.Role, renderMembers ? members.Select(m => memberRenderer.RenderPartialMember(m, tokenHidden)) @@ -70,7 +79,8 @@ public class UserRendererService( renderAuthMethods ? authMethods.Select(RenderAuthMethod) : null, tokenHidden ? user.ListHidden : null, tokenHidden ? user.LastActive : null, - tokenHidden ? user.LastSidReroll : null + tokenHidden ? user.LastSidReroll : null, + tokenHidden ? user.Timezone ?? "" : null ); } @@ -115,6 +125,7 @@ public class UserRendererService( IEnumerable Fields, Dictionary CustomPreferences, IEnumerable Flags, + int? UtcOffset, [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] UserRole Role, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] IEnumerable? Members, @@ -124,7 +135,8 @@ public class UserRendererService( bool? MemberListHidden, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] Instant? LastActive, [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - Instant? LastSidReroll + Instant? LastSidReroll, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string? Timezone ); public record AuthMethodResponse( diff --git a/Foxnouns.Frontend/.env.example b/Foxnouns.Frontend/.env.example index d3d5832..d79c672 100644 --- a/Foxnouns.Frontend/.env.example +++ b/Foxnouns.Frontend/.env.example @@ -1,5 +1,7 @@ # Example .env file--DO NOT EDIT PUBLIC_LANGUAGE=en +PUBLIC_BASE_URL=https://pronouns.cc +PUBLIC_SHORT_URL=https://prns.cc PUBLIC_API_BASE=https://pronouns.cc/api PRIVATE_API_HOST=http://localhost:5003/api PRIVATE_INTERNAL_API_HOST=http://localhost:5000/api diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 69d4a1f..142b442 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -38,9 +38,11 @@ "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee", "dependencies": { "@fontsource/firago": "^5.1.0", + "base64-arraybuffer": "^1.0.2", "bootstrap-icons": "^1.11.3", "luxon": "^3.5.0", "markdown-it": "^14.1.0", + "pretty-bytes": "^6.1.1", "sanitize-html": "^2.13.1", "tslog": "^4.9.3" } diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index d9bd974..d35d2ed 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@fontsource/firago': specifier: ^5.1.0 version: 5.1.0 + base64-arraybuffer: + specifier: ^1.0.2 + version: 1.0.2 bootstrap-icons: specifier: ^1.11.3 version: 1.11.3 @@ -20,6 +23,9 @@ importers: markdown-it: specifier: ^14.1.0 version: 14.1.0 + pretty-bytes: + specifier: ^6.1.1 + version: 6.1.1 sanitize-html: specifier: ^2.13.1 version: 2.13.1 @@ -704,6 +710,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + bootstrap-icons@1.11.3: resolution: {integrity: sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==} @@ -1211,6 +1221,10 @@ packages: engines: {node: '>=14'} hasBin: true + pretty-bytes@6.1.1: + resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} + engines: {node: ^14.13.1 || >=16.0.0} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -1938,6 +1952,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + bootstrap-icons@1.11.3: {} bootstrap@5.3.3(@popperjs/core@2.11.8): @@ -2432,6 +2448,8 @@ snapshots: prettier@3.3.3: {} + pretty-bytes@6.1.1: {} + punycode.js@2.3.1: {} punycode@2.3.1: {} diff --git a/Foxnouns.Frontend/src/lib/api/models/user.ts b/Foxnouns.Frontend/src/lib/api/models/user.ts index 715cf46..e32873e 100644 --- a/Foxnouns.Frontend/src/lib/api/models/user.ts +++ b/Foxnouns.Frontend/src/lib/api/models/user.ts @@ -1,5 +1,6 @@ export type PartialUser = { id: string; + sid: string; username: string; display_name: string | null; avatar_url: string | null; @@ -14,17 +15,20 @@ export type User = PartialUser & { 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; }; -export type UserWithMembers = User & { members: PartialMember[] }; +export type UserWithMembers = User & { members: PartialMember[] | null }; export type UserWithHiddenFields = User & { auth_methods?: unknown[]; @@ -38,6 +42,7 @@ export type UserSettings = { export type PartialMember = { id: string; + sid: string; name: string; display_name: string; bio: string | null; diff --git a/Foxnouns.Frontend/src/lib/components/Avatar.svelte b/Foxnouns.Frontend/src/lib/components/Avatar.svelte index 99a5608..31f8355 100644 --- a/Foxnouns.Frontend/src/lib/components/Avatar.svelte +++ b/Foxnouns.Frontend/src/lib/components/Avatar.svelte @@ -1,14 +1,22 @@ + + diff --git a/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte new file mode 100644 index 0000000..5998c9d --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte @@ -0,0 +1,77 @@ + + +

    + +

    + + + + + + +{#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/FormStatusMarker.svelte b/Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte new file mode 100644 index 0000000..43ca9b9 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/FormStatusMarker.svelte @@ -0,0 +1,18 @@ + + +{#if form?.error} + +{:else if form?.ok} +

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

    +{/if} diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index abc4d85..5c026f9 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -1,91 +1,118 @@ { - "hello": "Hello, {{name}}!", - "nav": { - "log-in": "Log in or sign up", - "settings": "Settings" - }, - "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" - }, - "title": { - "log-in": "Log in", - "welcome": "Welcome", - "settings": "Settings" - }, - "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-mastodon": "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" - }, - "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 long, 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" - }, - "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" - }, - "yes": "Yes", - "no": "No" + "hello": "Hello, {{name}}!", + "nav": { + "log-in": "Log in or sign up", + "settings": "Settings" + }, + "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" + }, + "title": { + "log-in": "Log in", + "welcome": "Welcome", + "settings": "Settings" + }, + "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-mastodon": "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" + }, + "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 long, 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" + }, + "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!" + }, + "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" + }, + "save-changes": "Save changes" } diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts index 00c3ef3..82f3cb2 100644 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -6,10 +6,12 @@ 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; if (cookies.get(TOKEN_COOKIE_NAME)) { try { meUser = await apiRequest("GET", "/users/@me", { fetch, cookies }); + token = cookies.get(TOKEN_COOKIE_NAME) || null; } 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); @@ -17,5 +19,5 @@ export const load = (async ({ fetch, cookies }) => { } const meta = await apiRequest("GET", "/meta", { fetch, cookies }); - return { meta, meUser }; + return { meta, meUser, token }; }) satisfies LayoutServerLoad; diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts index 330bd21..6c582bc 100644 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts @@ -1,5 +1,5 @@ import { apiRequest } from "$api"; -import type { UserWithMembers } from "$api/models"; +import type { PartialMember, UserWithMembers } from "$api/models"; export const load = async ({ params, fetch, cookies, url }) => { const user = await apiRequest("GET", `/users/${params.username}`, { @@ -8,12 +8,17 @@ export const load = async ({ params, fetch, cookies, url }) => { }); // Paginate members on the server side - let currentPage = Number(url.searchParams.get("page") || "0"); - const pageCount = Math.ceil(user.members.length / 20); - let members = user.members.slice(currentPage * 20, (currentPage + 1) * 20); - if (members.length === 0) { - members = user.members.slice(0, 20); - currentPage = 0; + let currentPage = 0; + let pageCount = 0; + let members: PartialMember[] = []; + if (user.members) { + currentPage = Number(url.searchParams.get("page") || "0"); + pageCount = Math.ceil(user.members.length / 20); + members = user.members.slice(currentPage * 20, (currentPage + 1) * 20); + if (members.length === 0) { + members = user.members.slice(0, 20); + currentPage = 0; + } } return { user, members, currentPage, pageCount }; diff --git a/Foxnouns.Frontend/src/routes/settings/+layout.server.ts b/Foxnouns.Frontend/src/routes/settings/+layout.server.ts index a1ac93c..fe2eaa3 100644 --- a/Foxnouns.Frontend/src/routes/settings/+layout.server.ts +++ b/Foxnouns.Frontend/src/routes/settings/+layout.server.ts @@ -4,5 +4,5 @@ export const load = async ({ parent }) => { const data = await parent(); if (!data.meUser) redirect(303, "/auth/log-in"); - return { user: data.meUser! }; + return { user: data.meUser!, token: data.token! }; }; diff --git a/Foxnouns.Frontend/src/routes/settings/+page.svelte b/Foxnouns.Frontend/src/routes/settings/+page.svelte index 062d9e6..cfd0b88 100644 --- a/Foxnouns.Frontend/src/routes/settings/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/+page.svelte @@ -29,7 +29,8 @@ {#if form?.ok}

    - Successfully changed your username! + + {$t("settings.username-update-success")}

    {:else if usernameError}

    @@ -46,7 +47,7 @@

    -
    Avatar
    +
    {$t("settings.avatar")}
    + import type { Snippet } from "svelte"; + import { page } from "$app/stores"; + import { t } from "$lib/i18n"; + + type Props = { children: Snippet }; + let { children }: Props = $props(); + + const isActive = (path: string) => $page.url.pathname === path; + + +

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

    + diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts new file mode 100644 index 0000000..2233626 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts @@ -0,0 +1,29 @@ +import { apiRequest, 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; + + let 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; + } + }, +}; diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte new file mode 100644 index 0000000..5b00f0c --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte @@ -0,0 +1,190 @@ + + +{#if error} + +{/if} + +
    +
    +

    {$t("settings.avatar")}

    + +
    +
    +

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

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

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

    +
    +
    + +
    +

    {$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 new file mode 100644 index 0000000..bb86f7e --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.server.ts @@ -0,0 +1,19 @@ +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 new file mode 100644 index 0000000..c3ac2fe --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte @@ -0,0 +1,40 @@ + + +

    Bio

    + + + +
    + + +
    + +

    + {$t("edit-profile.bio-length-hint", { + length: bio.length, + maxLength: data.meta.limits.bio_length, + })} +

    + +{#if bio !== ""} +
    +
    Preview
    +
    {@html renderMarkdown(bio)}
    +
    +{/if} diff --git a/package.json b/package.json index 50681f6..db48a60 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,8 @@ "concurrently": "^9.0.1" }, "scripts": { - "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'cd Foxnouns.Backend && dotnet watch --no-hot-reload' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", + "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'pnpm watch:be' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", + "watch:be": "dotnet watch --no-hot-reload --project Foxnouns.Backend -- --migrate-and-start", "format": "dotnet csharpier . && cd Foxnouns.Frontend && pnpm format" }, "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" From 8bba5f6137d7c47383fc58a5f701b46ab2c33b0d Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 25 Nov 2024 16:15:07 +0100 Subject: [PATCH 125/261] fix: tweak rate limits as just browsing is triggering them --- rate/rate_limiter.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rate/rate_limiter.go b/rate/rate_limiter.go index 28368e2..d223197 100644 --- a/rate/rate_limiter.go +++ b/rate/rate_limiter.go @@ -134,12 +134,12 @@ func (l *Limiter) bucketLimiter(user, method, bucket string) *httprate.RateLimit func requestLimitFor(method string) (int, time.Duration) { switch strings.ToUpper(method) { case "PATCH", "POST": - return 1, time.Second - case "DELETE": - return 1, 5 * time.Second - case "GET": return 3, time.Second + case "DELETE": + return 2, 5 * time.Second + case "GET": + return 10, time.Second default: - return 2, time.Second + return 5, time.Second } } From c0bb76580db097975db228e38a4110ba26bf377c Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 25 Nov 2024 17:35:24 +0100 Subject: [PATCH 126/261] even more frontend stuff --- .../Controllers/MembersController.cs | 6 +- .../Controllers/UsersController.cs | 11 +- .../Services/MemberRendererService.cs | 8 +- .../Services/UserRendererService.cs | 3 +- .../src/lib/components/Avatar.svelte | 11 +- .../src/lib/components/Paginator.svelte | 35 ++++++ .../lib/components/editor/AvatarEditor.svelte | 7 +- .../lib/components/editor/BioEditor.svelte | 26 ++++ .../components/editor/NoscriptWarning.svelte | 12 ++ .../editor/ShortNoscriptWarning.svelte | 11 ++ .../lib/components/editor/SidEditor.svelte | 30 +++++ .../components/profile/user/MemberCard.svelte | 8 +- .../src/lib/i18n/locales/en.json | 30 ++++- .../src/routes/@[username]/+page.server.ts | 11 +- .../src/routes/@[username]/+page.svelte | 30 ++--- .../src/routes/@[username]/Paginator.svelte | 31 ----- .../@[username]/[memberName]/+page.server.ts | 15 +++ .../@[username]/[memberName]/+page.svelte | 40 ++++++ .../routes/settings/members/+page.server.ts | 18 +++ .../src/routes/settings/members/+page.svelte | 48 ++++++++ .../settings/members/[id]/+layout.server.ts | 22 ++++ .../settings/members/[id]/+layout@.svelte | 65 ++++++++++ .../settings/members/[id]/+page.server.ts | 82 +++++++++++++ .../routes/settings/members/[id]/+page.svelte | 115 ++++++++++++++++++ .../settings/members/new/+page.server.ts | 34 ++++++ .../routes/settings/members/new/+page.svelte | 22 ++++ .../routes/settings/profile/+layout.server.ts | 7 ++ .../routes/settings/profile/+layout.svelte | 42 ------- .../routes/settings/profile/+layout@.svelte | 65 ++++++++++ .../routes/settings/profile/+page.server.ts | 20 ++- .../src/routes/settings/profile/+page.svelte | 76 ++++++------ .../routes/settings/profile/bio/+page.svelte | 31 +---- package.json | 2 +- 33 files changed, 796 insertions(+), 178 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/Paginator.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/NoscriptWarning.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/ShortNoscriptWarning.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/SidEditor.svelte delete mode 100644 Foxnouns.Frontend/src/routes/@[username]/Paginator.svelte create mode 100644 Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/members/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/members/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/+layout.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/members/new/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/members/new/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/+layout.server.ts delete mode 100644 Foxnouns.Frontend/src/routes/settings/profile/+layout.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index ba9cf28..968b571 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -303,8 +303,8 @@ public class MembersController( .SetProperty(u => u.LastActive, clock.GetCurrentInstant()) ); - // Re-fetch member to fetch the new sid - var updatedMember = await db.ResolveMemberAsync(CurrentUser!.Id, memberRef); - return Ok(memberRenderer.RenderMember(updatedMember, CurrentToken)); + // Fetch the new sid then pass that to RenderMember + var 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/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 33c38d6..2693bef 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -346,13 +346,20 @@ public class UsersController( .SetProperty(u => u.LastActive, clock.GetCurrentInstant()) ); + // Get the user's new sid + var newSid = await db + .Users.Where(u => u.Id == CurrentUser.Id) + .Select(u => u.Sid) + .FirstAsync(); + var user = await db.ResolveUserAsync(CurrentUser.Id); return Ok( await userRenderer.RenderUserAsync( - user, + CurrentUser, CurrentUser, CurrentToken, - renderMembers: false + renderMembers: false, + overrideSid: newSid ) ); } diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 7d7cac0..717f06c 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -24,13 +24,17 @@ public class MemberRendererService(DatabaseContext db, Config config) return members.Select(m => RenderPartialMember(m, renderUnlisted)); } - public MemberResponse RenderMember(Member member, Token? token = null) + public MemberResponse RenderMember( + Member member, + Token? token = null, + string? overrideSid = null + ) { var renderUnlisted = token?.UserId == member.UserId && token.HasScope("user.read_hidden"); return new MemberResponse( member.Id, - member.Sid, + overrideSid ?? member.Sid, member.Name, member.DisplayName ?? member.Name, member.Bio, diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index ceeba94..2911dd3 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -20,6 +20,7 @@ public class UserRendererService( Token? token = null, bool renderMembers = true, bool renderAuthMethods = false, + string? overrideSid = null, CancellationToken ct = default ) { @@ -59,7 +60,7 @@ public class UserRendererService( return new UserResponse( user.Id, - user.Sid, + overrideSid ?? user.Sid, user.Username, user.DisplayName, user.Bio, diff --git a/Foxnouns.Frontend/src/lib/components/Avatar.svelte b/Foxnouns.Frontend/src/lib/components/Avatar.svelte index 31f8355..99dd8f3 100644 --- a/Foxnouns.Frontend/src/lib/components/Avatar.svelte +++ b/Foxnouns.Frontend/src/lib/components/Avatar.svelte @@ -1,22 +1,23 @@ diff --git a/Foxnouns.Frontend/src/lib/components/Paginator.svelte b/Foxnouns.Frontend/src/lib/components/Paginator.svelte new file mode 100644 index 0000000..07cbd8d --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/Paginator.svelte @@ -0,0 +1,35 @@ + + +{#if pageCount > 1} +
    + + + + + + + + {#each new Array(pageCount) as _, page} + + {page + 1} + + {/each} + + + + + + + +
    +{/if} diff --git a/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte index 5998c9d..05a9a62 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte @@ -4,14 +4,15 @@ import { Icon, InputGroup } from "@sveltestrap/sveltestrap"; import { encode } from "base64-arraybuffer"; import prettyBytes from "pretty-bytes"; + import ShortNoscriptWarning from "./ShortNoscriptWarning.svelte"; type Props = { current: string | null; alt: string; - onclick: (avatar: string) => Promise; + update: (avatar: string) => Promise; updated: boolean; }; - let { current, alt, onclick, updated }: Props = $props(); + let { current, alt, update: onclick, updated }: Props = $props(); const MAX_AVATAR_BYTES = 1_000_000; @@ -59,6 +60,8 @@ + + {#if updated}

    diff --git a/Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte new file mode 100644 index 0000000..8c7e744 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte @@ -0,0 +1,26 @@ + + + + + +

    + {$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/NoscriptWarning.svelte b/Foxnouns.Frontend/src/lib/components/editor/NoscriptWarning.svelte new file mode 100644 index 0000000..4bddf68 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/NoscriptWarning.svelte @@ -0,0 +1,12 @@ + + + diff --git a/Foxnouns.Frontend/src/lib/components/editor/ShortNoscriptWarning.svelte b/Foxnouns.Frontend/src/lib/components/editor/ShortNoscriptWarning.svelte new file mode 100644 index 0000000..fb474db --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/ShortNoscriptWarning.svelte @@ -0,0 +1,11 @@ + + + diff --git a/Foxnouns.Frontend/src/lib/components/editor/SidEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/SidEditor.svelte new file mode 100644 index 0000000..1547ff2 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/SidEditor.svelte @@ -0,0 +1,30 @@ + + +{$t("edit-profile.sid-current")} {sid} + + + + + +

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

    diff --git a/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte b/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte index 7d1bdc7..d77de02 100644 --- a/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte +++ b/Foxnouns.Frontend/src/lib/components/profile/user/MemberCard.svelte @@ -35,11 +35,15 @@
    - +

    - {member.name} + {member.display_name} {#if pronouns}
    diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 5c026f9..7f1e377 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -6,13 +6,14 @@ }, "avatar-tooltip": "Avatar for {{name}}", "profile": { - "edit-member-profile-notice": "You are currently viewing the public profile of {memberName}.", + "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" + "create-member-button": "Create member", + "back-to-user": "Back to {{name}}" }, "title": { "log-in": "Log in", @@ -59,7 +60,10 @@ "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" + "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" }, "settings": { "general-information-tab": "General information", @@ -86,7 +90,9 @@ "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!" + "username-update-success": "Successfully changed your username!", + "create-member-title": "Create a new member", + "create-member-name-label": "Member name" }, "yes": "Yes", "no": "No", @@ -112,7 +118,19 @@ "profile-options-header": "Profile options", "bio-tab": "Bio", "saved-changes": "Successfully saved changes!", - "bio-length-hint": "Using {{length}}/{{maxLength}} characters" + "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 member {{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" }, - "save-changes": "Save changes" + "save-changes": "Save changes", + "change": "Change" } diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts index 6c582bc..99e7359 100644 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.server.ts @@ -1,6 +1,8 @@ import { apiRequest } from "$api"; import type { PartialMember, UserWithMembers } from "$api/models"; +const MEMBERS_PER_PAGE = 20; + export const load = async ({ params, fetch, cookies, url }) => { const user = await apiRequest("GET", `/users/${params.username}`, { fetch, @@ -13,10 +15,13 @@ export const load = async ({ params, fetch, cookies, url }) => { let members: PartialMember[] = []; if (user.members) { currentPage = Number(url.searchParams.get("page") || "0"); - pageCount = Math.ceil(user.members.length / 20); - members = user.members.slice(currentPage * 20, (currentPage + 1) * 20); + pageCount = Math.ceil(user.members.length / MEMBERS_PER_PAGE); + members = user.members.slice( + currentPage * MEMBERS_PER_PAGE, + (currentPage + 1) * MEMBERS_PER_PAGE, + ); if (members.length === 0) { - members = user.members.slice(0, 20); + members = user.members.slice(0, MEMBERS_PER_PAGE); currentPage = 0; } } diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte index 0ed36cc..792df3f 100644 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte @@ -6,7 +6,7 @@ import ProfileFields from "$components/profile/ProfileFields.svelte"; import { t } from "$lib/i18n"; import { Icon } from "@sveltestrap/sveltestrap"; - import Paginator from "./Paginator.svelte"; + import Paginator from "$components/Paginator.svelte"; import MemberCard from "$components/profile/user/MemberCard.svelte"; type Props = { data: PageData }; @@ -25,7 +25,7 @@ {/if} - + {#if data.members.length > 0} @@ -34,27 +34,27 @@ {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]/Paginator.svelte b/Foxnouns.Frontend/src/routes/@[username]/Paginator.svelte deleted file mode 100644 index cf5aa54..0000000 --- a/Foxnouns.Frontend/src/routes/@[username]/Paginator.svelte +++ /dev/null @@ -1,31 +0,0 @@ - - -{#if pageCount > 1} - - - - - - - - - {currentPage + 1} - - - - - - - - -{/if} diff --git a/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.server.ts b/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.server.ts new file mode 100644 index 0000000..f3f8400 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.server.ts @@ -0,0 +1,15 @@ +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 new file mode 100644 index 0000000..a69544a --- /dev/null +++ b/Foxnouns.Frontend/src/routes/@[username]/[memberName]/+page.svelte @@ -0,0 +1,40 @@ + + + + {data.member.display_name} • @{data.member.user.username} • pronouns.cc + + + diff --git a/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts new file mode 100644 index 0000000..220865b --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/+page.server.ts @@ -0,0 +1,18 @@ +const MEMBERS_PER_PAGE = 15; + +export const load = async ({ url, parent }) => { + const { user } = await parent(); + + let currentPage = Number(url.searchParams.get("page") || "0"); + let pageCount = Math.ceil(user.members.length / MEMBERS_PER_PAGE); + let members = user.members.slice( + currentPage * MEMBERS_PER_PAGE, + (currentPage + 1) * MEMBERS_PER_PAGE, + ); + if (members.length === 0) { + members = user.members.slice(0, MEMBERS_PER_PAGE); + currentPage = 0; + } + + return { members, currentPage, pageCount }; +}; diff --git a/Foxnouns.Frontend/src/routes/settings/members/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/+page.svelte new file mode 100644 index 0000000..563cf90 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/+page.svelte @@ -0,0 +1,48 @@ + + +

    {$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 new file mode 100644 index 0000000..0cdf2e1 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout.server.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..a2539ab --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte @@ -0,0 +1,65 @@ + + + + {$t("edit-profile.member-header", { name: data.member.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 new file mode 100644 index 0000000..7665014 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts @@ -0,0 +1,82 @@ +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(); + const bio = body.get("bio") as string | 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; + } + }, +}; diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte new file mode 100644 index 0000000..174108e --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte @@ -0,0 +1,115 @@ + + +{#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.bio-tab")}

    +
    + + +
    +
    diff --git a/Foxnouns.Frontend/src/routes/settings/members/new/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/new/+page.server.ts new file mode 100644 index 0000000..3fdf2e0 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/new/+page.server.ts @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..da17621 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/new/+page.svelte @@ -0,0 +1,22 @@ + + +

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

    + +{#if form?.error} + +{/if} + +
    +
    + + +
    + +
    diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+layout.server.ts b/Foxnouns.Frontend/src/routes/settings/profile/+layout.server.ts new file mode 100644 index 0000000..9d7ef68 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/+layout.server.ts @@ -0,0 +1,7 @@ +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 590d5b5..0000000 --- a/Foxnouns.Frontend/src/routes/settings/profile/+layout.svelte +++ /dev/null @@ -1,42 +0,0 @@ - - -

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

    - diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte b/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte new file mode 100644 index 0000000..12c16d4 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte @@ -0,0 +1,65 @@ + + + + {$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 index 2233626..df356c4 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/settings/profile/+page.server.ts @@ -1,4 +1,4 @@ -import { apiRequest, fastRequest } from "$api"; +import { fastRequest } from "$api"; import ApiError from "$api/error"; import log from "$lib/log.js"; @@ -26,4 +26,22 @@ export const actions = { 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 index 5b00f0c..c603ab6 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/+page.svelte @@ -1,39 +1,29 @@ -

    Bio

    - +

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

    -
    - - + - -

    - {$t("edit-profile.bio-length-hint", { - length: bio.length, - maxLength: data.meta.limits.bio_length, - })} -

    - -{#if bio !== ""} -
    -
    Preview
    -
    {@html renderMarkdown(bio)}
    -
    -{/if} diff --git a/package.json b/package.json index db48a60..60f2ca5 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "concurrently": "^9.0.1" }, "scripts": { - "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'pnpm watch:be' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", "watch:be": "dotnet watch --no-hot-reload --project Foxnouns.Backend -- --migrate-and-start", + "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'pnpm watch:be' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", "format": "dotnet csharpier . && cd Foxnouns.Frontend && pnpm format" }, "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" From c237aa88278be790be482e22d4dbd7de794cd410 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 25 Nov 2024 21:24:28 +0100 Subject: [PATCH 127/261] fix(backend): add unlisted param to patch member --- Foxnouns.Backend/Controllers/MembersController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 968b571..42b8ee5 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -164,6 +164,9 @@ public class MembersController( member.Links = req.Links ?? []; } + if (req.HasProperty(nameof(req.Unlisted))) + member.Unlisted = req.Unlisted ?? false; + if (req.Names != null) { errors.AddRange( @@ -244,6 +247,7 @@ public class MembersController( public Pronoun[]? Pronouns { get; init; } public Field[]? Fields { get; init; } public Snowflake[]? Flags { get; init; } + public bool? Unlisted { get; init; } } [HttpDelete("/api/v2/users/@me/members/{memberRef}")] From 004111feb6226034fe4f31563d6ad6d08b05914a Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 25 Nov 2024 21:25:18 +0100 Subject: [PATCH 128/261] feat(frontend): unlisted toggle on member editor --- .../src/lib/i18n/locales/en.json | 270 +++++++++--------- .../src/routes/@[username]/+page.svelte | 2 +- .../settings/members/[id]/+page.server.ts | 17 ++ .../routes/settings/members/[id]/+page.svelte | 34 ++- 4 files changed, 186 insertions(+), 137 deletions(-) diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 7f1e377..4dd0cb6 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -1,136 +1,138 @@ { - "hello": "Hello, {{name}}!", - "nav": { - "log-in": "Log in or sign up", - "settings": "Settings" - }, - "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}}" - }, - "title": { - "log-in": "Log in", - "welcome": "Welcome", - "settings": "Settings" - }, - "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-mastodon": "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" - }, - "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 long, 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" - }, - "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" - }, - "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 member {{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" - }, - "save-changes": "Save changes", - "change": "Change" + "hello": "Hello, {{name}}!", + "nav": { + "log-in": "Log in or sign up", + "settings": "Settings" + }, + "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}}" + }, + "title": { + "log-in": "Log in", + "welcome": "Welcome", + "settings": "Settings" + }, + "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-mastodon": "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" + }, + "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 long, 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" + }, + "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" + }, + "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 member {{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:" + }, + "save-changes": "Save changes", + "change": "Change" } diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte index 792df3f..f2234c2 100644 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte @@ -25,7 +25,7 @@ {/if} - + {#if data.members.length > 0} diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts index 7665014..252471d 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts @@ -79,4 +79,21 @@ export const actions = { throw e; } }, + options: async ({ params, request, fetch, cookies }) => { + const body = await request.formData(); + let 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 index 174108e..04637ee 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte @@ -5,7 +5,7 @@ import { apiRequest, fastRequest } from "$api"; import ApiError from "$api/error"; import log from "$lib/log"; - import { InputGroup } from "@sveltestrap/sveltestrap"; + import { Icon, InputGroup } from "@sveltestrap/sveltestrap"; import { t } from "$lib/i18n"; import AvatarEditor from "$components/editor/AvatarEditor.svelte"; import ErrorAlert from "$components/ErrorAlert.svelte"; @@ -13,6 +13,7 @@ import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import SidEditor from "$components/editor/SidEditor.svelte"; import BioEditor from "$components/editor/BioEditor.svelte"; + import { PUBLIC_BASE_URL } from "$env/static/public"; type Props = { data: PageData; form: ActionData }; let { data, form }: Props = $props(); @@ -106,7 +107,36 @@

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

    From b6d42fb15d4d2f8c944ecd25d97a62c45db2feca Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 25 Nov 2024 21:43:11 +0100 Subject: [PATCH 129/261] feat(frontend): replace non-working bootstrap tooltips with tippy.js --- Foxnouns.Frontend/package.json | 2 ++ Foxnouns.Frontend/pnpm-lock.yaml | 20 +++++++++++++++++++ Foxnouns.Frontend/src/app.scss | 9 +++++++++ .../src/lib/components/StatusIcon.svelte | 9 +++------ .../lib/components/profile/ProfileFlag.svelte | 13 ++++++------ Foxnouns.Frontend/src/lib/tippy.ts | 10 ++++++++++ 6 files changed, 51 insertions(+), 12 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/tippy.ts diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 142b442..3fc70d1 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -44,6 +44,8 @@ "markdown-it": "^14.1.0", "pretty-bytes": "^6.1.1", "sanitize-html": "^2.13.1", + "svelte-tippy": "^1.3.2", + "tippy.js": "^6.3.7", "tslog": "^4.9.3" } } diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index d35d2ed..9b78513 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -29,6 +29,12 @@ importers: sanitize-html: specifier: ^2.13.1 version: 2.13.1 + svelte-tippy: + specifier: ^1.3.2 + version: 1.3.2 + tippy.js: + specifier: ^6.3.7 + version: 6.3.7 tslog: specifier: ^4.9.3 version: 4.9.3 @@ -1325,6 +1331,9 @@ packages: svelte: optional: true + svelte-tippy@1.3.2: + resolution: {integrity: sha512-41f+85hwhKBRqX0UNYrgFsi34Kk/KDvUkIZXYANxkWoA2NTVTCZbUC2J8hRNZ4TRVxObTshoZRjK2co5+i6LMw==} + svelte@5.2.2: resolution: {integrity: sha512-eHIJRcvA6iuXdRGMESTmBtWTQCcCiol4gyH9DA60ybS35W1x27cvtbndNvWDqX72blyf+AYeQ4gzZ0XGg3L8sw==} engines: {node: '>=18'} @@ -1337,6 +1346,9 @@ packages: tiny-glob@0.2.9: resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2565,6 +2577,10 @@ snapshots: optionalDependencies: svelte: 5.2.2 + svelte-tippy@1.3.2: + dependencies: + tippy.js: 6.3.7 + svelte@5.2.2: dependencies: '@ampproject/remapping': 2.3.0 @@ -2592,6 +2608,10 @@ snapshots: globalyzer: 0.1.0 globrex: 0.1.2 + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 diff --git a/Foxnouns.Frontend/src/app.scss b/Foxnouns.Frontend/src/app.scss index 4a9d5dd..f250ce6 100644 --- a/Foxnouns.Frontend/src/app.scss +++ b/Foxnouns.Frontend/src/app.scss @@ -29,6 +29,15 @@ 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) { diff --git a/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte b/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte index d029158..1dbd5a3 100644 --- a/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte +++ b/Foxnouns.Frontend/src/lib/components/StatusIcon.svelte @@ -1,17 +1,14 @@ - + {preference.tooltip}: -{preference.tooltip} diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte index 5c042cc..bf171cd 100644 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileFlag.svelte @@ -1,17 +1,18 @@ - {flag.description ?? flag.name} - {flag.description + {flag.description {flag.name} diff --git a/Foxnouns.Frontend/src/lib/tippy.ts b/Foxnouns.Frontend/src/lib/tippy.ts new file mode 100644 index 0000000..3fce60d --- /dev/null +++ b/Foxnouns.Frontend/src/lib/tippy.ts @@ -0,0 +1,10 @@ +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", +}); + +export default tippy; From 59496a8cd8e6abed31feaed3572bc73619a71f81 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 25 Nov 2024 23:07:17 +0100 Subject: [PATCH 130/261] feat(frontend): edit names/pronouns --- Foxnouns.Backend/Utils/ValidationUtils.cs | 4 +- .../src/lib/components/Error.svelte | 2 +- .../src/lib/components/IconButton.svelte | 20 ++++ .../lib/components/editor/FieldEditor.svelte | 56 ++++++++++ .../components/editor/FieldEntryEditor.svelte | 65 ++++++++++++ .../editor/PronounEntryEditor.svelte | 100 ++++++++++++++++++ .../components/editor/PronounsEditor.svelte | 65 ++++++++++++ .../src/lib/defaultPronouns/en.ts | 16 +++ .../src/lib/defaultPronouns/index.ts | 7 ++ .../{errorCodes.svelte.ts => errorCodes.ts} | 0 .../src/lib/i18n/locales/en.json | 20 +++- Foxnouns.Frontend/src/lib/tippy.ts | 1 + .../src/routes/@[username]/+page.svelte | 2 +- .../settings/members/[id]/+layout@.svelte | 12 ++- .../settings/members/[id]/+page.server.ts | 3 +- .../members/[id]/names-pronouns/+page.svelte | 52 +++++++++ .../routes/settings/profile/+layout@.svelte | 8 +- .../profile/names-pronouns/+page.svelte | 51 +++++++++ 18 files changed, 470 insertions(+), 14 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/IconButton.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/PronounsEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/defaultPronouns/en.ts create mode 100644 Foxnouns.Frontend/src/lib/defaultPronouns/index.ts rename Foxnouns.Frontend/src/lib/{errorCodes.svelte.ts => errorCodes.ts} (100%) create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index bb225ff..3374e3e 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -414,7 +414,7 @@ public static partial class ValidationUtils case > Limits.FieldEntryTextLimit: errors.Add( ( - $"{errorPrefix}.{entryIdx}.value", + $"{errorPrefix}.{entryIdx}.display_text", ValidationError.LengthError( "Pronoun display text is too long", 1, @@ -427,7 +427,7 @@ public static partial class ValidationUtils case < 1: errors.Add( ( - $"{errorPrefix}.{entryIdx}.value", + $"{errorPrefix}.{entryIdx}.display_text", ValidationError.LengthError( "Pronoun display text is too short", 1, diff --git a/Foxnouns.Frontend/src/lib/components/Error.svelte b/Foxnouns.Frontend/src/lib/components/Error.svelte index 09337a9..64f3bc7 100644 --- a/Foxnouns.Frontend/src/lib/components/Error.svelte +++ b/Foxnouns.Frontend/src/lib/components/Error.svelte @@ -1,6 +1,6 @@ + + diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte new file mode 100644 index 0000000..3fa87b5 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte @@ -0,0 +1,56 @@ + + +

    {name}

    + +{#each entries as _, index} + +{/each} + + + + + diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte new file mode 100644 index 0000000..63e56cc --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte @@ -0,0 +1,65 @@ + + +
    + moveValue(index, true)} + /> + moveValue(index, true)} + /> + + + + + + + + + {#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/PronounEntryEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte new file mode 100644 index 0000000..aee6859 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte @@ -0,0 +1,100 @@ + + +
    +
    + moveValue(index, true)} + /> + moveValue(index, true)} + /> + + + + + + + + + {#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 new file mode 100644 index 0000000..9219c02 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/PronounsEditor.svelte @@ -0,0 +1,65 @@ + + +

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

    + +{#each entries as _, index} + +{/each} + +
    + + + diff --git a/Foxnouns.Frontend/src/lib/defaultPronouns/en.ts b/Foxnouns.Frontend/src/lib/defaultPronouns/en.ts new file mode 100644 index 0000000..412689a --- /dev/null +++ b/Foxnouns.Frontend/src/lib/defaultPronouns/en.ts @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..e60d38b --- /dev/null +++ b/Foxnouns.Frontend/src/lib/defaultPronouns/index.ts @@ -0,0 +1,7 @@ +import enPronouns from "./en"; + +const defaultPronouns = { + en: enPronouns, +} as Record>; + +export default defaultPronouns; diff --git a/Foxnouns.Frontend/src/lib/errorCodes.svelte.ts b/Foxnouns.Frontend/src/lib/errorCodes.ts similarity index 100% rename from Foxnouns.Frontend/src/lib/errorCodes.svelte.ts rename to Foxnouns.Frontend/src/lib/errorCodes.ts diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 4dd0cb6..f743706 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -55,7 +55,7 @@ "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 long, minimum length is {{min}}, 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", @@ -123,7 +123,7 @@ "fields-tab": "Fields", "flags-links-tab": "Flags & links", "back-to-settings-tab": "Back to settings", - "member-header": "Editing member {{name}}", + "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", @@ -131,8 +131,20 @@ "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:" + "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" }, "save-changes": "Save changes", - "change": "Change" + "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." + } } diff --git a/Foxnouns.Frontend/src/lib/tippy.ts b/Foxnouns.Frontend/src/lib/tippy.ts index 3fce60d..39e2ca0 100644 --- a/Foxnouns.Frontend/src/lib/tippy.ts +++ b/Foxnouns.Frontend/src/lib/tippy.ts @@ -5,6 +5,7 @@ import { createTippy } from "svelte-tippy"; // 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/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte index f2234c2..903312d 100644 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte @@ -33,7 +33,7 @@

    {data.user.member_title || $t("profile.default-members-header")} {#if isMeUser} - + {$t("profile.create-member-button")} diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte index a2539ab..7de4046 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+layout@.svelte @@ -8,14 +8,20 @@ let { data, children }: Props = $props(); const isActive = (path: string) => $page.url.pathname === path; + + let name = $derived( + data.member.display_name === data.member.name + ? data.member.name + : `${data.member.display_name} (${data.member.name})`, + ); - {$t("edit-profile.member-header", { name: data.member.name })} • pronouns.cc + {$t("edit-profile.member-header", { name })} • pronouns.cc
    -

    {$t("edit-profile.member-header", { name: data.member.name })}

    +

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

    @@ -51,7 +57,7 @@ href="/@{data.user.username}/{data.member.name}" class="list-group-item list-group-item-action text-danger" > - Back to member + {$t("edit-profile.back-to-profile-tab")} {$t("edit-profile.back-to-settings-tab")} diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts index 252471d..3ff5ac3 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.server.ts @@ -64,7 +64,8 @@ export const actions = { }, bio: async ({ params, request, fetch, cookies }) => { const body = await request.formData(); - const bio = body.get("bio") as string | null; + let bio = body.get("bio") as string | null; + if (!bio || bio === "") bio = null; try { await fastRequest("PATCH", `/users/@me/members/${params.id}`, { 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 new file mode 100644 index 0000000..e21bffc --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte @@ -0,0 +1,52 @@ + + + + +
    + +
    + +
    + +
    + +
    + +
    diff --git a/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte b/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte index 12c16d4..6f3d337 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/+layout@.svelte @@ -2,9 +2,10 @@ import type { Snippet } from "svelte"; import { page } from "$app/stores"; import { t } from "$lib/i18n"; + import type { LayoutData } from "./$types"; - type Props = { children: Snippet }; - let { children }: Props = $props(); + type Props = { data: LayoutData; children: Snippet }; + let { data, children }: Props = $props(); const isActive = (path: string) => $page.url.pathname === path; @@ -53,6 +54,9 @@ > {$t("edit-profile.flags-links-tab")}
    + + {$t("edit-profile.back-to-profile-tab")} + {$t("edit-profile.back-to-settings-tab")} diff --git a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte new file mode 100644 index 0000000..4954876 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte @@ -0,0 +1,51 @@ + + + + +
    + +
    + +
    + +
    + +
    + +
    From 7c52ab759c5e8d12e42a945eb78241973e8285f4 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 25 Nov 2024 23:12:19 +0100 Subject: [PATCH 131/261] tiny readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de5b9b8..ea28fc8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Foxnouns.NET -Rewrite of pronouns.cc's codebase in C#, using Remix for the frontend. +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 From f435ad4cf55b7c4103ad86b811446db2643805d6 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 27 Nov 2024 19:50:45 +0100 Subject: [PATCH 132/261] feat(frontend): fields editor --- .../lib/components/editor/FieldEditor.svelte | 59 +++- .../components/editor/FieldEntryEditor.svelte | 2 +- .../lib/components/editor/FieldsEditor.svelte | 79 +++++ .../src/lib/i18n/locales/en.json | 303 +++++++++--------- .../members/[id]/names-pronouns/+page.svelte | 4 +- .../settings/profile/fields/+page.svelte | 32 ++ .../profile/names-pronouns/+page.svelte | 6 +- 7 files changed, 319 insertions(+), 166 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte index 3fa87b5..c2536dd 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte @@ -2,14 +2,25 @@ import type { CustomPreference, FieldEntry } from "$api/models"; import IconButton from "$components/IconButton.svelte"; import { t } from "$lib/i18n"; + import { InputGroup, InputGroupText } from "@sveltestrap/sveltestrap"; import FieldEntryEditor from "./FieldEntryEditor.svelte"; type Props = { name: string; entries: FieldEntry[]; allPreferences: Record; + index?: number; + move?: (index: number, up: boolean) => void; + remove?: (index: number) => void; }; - let { name, entries = $bindable(), allPreferences }: Props = $props(); + let { + name = $bindable(), + entries = $bindable(), + allPreferences, + index, + move, + remove, + }: Props = $props(); let newEntry = $state(""); @@ -38,19 +49,45 @@ }; -

    {name}

    +{#if index !== undefined && move && remove} +
    + + move(index, true)} + /> + move(index, false)} + /> + {$t("editor.field-name")} + + remove(index)} + /> + +
    +{:else} +

    {name}

    +{/if} -{#each entries as _, index} - +{#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 index 63e56cc..65c2355 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte @@ -38,7 +38,7 @@ icon="chevron-down" color="secondary" tooltip={$t("editor.move-entry-down")} - onclick={() => moveValue(index, true)} + onclick={() => moveValue(index, false)} /> diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte new file mode 100644 index 0000000..6bbba9c --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte @@ -0,0 +1,79 @@ + + + + + +

    {$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/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index f743706..50662d1 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -1,150 +1,157 @@ { - "hello": "Hello, {{name}}!", - "nav": { - "log-in": "Log in or sign up", - "settings": "Settings" - }, - "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}}" - }, - "title": { - "log-in": "Log in", - "welcome": "Welcome", - "settings": "Settings" - }, - "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-mastodon": "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" - }, - "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" - }, - "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" - }, - "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" - }, - "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." - } + "hello": "Hello, {{name}}!", + "nav": { + "log-in": "Log in or sign up", + "settings": "Settings" + }, + "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}}" + }, + "title": { + "log-in": "Log in", + "welcome": "Welcome", + "settings": "Settings" + }, + "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-mastodon": "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" + }, + "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" + }, + "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" + }, + "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" + } } 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 index e21bffc..19ed7e5 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/names-pronouns/+page.svelte @@ -5,6 +5,7 @@ import { mergePreferences } from "$api/models/user"; import FieldEditor from "$components/editor/FieldEditor.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; + import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; import PronounsEditor from "$components/editor/PronounsEditor.svelte"; import { t } from "$lib/i18n"; import log from "$lib/log"; @@ -37,16 +38,15 @@ }; +
    -
    -
    diff --git a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte new file mode 100644 index 0000000..8bab783 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte @@ -0,0 +1,32 @@ + + + diff --git a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte index 4954876..e22c5d5 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/names-pronouns/+page.svelte @@ -4,6 +4,7 @@ import { mergePreferences, type User } from "$api/models/user"; import FieldEditor from "$components/editor/FieldEditor.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; + import NoscriptWarning from "$components/editor/NoscriptWarning.svelte"; import PronounsEditor from "$components/editor/PronounsEditor.svelte"; import { t } from "$lib/i18n"; import log from "$lib/log"; @@ -14,9 +15,7 @@ let names = $state(data.user.names); let pronouns = $state(data.user.pronouns); - let ok: { ok: boolean; error: RawApiError | null } | null = $state(null); - let allPreferences = $derived(mergePreferences(data.user.custom_preferences)); const update = async () => { @@ -36,16 +35,15 @@ }; +
    -
    -
    From 71b59dbb0022ad921bd3c465a949ffbab21cd7a3 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 27 Nov 2024 20:00:28 +0100 Subject: [PATCH 133/261] feat: add icon list generation script this is used to validate icons for custom preferences. it generates both typescript and c# code --- .editorconfig | 3 + .../Utils/BootstrapIcons.Icons.generated.cs | 2060 +++++++++++++++++ Foxnouns.Backend/Utils/BootstrapIcons.cs | 6 + Foxnouns.Frontend/.prettierignore | 1 + Foxnouns.Frontend/icons.js | 42 + Foxnouns.Frontend/src/lib/icons.ts | 4 + 6 files changed, 2116 insertions(+) create mode 100644 Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs create mode 100644 Foxnouns.Backend/Utils/BootstrapIcons.cs create mode 100644 Foxnouns.Frontend/icons.js create mode 100644 Foxnouns.Frontend/src/lib/icons.ts diff --git a/.editorconfig b/.editorconfig index 0229143..1ecf322 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,3 +3,6 @@ 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 + +[*generated.cs] +generated_code = true diff --git a/Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs b/Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs new file mode 100644 index 0000000..6f0b2be --- /dev/null +++ b/Foxnouns.Backend/Utils/BootstrapIcons.Icons.generated.cs @@ -0,0 +1,2060 @@ +// + +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 new file mode 100644 index 0000000..9eafe9c --- /dev/null +++ b/Foxnouns.Backend/Utils/BootstrapIcons.cs @@ -0,0 +1,6 @@ +namespace Foxnouns.Backend.Utils; + +public static partial class BootstrapIcons +{ + public static bool IsValid(string icon) => Icons.Contains(icon); +} diff --git a/Foxnouns.Frontend/.prettierignore b/Foxnouns.Frontend/.prettierignore index ab78a95..11ea406 100644 --- a/Foxnouns.Frontend/.prettierignore +++ b/Foxnouns.Frontend/.prettierignore @@ -2,3 +2,4 @@ package-lock.json pnpm-lock.yaml yarn.lock +src/lib/icons.ts diff --git a/Foxnouns.Frontend/icons.js b/Foxnouns.Frontend/icons.js new file mode 100644 index 0000000..3efc0a4 --- /dev/null +++ b/Foxnouns.Frontend/icons.js @@ -0,0 +1,42 @@ +// 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 `pnpm 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/src/lib/icons.ts b/Foxnouns.Frontend/src/lib/icons.ts new file mode 100644 index 0000000..daf700e --- /dev/null +++ b/Foxnouns.Frontend/src/lib/icons.ts @@ -0,0 +1,4 @@ +// 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 From 8b1d5b2c1b6a9afface067ada3b8d9b844b7ac0e Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 28 Nov 2024 17:28:52 +0100 Subject: [PATCH 134/261] feat(backend): validate custom preferences on save --- .../Controllers/MetaController.cs | 2 +- .../Controllers/UsersController.cs | 34 +- .../Utils/ValidationUtils.Fields.cs | 287 +++++++++++ .../Utils/ValidationUtils.Preferences.cs | 72 +++ .../Utils/ValidationUtils.Strings.cs | 197 ++++++++ Foxnouns.Backend/Utils/ValidationUtils.cs | 445 ------------------ 6 files changed, 560 insertions(+), 477 deletions(-) create mode 100644 Foxnouns.Backend/Utils/ValidationUtils.Fields.cs create mode 100644 Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs create mode 100644 Foxnouns.Backend/Utils/ValidationUtils.Strings.cs diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 411e2e8..d699fc2 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -27,7 +27,7 @@ public class MetaController : ApiControllerBase new Limits( MemberCount: MembersController.MaxMemberCount, BioLength: ValidationUtils.MaxBioLength, - CustomPreferences: UsersController.MaxCustomPreferences + CustomPreferences: ValidationUtils.MaxCustomPreferences ) ) ); diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 2693bef..aa8d02d 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -197,11 +197,11 @@ public class UsersController( [Authorize("user.update")] [ProducesResponseType>(StatusCodes.Status200OK)] public async Task UpdateCustomPreferencesAsync( - [FromBody] List req, + [FromBody] List req, CancellationToken ct = default ) { - ValidationUtils.Validate(ValidateCustomPreferences(req)); + ValidationUtils.Validate(ValidationUtils.ValidateCustomPreferences(req)); var user = await db.ResolveUserAsync(CurrentUser!.Id, ct); var preferences = user @@ -241,7 +241,7 @@ public class UsersController( } [SuppressMessage("ReSharper", "ClassNeverInstantiated.Global")] - public class CustomPreferencesUpdateRequest + public class CustomPreferenceUpdate { public Snowflake? Id { get; init; } public required string Icon { get; set; } @@ -251,34 +251,6 @@ public class UsersController( public bool Favourite { get; set; } } - public const int MaxCustomPreferences = 25; - - private 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; - - // TODO: validate individual preferences - - return errors; - } - public class UpdateUserRequest : PatchRequest { public string? Username { get; init; } diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs new file mode 100644 index 0000000..1ed083c --- /dev/null +++ b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs @@ -0,0 +1,287 @@ +// 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.Utils; + +public static partial class ValidationUtils +{ + private static readonly string[] DefaultStatusOptions = + [ + "favourite", + "okay", + "jokingly", + "friends_only", + "avoid", + ]; + + public static IEnumerable<(string, ValidationError?)> ValidateFields( + List? fields, + IReadOnlyDictionary customPreferences + ) + { + if (fields == null) + return []; + + var errors = new List<(string, ValidationError?)>(); + if (fields.Count > 25) + errors.Add( + ( + "fields", + ValidationError.LengthError( + "Too many fields", + 0, + Limits.FieldLimit, + fields.Count + ) + ) + ); + // No overwhelming this function, thank you + if (fields.Count > 100) + return errors; + + foreach (var (field, index) in fields.Select((field, index) => (field, index))) + { + switch (field.Name.Length) + { + case > Limits.FieldNameLimit: + errors.Add( + ( + $"fields.{index}.name", + ValidationError.LengthError( + "Field name is too long", + 1, + Limits.FieldNameLimit, + field.Name.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"fields.{index}.name", + ValidationError.LengthError( + "Field name is too short", + 1, + Limits.FieldNameLimit, + field.Name.Length + ) + ) + ); + break; + } + + errors = errors + .Concat( + ValidateFieldEntries( + field.Entries, + customPreferences, + $"fields.{index}.entries" + ) + ) + .ToList(); + } + + return errors; + } + + public static 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.FieldEntriesLimit) + errors.Add( + ( + errorPrefix, + ValidationError.LengthError( + "Field has too many entries", + 0, + Limits.FieldEntriesLimit, + entries.Length + ) + ) + ); + + // Same as above, no overwhelming this function with a ridiculous amount of entries + if (entries.Length > Limits.FieldEntriesLimit + 50) + return errors; + + foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) + { + switch (entry.Value.Length) + { + case > Limits.FieldEntryTextLimit: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Field value is too long", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Field value is too short", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + } + + var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; + + if ( + !DefaultStatusOptions.Contains(entry.Status) + && !customPreferenceIds.Contains(entry.Status) + ) + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.status", + ValidationError.GenericValidationError("Invalid status", entry.Status) + ) + ); + } + + return errors; + } + + public static 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.FieldEntriesLimit) + errors.Add( + ( + errorPrefix, + ValidationError.LengthError( + "Too many pronouns", + 0, + Limits.FieldEntriesLimit, + entries.Length + ) + ) + ); + + // Same as above, no overwhelming this function with a ridiculous amount of entries + if (entries.Length > Limits.FieldEntriesLimit + 50) + return errors; + + foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) + { + switch (entry.Value.Length) + { + case > Limits.FieldEntryTextLimit: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Pronoun value is too long", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Pronoun value is too short", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + } + + if (entry.DisplayText != null) + { + switch (entry.DisplayText.Length) + { + case > Limits.FieldEntryTextLimit: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.display_text", + ValidationError.LengthError( + "Pronoun display text is too long", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + case < 1: + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.display_text", + ValidationError.LengthError( + "Pronoun display text is too short", + 1, + Limits.FieldEntryTextLimit, + entry.Value.Length + ) + ) + ); + break; + } + } + + var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; + + 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/Utils/ValidationUtils.Preferences.cs b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs new file mode 100644 index 0000000..379a552 --- /dev/null +++ b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs @@ -0,0 +1,72 @@ +// 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.Controllers; + +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 (var (p, 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 new file mode 100644 index 0000000..0193b7e --- /dev/null +++ b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs @@ -0,0 +1,197 @@ +// 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.Utils; + +public static partial class ValidationUtils +{ + private static readonly string[] InvalidUsernames = + [ + "..", + "admin", + "administrator", + "mod", + "moderator", + "api", + "page", + "pronouns", + "settings", + "pronouns.cc", + "pronounscc", + ]; + + private static readonly string[] InvalidMemberNames = + [ + // these break routing outright + ".", + "..", + // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible + "edit", + ]; + + public static ValidationError? ValidateUsername(string username) + { + if (!UsernameRegex().IsMatch(username)) + return username.Length switch + { + < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), + > 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length), + _ => 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 static ValidationError? ValidateMemberName(string memberName) + { + if (!MemberRegex().IsMatch(memberName)) + return memberName.Length switch + { + < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), + > 100 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), + _ => 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 static ValidationError? ValidateDisplayName(string? displayName) + { + return displayName?.Length switch + { + 0 => ValidationError.LengthError( + "Display name is too short", + 1, + 100, + displayName.Length + ), + > 100 => ValidationError.LengthError( + "Display name is too long", + 1, + 100, + displayName.Length + ), + _ => null, + }; + } + + private const int MaxLinks = 25; + private const int MaxLinkLength = 256; + + public static IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) + { + if (links == null) + return []; + if (links.Length > MaxLinks) + return + [ + ("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length)), + ]; + + var errors = new List<(string, ValidationError?)>(); + foreach (var (link, idx) in links.Select((l, i) => (l, i))) + { + switch (link.Length) + { + case 0: + errors.Add( + ( + $"links.{idx}", + ValidationError.LengthError("Link cannot be empty", 1, 256, 0) + ) + ); + break; + case > MaxLinkLength: + errors.Add( + ( + $"links.{idx}", + ValidationError.LengthError( + "Link is too long", + 1, + MaxLinkLength, + link.Length + ) + ) + ); + break; + } + } + + return errors; + } + + public const int MaxBioLength = 1024; + public const int MaxAvatarLength = 1_500_000; + + public static ValidationError? ValidateBio(string? bio) + { + return bio?.Length switch + { + 0 => ValidationError.LengthError("Bio is too short", 1, MaxBioLength, bio.Length), + > MaxBioLength => ValidationError.LengthError( + "Bio is too long", + 1, + MaxBioLength, + bio.Length + ), + _ => null, + }; + } + + public static ValidationError? ValidateAvatar(string? avatar) + { + return avatar?.Length switch + { + 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), + > MaxAvatarLength => ValidationError.GenericValidationError( + "Avatar is too large", + null + ), + _ => null, + }; + } + + [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] + private static partial Regex UsernameRegex(); + + [GeneratedRegex( + """^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", + RegexOptions.IgnoreCase, + "en-NL" + )] + private static partial Regex MemberRegex(); +} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index 3374e3e..3e0acd5 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -1,7 +1,3 @@ -using System.Text.RegularExpressions; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; - namespace Foxnouns.Backend.Utils; /// @@ -9,76 +5,6 @@ namespace Foxnouns.Backend.Utils; /// public static partial class ValidationUtils { - private static readonly string[] InvalidUsernames = - [ - "..", - "admin", - "administrator", - "mod", - "moderator", - "api", - "page", - "pronouns", - "settings", - "pronouns.cc", - "pronounscc", - ]; - - private static readonly string[] InvalidMemberNames = - [ - // these break routing outright - ".", - "..", - // the user edit page lives at `/@{username}/edit`, so a member named "edit" would be inaccessible - "edit", - ]; - - public static ValidationError? ValidateUsername(string username) - { - if (!UsernameRegex().IsMatch(username)) - return username.Length switch - { - < 2 => ValidationError.LengthError("Username is too short", 2, 40, username.Length), - > 40 => ValidationError.LengthError("Username is too long", 2, 40, username.Length), - _ => 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 static ValidationError? ValidateMemberName(string memberName) - { - if (!MemberRegex().IsMatch(memberName)) - return memberName.Length switch - { - < 1 => ValidationError.LengthError("Name is too short", 1, 100, memberName.Length), - > 100 => ValidationError.LengthError("Name is too long", 1, 100, memberName.Length), - _ => 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 static void Validate(IEnumerable<(string, ValidationError?)> errors) { errors = errors.Where(e => e.Item2 != null).ToList(); @@ -95,375 +21,4 @@ public static partial class ValidationUtils throw new ApiError.BadRequest("Error validating input", errorDict); } - - public static ValidationError? ValidateDisplayName(string? displayName) - { - return displayName?.Length switch - { - 0 => ValidationError.LengthError( - "Display name is too short", - 1, - 100, - displayName.Length - ), - > 100 => ValidationError.LengthError( - "Display name is too long", - 1, - 100, - displayName.Length - ), - _ => null, - }; - } - - private const int MaxLinks = 25; - private const int MaxLinkLength = 256; - - public static IEnumerable<(string, ValidationError?)> ValidateLinks(string[]? links) - { - if (links == null) - return []; - if (links.Length > MaxLinks) - return - [ - ("links", ValidationError.LengthError("Too many links", 0, MaxLinks, links.Length)), - ]; - - var errors = new List<(string, ValidationError?)>(); - foreach (var (link, idx) in links.Select((l, i) => (l, i))) - { - switch (link.Length) - { - case 0: - errors.Add( - ( - $"links.{idx}", - ValidationError.LengthError("Link cannot be empty", 1, 256, 0) - ) - ); - break; - case > MaxLinkLength: - errors.Add( - ( - $"links.{idx}", - ValidationError.LengthError( - "Link is too long", - 1, - MaxLinkLength, - link.Length - ) - ) - ); - break; - } - } - - return errors; - } - - public const int MaxBioLength = 1024; - public const int MaxAvatarLength = 1_500_000; - - public static ValidationError? ValidateBio(string? bio) - { - return bio?.Length switch - { - 0 => ValidationError.LengthError("Bio is too short", 1, MaxBioLength, bio.Length), - > MaxBioLength => ValidationError.LengthError( - "Bio is too long", - 1, - MaxBioLength, - bio.Length - ), - _ => null, - }; - } - - public static ValidationError? ValidateAvatar(string? avatar) - { - return avatar?.Length switch - { - 0 => ValidationError.GenericValidationError("Avatar cannot be empty", null), - > MaxAvatarLength => ValidationError.GenericValidationError( - "Avatar is too large", - null - ), - _ => null, - }; - } - - private static readonly string[] DefaultStatusOptions = - [ - "favourite", - "okay", - "jokingly", - "friends_only", - "avoid", - ]; - - public static IEnumerable<(string, ValidationError?)> ValidateFields( - List? fields, - IReadOnlyDictionary customPreferences - ) - { - if (fields == null) - return []; - - var errors = new List<(string, ValidationError?)>(); - if (fields.Count > 25) - errors.Add( - ( - "fields", - ValidationError.LengthError( - "Too many fields", - 0, - Limits.FieldLimit, - fields.Count - ) - ) - ); - // No overwhelming this function, thank you - if (fields.Count > 100) - return errors; - - foreach (var (field, index) in fields.Select((field, index) => (field, index))) - { - switch (field.Name.Length) - { - case > Limits.FieldNameLimit: - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too long", - 1, - Limits.FieldNameLimit, - field.Name.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too short", - 1, - Limits.FieldNameLimit, - field.Name.Length - ) - ) - ); - break; - } - - errors = errors - .Concat( - ValidateFieldEntries( - field.Entries, - customPreferences, - $"fields.{index}.entries" - ) - ) - .ToList(); - } - - return errors; - } - - public static 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.FieldEntriesLimit) - errors.Add( - ( - errorPrefix, - ValidationError.LengthError( - "Field has too many entries", - 0, - Limits.FieldEntriesLimit, - entries.Length - ) - ) - ); - - // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > Limits.FieldEntriesLimit + 50) - return errors; - - foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) - { - switch (entry.Value.Length) - { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too short", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - } - - var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; - - if ( - !DefaultStatusOptions.Contains(entry.Status) - && !customPreferenceIds.Contains(entry.Status) - ) - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.status", - ValidationError.GenericValidationError("Invalid status", entry.Status) - ) - ); - } - - return errors; - } - - public static 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.FieldEntriesLimit) - errors.Add( - ( - errorPrefix, - ValidationError.LengthError( - "Too many pronouns", - 0, - Limits.FieldEntriesLimit, - entries.Length - ) - ) - ); - - // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > Limits.FieldEntriesLimit + 50) - return errors; - - foreach (var (entry, entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx))) - { - switch (entry.Value.Length) - { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too short", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - } - - if (entry.DisplayText != null) - { - switch (entry.DisplayText.Length) - { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too short", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) - ) - ); - break; - } - } - - var customPreferenceIds = customPreferences?.Keys.Select(id => id.ToString()) ?? []; - - if ( - !DefaultStatusOptions.Contains(entry.Status) - && !customPreferenceIds.Contains(entry.Status) - ) - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.status", - ValidationError.GenericValidationError("Invalid status", entry.Status) - ) - ); - } - - return errors; - } - - [GeneratedRegex(@"^[a-zA-Z_0-9\-\.]{2,40}$", RegexOptions.IgnoreCase, "en-NL")] - private static partial Regex UsernameRegex(); - - [GeneratedRegex( - """^[^@'$%&()+<=>^|~`,*!#/\\\[\]""\{\}\?]{1,100}$""", - RegexOptions.IgnoreCase, - "en-NL" - )] - private static partial Regex MemberRegex(); } From 4780be301997ce3c94b355b982658ae91598d236 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 28 Nov 2024 21:29:25 +0100 Subject: [PATCH 135/261] fix(backend): add unique index to auth methods --- Foxnouns.Backend/Database/DatabaseContext.cs | 16 +++++++ ...20241128202508_AddAuthMethodUniqueIndex.cs | 47 +++++++++++++++++++ .../DatabaseContextModelSnapshot.cs | 10 ++++ 3 files changed, 73 insertions(+) create mode 100644 Foxnouns.Backend/Database/Migrations/20241128202508_AddAuthMethodUniqueIndex.cs diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index e6dc524..dae8b28 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -71,6 +71,22 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); modelBuilder.Entity().HasIndex(m => m.Sid).IsUnique(); modelBuilder.Entity().HasIndex(k => k.Key).IsUnique(); + modelBuilder + .Entity() + .HasIndex(m => new + { + m.AuthType, + m.RemoteId, + m.FediverseApplicationId, + }) + .HasFilter("fediverse_application_id IS NOT NULL") + .IsUnique(); + + modelBuilder + .Entity() + .HasIndex(m => new { m.AuthType, m.RemoteId }) + .HasFilter("fediverse_application_id IS NULL") + .IsUnique(); modelBuilder.Entity().Property(u => u.Sid).HasDefaultValueSql("find_free_user_sid()"); modelBuilder.Entity().Property(u => u.Fields).HasColumnType("jsonb"); diff --git a/Foxnouns.Backend/Database/Migrations/20241128202508_AddAuthMethodUniqueIndex.cs b/Foxnouns.Backend/Database/Migrations/20241128202508_AddAuthMethodUniqueIndex.cs new file mode 100644 index 0000000..f6a00b5 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20241128202508_AddAuthMethodUniqueIndex.cs @@ -0,0 +1,47 @@ +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index d012fe0..f9f1609 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -98,6 +98,16 @@ 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); }); From de733a06824fdf0d15620c4e0a2b5609f245ef34 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 28 Nov 2024 21:35:55 +0100 Subject: [PATCH 136/261] feat(frontend): discord registration/login/linking also moves the registration form found on the mastodon callback page into a component so we're not repeating the same code for every auth method --- .../Authentication/AuthController.cs | 6 +- .../Controllers/MetaController.cs | 10 +- Foxnouns.Backend/Services/Auth/AuthService.cs | 9 + Foxnouns.Frontend/src/lib/actions/register.ts | 35 ++ Foxnouns.Frontend/src/lib/api/models/auth.ts | 9 +- Foxnouns.Frontend/src/lib/api/models/meta.ts | 1 + Foxnouns.Frontend/src/lib/api/models/user.ts | 4 +- .../components/settings/AuthMethodList.svelte | 24 ++ .../components/settings/AuthMethodRow.svelte | 26 ++ .../components/settings/NewAuthMethod.svelte | 34 ++ .../settings/OauthRegistrationForm.svelte | 35 ++ .../src/lib/i18n/locales/en.json | 321 +++++++++--------- .../auth/callback/discord/+page.server.ts | 64 ++++ .../routes/auth/callback/discord/+page.svelte | 31 ++ .../mastodon/[instance]/+page.server.ts | 35 +- .../callback/mastodon/[instance]/+page.svelte | 29 +- .../src/routes/settings/auth/+page.server.ts | 7 + .../src/routes/settings/auth/+page.svelte | 65 ++++ .../settings/auth/add-discord/+page.server.ts | 12 + 19 files changed, 545 insertions(+), 212 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/actions/register.ts create mode 100644 Foxnouns.Frontend/src/lib/components/settings/AuthMethodList.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/settings/OauthRegistrationForm.svelte create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/discord/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/discord/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/auth/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/auth/add-discord/+page.server.ts diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index abb403c..bc34c9f 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -5,6 +5,7 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Middleware; using Foxnouns.Backend.Services; +using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; @@ -56,9 +57,10 @@ public class AuthController( public record AddOauthAccountResponse( Snowflake Id, - AuthType Type, + [property: JsonConverter(typeof(ScreamingSnakeCaseEnumConverter))] AuthType Type, string RemoteId, - string? RemoteUsername + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + string? RemoteUsername ); public record OauthRegisterRequest(string Ticket, string Username); diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index d699fc2..76132ee 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -27,7 +27,8 @@ public class MetaController : ApiControllerBase new Limits( MemberCount: MembersController.MaxMemberCount, BioLength: ValidationUtils.MaxBioLength, - CustomPreferences: ValidationUtils.MaxCustomPreferences + CustomPreferences: ValidationUtils.MaxCustomPreferences, + MaxAuthMethods: AuthUtils.MaxAuthMethodsPerType ) ) ); @@ -49,5 +50,10 @@ public class MetaController : ApiControllerBase private record UserInfo(int Total, int ActiveMonth, int ActiveWeek, int ActiveDay); // All limits that the frontend should know about (for UI purposes) - private record Limits(int MemberCount, int BioLength, int CustomPreferences); + private record Limits( + int MemberCount, + int BioLength, + int CustomPreferences, + int MaxAuthMethods + ); } diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs index adbf5b1..4eca66e 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -223,6 +223,15 @@ public class AuthService( { AssertValidAuthType(authType, null); + // This is already checked when + var 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 3 per account." + ); + var authMethod = new AuthMethod { Id = snowflakeGenerator.GenerateSnowflake(), diff --git a/Foxnouns.Frontend/src/lib/actions/register.ts b/Foxnouns.Frontend/src/lib/actions/register.ts new file mode 100644 index 0000000..d3c126d --- /dev/null +++ b/Foxnouns.Frontend/src/lib/actions/register.ts @@ -0,0 +1,35 @@ +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, type RequestEvent } from "@sveltejs/kit"; + +export default function createRegisterAction(callbackUrl: string) { + return async function ({ request, fetch, cookies }: RequestEvent) { + 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", callbackUrl, { + 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/lib/api/models/auth.ts b/Foxnouns.Frontend/src/lib/api/models/auth.ts index 3ea7cab..2129425 100644 --- a/Foxnouns.Frontend/src/lib/api/models/auth.ts +++ b/Foxnouns.Frontend/src/lib/api/models/auth.ts @@ -1,4 +1,4 @@ -import type { User } from "./user"; +import type { AuthType, User } from "./user"; export type AuthResponse = { user: User; @@ -21,3 +21,10 @@ export type AuthUrls = { 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/meta.ts b/Foxnouns.Frontend/src/lib/api/models/meta.ts index f822478..eb77a31 100644 --- a/Foxnouns.Frontend/src/lib/api/models/meta.ts +++ b/Foxnouns.Frontend/src/lib/api/models/meta.ts @@ -16,4 +16,5 @@ export type Limits = { member_count: number; bio_length: number; custom_preferences: number; + max_auth_methods: number; }; diff --git a/Foxnouns.Frontend/src/lib/api/models/user.ts b/Foxnouns.Frontend/src/lib/api/models/user.ts index e32873e..d2deb8f 100644 --- a/Foxnouns.Frontend/src/lib/api/models/user.ts +++ b/Foxnouns.Frontend/src/lib/api/models/user.ts @@ -71,9 +71,11 @@ export type PrideFlag = { description: string | null; }; +export type AuthType = "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; + export type AuthMethod = { id: string; - type: "DISCORD" | "GOOGLE" | "TUMBLR" | "FEDIVERSE" | "EMAIL"; + type: AuthType; remote_id: string; remote_username?: string; }; diff --git a/Foxnouns.Frontend/src/lib/components/settings/AuthMethodList.svelte b/Foxnouns.Frontend/src/lib/components/settings/AuthMethodList.svelte new file mode 100644 index 0000000..e889df2 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/settings/AuthMethodList.svelte @@ -0,0 +1,24 @@ + + +{#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 new file mode 100644 index 0000000..62c5d6f --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte @@ -0,0 +1,26 @@ + + +
    +
    +
    + {name} + {#if showId}({method.remote_id}){/if} +
    + {#if canRemove} + + {/if} +
    +
    diff --git a/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte b/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte new file mode 100644 index 0000000..32018a6 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte @@ -0,0 +1,34 @@ + + +

    {$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/OauthRegistrationForm.svelte b/Foxnouns.Frontend/src/lib/components/settings/OauthRegistrationForm.svelte new file mode 100644 index 0000000..6199517 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/settings/OauthRegistrationForm.svelte @@ -0,0 +1,35 @@ + + +

    {title}

    + +{#if error} + +{/if} + +
    +
    + + +
    +
    + + +
    + + +
    diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 50662d1..47a39ac 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -1,157 +1,168 @@ { - "hello": "Hello, {{name}}!", - "nav": { - "log-in": "Log in or sign up", - "settings": "Settings" - }, - "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}}" - }, - "title": { - "log-in": "Log in", - "welcome": "Welcome", - "settings": "Settings" - }, - "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-mastodon": "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" - }, - "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" - }, - "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" - }, - "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" - } + "hello": "Hello, {{name}}!", + "nav": { + "log-in": "Log in or sign up", + "settings": "Settings" + }, + "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}}" + }, + "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-mastodon": "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" + }, + "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" + }, + "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" + }, + "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" + } } diff --git a/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.server.ts new file mode 100644 index 0000000..b9963d8 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.server.ts @@ -0,0 +1,64 @@ +import { apiRequest } from "$api"; +import ApiError, { ErrorCode } from "$api/error"; +import type { AddAccountResponse, CallbackResponse } from "$api/models/auth"; +import { setToken } from "$lib"; +import createRegisterAction from "$lib/actions/register.js"; +import log from "$lib/log.js"; +import { isRedirect, redirect } from "@sveltejs/kit"; + +export const load = async ({ url, parent, fetch, cookies }) => { + 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; + + const { meUser } = await parent(); + if (meUser) { + try { + const resp = await apiRequest( + "POST", + "/auth/discord/add-account/callback", + { + isInternal: true, + body: { code, state }, + 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 discord account to user %s:", meUser.id, e); + throw e; + } + } + + try { + const resp = await apiRequest("POST", "/auth/discord/callback", { + body: { code, state }, + isInternal: true, + fetch, + }); + + if (resp.has_account) { + setToken(cookies, resp.token!); + redirect(303, `/@${resp.user!.username}`); + } + + return { + hasAccount: false, + isLinkRequest: false, + ticket: resp.ticket!, + remoteUser: resp.remote_username!, + }; + } catch (e) { + if (isRedirect(e)) throw e; + if (e instanceof ApiError) return { isLinkRequest: false, error: e.obj }; + log.error("error while requesting discord callback:", e); + throw e; + } +}; + +export const actions = { + default: createRegisterAction("/auth/discord/register"), +}; diff --git a/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.svelte new file mode 100644 index 0000000..e82f0f1 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/discord/+page.svelte @@ -0,0 +1,31 @@ + + + + {$t("auth.register-with-discord")} • pronouns.cc + + +
    + {#if data.error} +

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

    + + {:else if data.isLinkRequest} + + {: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 index ce145d2..ce4d473 100644 --- a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts @@ -1,9 +1,9 @@ import { apiRequest } from "$api"; -import ApiError, { ErrorCode, type RawApiError } from "$api/error"; -import type { AuthResponse, CallbackResponse } from "$api/models/auth.js"; +import ApiError, { ErrorCode } from "$api/error"; +import type { CallbackResponse } from "$api/models/auth.js"; import { setToken } from "$lib"; -import log from "$lib/log.js"; -import { isRedirect, redirect } from "@sveltejs/kit"; +import createRegisterAction from "$lib/actions/register.js"; +import { redirect } from "@sveltejs/kit"; export const load = async ({ parent, params, url, fetch, cookies }) => { const { meUser } = await parent(); @@ -33,30 +33,5 @@ export const load = async ({ parent, params, url, fetch, cookies }) => { }; 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; - - if (!username || !ticket) - return { - error: { message: "Bad request", code: ErrorCode.BadRequest, status: 403 } as RawApiError, - }; - - try { - const resp = await apiRequest("POST", "/auth/fediverse/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; - } - }, + default: createRegisterAction("/auth/fediverse/register"), }; diff --git a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte index c68235f..5d02eeb 100644 --- a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.svelte @@ -1,9 +1,7 @@ + +{#if data.urls.email_enabled} +

    Email addresses

    + +{/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 new file mode 100644 index 0000000..543b51e --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/auth/add-discord/+page.server.ts @@ -0,0 +1,12 @@ +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); +}; From f3bb2d5d019199dfd2898add8b6935adc5339fa7 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 2 Dec 2024 15:06:17 +0100 Subject: [PATCH 137/261] fix(frontend): add autocomplete=off tags to most inputs --- .../src/lib/components/editor/FieldEditor.svelte | 3 ++- .../src/lib/components/editor/FieldEntryEditor.svelte | 2 +- .../src/lib/components/editor/FieldsEditor.svelte | 1 + .../src/lib/components/editor/PronounEntryEditor.svelte | 3 ++- .../src/lib/components/editor/PronounsEditor.svelte | 2 +- Foxnouns.Frontend/src/routes/settings/+page.svelte | 8 +++++++- .../src/routes/settings/members/[id]/+page.svelte | 9 ++++++++- .../src/routes/settings/members/new/+page.svelte | 2 +- .../src/routes/settings/profile/+page.svelte | 2 ++ 9 files changed, 25 insertions(+), 7 deletions(-) diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte index c2536dd..b409290 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldEditor.svelte @@ -65,7 +65,7 @@ onclick={() => move(index, false)} /> {$t("editor.field-name")} - + diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte index 65c2355..5e407ac 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldEntryEditor.svelte @@ -40,7 +40,7 @@ tooltip={$t("editor.move-entry-down")} onclick={() => moveValue(index, false)} /> - + diff --git a/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte index 6bbba9c..bcd3e78 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/FieldsEditor.svelte @@ -56,6 +56,7 @@ class="form-control" bind:value={newFieldName} placeholder={$t("editor.field-name")} + autocomplete="off" /> diff --git a/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte index aee6859..bcf5c15 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte @@ -50,7 +50,7 @@ tooltip={$t("editor.move-entry-down")} onclick={() => moveValue(index, true)} /> - + @@ -88,6 +88,7 @@ type="text" class="form-control" bind:value={value.display_text} + autocomplete="off" /> + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index ea28fc8..146591f 100644 --- a/README.md +++ b/README.md @@ -33,3 +33,8 @@ Code taken entirely or almost entirely from external sources: taken from [PluralKit](https://github.com/PluralKit/PluralKit/blob/32a6e97342acc3b35e6f9e7b4dd169e21d888770/PluralKit.Core/Database/Functions/functions.sql) - `Foxnouns.Backend/Database/prune-designer-cs-files.sh`, taken from [Iceshrimp.NET](https://iceshrimp.dev/iceshrimp/Iceshrimp.NET/src/commit/7c93dcf79dda54fc1a4ea9772e3f80874e6bcefb/Iceshrimp.Backend/Core/Database/prune-designer-cs-files.sh) + +Files under a different license: + +- `Foxnouns.Frontend/static/unknown_flag.svg` is https://commons.wikimedia.org/wiki/File:Unknown_flag.svg, + by 8938e on Wikimedia Commons, licensed as CC BY-SA 4.0. \ No newline at end of file From 2a0df335bc6b038c571ba8bec787420c79a6d853 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 16:33:06 +0100 Subject: [PATCH 155/261] feat(frontend): user profile flag editor --- Foxnouns.Frontend/package.json | 1 + Foxnouns.Frontend/pnpm-lock.yaml | 8 ++ .../src/lib/components/IconButton.svelte | 11 ++- .../lib/components/editor/FlagButton.svelte | 35 +++++++ .../lib/components/editor/FlagEditor.svelte | 9 +- .../lib/components/editor/FlagSearch.svelte | 48 ++++++++++ .../editor/ProfileFlagsEditor.svelte | 95 +++++++++++++++++++ .../src/lib/i18n/locales/en.json | 18 +++- .../src/routes/settings/flags/+page.svelte | 20 ++-- .../routes/settings/members/[id]/+page.svelte | 3 +- .../profile/flags-links/+page.server.ts | 7 ++ .../settings/profile/flags-links/+page.svelte | 28 ++++++ 12 files changed, 270 insertions(+), 13 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/editor/FlagButton.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/ProfileFlagsEditor.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index 0e74736..bd5ed64 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -29,6 +29,7 @@ "prettier-plugin-svelte": "^3.2.6", "sass": "^1.81.0", "svelte": "^5.0.0", + "svelte-bootstrap-icons": "^3.1.1", "svelte-check": "^4.0.0", "sveltekit-i18n": "^2.4.2", "typescript": "^5.0.0", diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index bb1a839..d2289ec 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -93,6 +93,9 @@ importers: svelte: specifier: ^5.0.0 version: 5.2.2 + svelte-bootstrap-icons: + specifier: ^3.1.1 + version: 3.1.1 svelte-check: specifier: ^4.0.0 version: 4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3) @@ -1321,6 +1324,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte-bootstrap-icons@3.1.1: + resolution: {integrity: sha512-ghJlt6TX3IX35M7wSvGyrmVgXeT5GMRF+7+q6L4OUT2RJWF09mQIvZTZ04Ii3FBfg10KdzFdvVuoB8M0cVHfzw==} + svelte-check@4.0.9: resolution: {integrity: sha512-SVNCz2L+9ZELGli7G0n3B3QE5kdf0u27RtKr2ZivWQhcWIXatZxwM4VrQ6AiA2k9zKp2mk5AxkEhdjbpjv7rEw==} engines: {node: '>= 18.0.0'} @@ -2564,6 +2570,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte-bootstrap-icons@3.1.1: {} + svelte-check@4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 diff --git a/Foxnouns.Frontend/src/lib/components/IconButton.svelte b/Foxnouns.Frontend/src/lib/components/IconButton.svelte index 4633dd1..1feedd9 100644 --- a/Foxnouns.Frontend/src/lib/components/IconButton.svelte +++ b/Foxnouns.Frontend/src/lib/components/IconButton.svelte @@ -10,11 +10,18 @@ type?: "submit" | "reset" | "button"; id?: string; onclick?: MouseEventHandler; + outline?: boolean; }; - let { icon, tooltip, color = "primary", type, id, onclick }: Props = $props(); + let { icon, tooltip, color = "primary", type, id, onclick, outline }: Props = $props(); - diff --git a/Foxnouns.Frontend/src/lib/components/editor/FlagButton.svelte b/Foxnouns.Frontend/src/lib/components/editor/FlagButton.svelte new file mode 100644 index 0000000..51b87c4 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/FlagButton.svelte @@ -0,0 +1,35 @@ + + + + + diff --git a/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte index 8b542da..8cb994c 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/FlagEditor.svelte @@ -24,11 +24,16 @@ {flag.description
    - + diff --git a/Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte b/Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte new file mode 100644 index 0000000..78a6c0f --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/FlagSearch.svelte @@ -0,0 +1,48 @@ + + + + +
    + {#each filteredFlags as flag (flag.id)} + select(flag)} padding /> + {:else} +
    +

    + +

    +

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

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

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

    + {:else} +

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

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

    + {$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/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 20e8404..eb80a83 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -128,7 +128,10 @@ "flag-current-flags-title": "Current flags ({{count}}/{{max}})", "flag-title": "Flags", "flag-upload-title": "Upload a new flag", - "flag-upload-button": "Upload" + "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." }, "yes": "Yes", "no": "No", @@ -188,7 +191,18 @@ "remove-field": "Remove field", "field-name": "Field name", "add-field": "Add field", - "new-entry": "New entry" + "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" }, "cancel": "Cancel" } diff --git a/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte b/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte index 3e806f9..942e90f 100644 --- a/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/flags/+page.svelte @@ -78,10 +78,7 @@
    - +

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

    - - + + diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte index b4b38e2..c286c38 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/+page.svelte @@ -6,6 +6,7 @@ import ApiError from "$api/error"; import log from "$lib/log"; import { Icon, InputGroup } from "@sveltestrap/sveltestrap"; + import InfoCircleFill from "svelte-bootstrap-icons/lib/InfoCircleFill.svelte"; import { t } from "$lib/i18n"; import AvatarEditor from "$components/editor/AvatarEditor.svelte"; import ErrorAlert from "$components/ErrorAlert.svelte"; @@ -133,7 +134,7 @@

    - + {$t("edit-profile.unlisted-note")} {PUBLIC_BASE_URL.substring("https://".length)}/@{data.member.user.username}/{data.member diff --git a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.server.ts new file mode 100644 index 0000000..0b3a452 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.server.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..0273e6b --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte @@ -0,0 +1,28 @@ + + + From b0a286dd9f4bf6f0eb46df1e455135bcb3b5d3bb Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 16:41:54 +0100 Subject: [PATCH 156/261] feat(frontend): member fields and flags editors, fix user fields editor --- .../settings/members/[id]/fields/+page.svelte | 32 +++++++++++++++++++ .../members/[id]/flags-links/+page.server.ts | 7 ++++ .../members/[id]/flags-links/+page.svelte | 28 ++++++++++++++++ .../settings/profile/fields/+page.svelte | 2 +- 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte diff --git a/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte new file mode 100644 index 0000000..491a45f --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/fields/+page.svelte @@ -0,0 +1,32 @@ + + + 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 new file mode 100644 index 0000000..0b3a452 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.server.ts @@ -0,0 +1,7 @@ +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 new file mode 100644 index 0000000..db35e18 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte @@ -0,0 +1,28 @@ + + + diff --git a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte index 8bab783..4c61a58 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/fields/+page.svelte @@ -29,4 +29,4 @@ }; - + From c6eba5b51a72a448efce78f9bdeefd9417687500 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 17:05:43 +0100 Subject: [PATCH 157/261] feat(frontend): links editor --- .../lib/components/editor/LinksEditor.svelte | 84 +++++++++++++++++++ .../lib/components/profile/ProfileLink.svelte | 2 +- .../src/lib/i18n/locales/en.json | 3 +- .../members/[id]/flags-links/+page.svelte | 32 +++++-- .../settings/profile/flags-links/+page.svelte | 32 +++++-- 5 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte diff --git a/Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte new file mode 100644 index 0000000..f908e0e --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/editor/LinksEditor.svelte @@ -0,0 +1,84 @@ + + +

    + {$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/profile/ProfileLink.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte index d4672a8..0a42c58 100644 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileLink.svelte @@ -9,7 +9,7 @@ if (raw.startsWith("https://")) out = raw.substring("https://".length); else if (raw.startsWith("http://")) out = raw.substring("http://".length); - if (raw.endsWith("/")) out = raw.substring(0, raw.length - 1); + if (out.endsWith("/")) out = out.substring(0, out.length - 1); return out; }; diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index eb80a83..f3a7d87 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -202,7 +202,8 @@ "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" + "flag-manage-your-flags": "Manage your flags", + "links-header": "Links" }, "cancel": "Cancel" } 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 index db35e18..b6aaadb 100644 --- a/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/members/[id]/flags-links/+page.svelte @@ -2,6 +2,7 @@ import { fastRequest } from "$api"; import type { RawApiError } from "$api/error"; import ApiError from "$api/error"; + import LinksEditor from "$components/editor/LinksEditor.svelte"; import ProfileFlagsEditor from "$components/editor/ProfileFlagsEditor.svelte"; import log from "$lib/log"; import type { PageData } from "./$types"; @@ -9,20 +10,41 @@ type Props = { data: PageData }; let { data }: Props = $props(); - let form: { ok: boolean; error: RawApiError | null } | null = $state(null); + let flagForm: { ok: boolean; error: RawApiError | null } | null = $state(null); + let linksForm: { ok: boolean; error: RawApiError | null } | null = $state(null); - const save = async (flags: string[]) => { + const flagSave = async (flags: string[]) => { try { await fastRequest("PATCH", `/users/@me/members/${data.member.id}`, { body: { flags }, token: data.token, }); - form = { ok: true, error: null }; + flagForm = { ok: true, error: null }; } catch (e) { log.error("Could not update profile flags for member %s:", data.member.id, e); - if (e instanceof ApiError) form = { ok: false, error: e.obj }; + if (e instanceof ApiError) flagForm = { ok: false, error: e.obj }; + } + }; + + const linksSave = async (links: string[]) => { + try { + await fastRequest("PATCH", "/users/@me", { + body: { links }, + token: data.token, + }); + linksForm = { ok: true, error: null }; + } catch (e) { + log.error("Could not update profile links:", e); + if (e instanceof ApiError) linksForm = { ok: false, error: e.obj }; } }; - + + + diff --git a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte index 0273e6b..4b2b165 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/flags-links/+page.svelte @@ -2,6 +2,7 @@ import { fastRequest } from "$api"; import type { RawApiError } from "$api/error"; import ApiError from "$api/error"; + import LinksEditor from "$components/editor/LinksEditor.svelte"; import ProfileFlagsEditor from "$components/editor/ProfileFlagsEditor.svelte"; import log from "$lib/log"; import type { PageData } from "./$types"; @@ -9,20 +10,41 @@ type Props = { data: PageData }; let { data }: Props = $props(); - let form: { ok: boolean; error: RawApiError | null } | null = $state(null); + let flagForm: { ok: boolean; error: RawApiError | null } | null = $state(null); + let linksForm: { ok: boolean; error: RawApiError | null } | null = $state(null); - const save = async (flags: string[]) => { + const flagSave = async (flags: string[]) => { try { await fastRequest("PATCH", "/users/@me", { body: { flags }, token: data.token, }); - form = { ok: true, error: null }; + flagForm = { ok: true, error: null }; } catch (e) { log.error("Could not update profile flags:", e); - if (e instanceof ApiError) form = { ok: false, error: e.obj }; + if (e instanceof ApiError) flagForm = { ok: false, error: e.obj }; + } + }; + + const linksSave = async (links: string[]) => { + try { + await fastRequest("PATCH", "/users/@me", { + body: { links }, + token: data.token, + }); + linksForm = { ok: true, error: null }; + } catch (e) { + log.error("Could not update profile links:", e); + if (e instanceof ApiError) linksForm = { ok: false, error: e.obj }; } }; - + + + From bb2fa55cd5999a3236d87e3594b1843c70efaa31 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 18:04:56 +0100 Subject: [PATCH 158/261] feat: docker config for new frontend --- .gitignore | 4 ++++ DOCKER.md | 8 +++++--- Foxnouns.Frontend/.dockerignore | 4 ++++ Foxnouns.Frontend/Dockerfile | 17 ++++++++++++++++ Foxnouns.Frontend/src/hooks.server.ts | 1 + docker-compose.yml | 28 ++++++++++----------------- docker/Caddyfile | 11 +++++++++-- docker/frontend.env | 0 docker/frontend.example.env | 4 ++++ 9 files changed, 54 insertions(+), 23 deletions(-) create mode 100644 Foxnouns.Frontend/.dockerignore create mode 100644 Foxnouns.Frontend/Dockerfile delete mode 100644 docker/frontend.env create mode 100644 docker/frontend.example.env diff --git a/.gitignore b/.gitignore index d9b0dfd..c61154d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ config.ini *.DotSettings.user proxy-config.json .DS_Store + +docker/config.ini +docker/proxy-config.json +docker/frontend.env diff --git a/DOCKER.md b/DOCKER.md index a4f8c3a..5cbb6e6 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -2,7 +2,9 @@ 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. Build with `docker compose build` -4. Run with `docker compose up` +3. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do th esame. +4. Build with `docker compose build` +5. Run with `docker compose up` -The Caddy server will listen on `localhost:5004`. +The Caddy server will listen on `localhost:5004` for the frontend and API, +and on `localhost:5005` for the profile URL shortener. diff --git a/Foxnouns.Frontend/.dockerignore b/Foxnouns.Frontend/.dockerignore new file mode 100644 index 0000000..14ad623 --- /dev/null +++ b/Foxnouns.Frontend/.dockerignore @@ -0,0 +1,4 @@ +node_modules/ +build/ +.env +.env* diff --git a/Foxnouns.Frontend/Dockerfile b/Foxnouns.Frontend/Dockerfile new file mode 100644 index 0000000..166be23 --- /dev/null +++ b/Foxnouns.Frontend/Dockerfile @@ -0,0 +1,17 @@ +FROM docker.io/node:22-slim + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +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 --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm run build + +CMD ["pnpm", "node", "build/index.js"] diff --git a/Foxnouns.Frontend/src/hooks.server.ts b/Foxnouns.Frontend/src/hooks.server.ts index e8ec723..eb1f93d 100644 --- a/Foxnouns.Frontend/src/hooks.server.ts +++ b/Foxnouns.Frontend/src/hooks.server.ts @@ -3,6 +3,7 @@ import { PUBLIC_API_BASE } from "$env/static/public"; import type { HandleFetch } from "@sveltejs/kit"; export const handleFetch: HandleFetch = async ({ request, fetch }) => { + console.log(PUBLIC_API_BASE, PRIVATE_INTERNAL_API_HOST, PRIVATE_API_HOST); 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)) { diff --git a/docker-compose.yml b/docker-compose.yml index 6fafd18..5606760 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,8 +5,8 @@ services: context: . dockerfile: ./Dockerfile.backend environment: - - "Database:Url=Host=pgbouncer;Database=postgres;Username=postgres;Password=postgres" - - "Database:EnablePooling=false" + - "Database:Url=Host=postgres;Database=postgres;Username=postgres;Password=postgres" + - "Database:EnablePooling=true" - "Host=0.0.0.0" - "Port=5000" restart: unless-stopped @@ -15,13 +15,14 @@ services: frontend: image: frontend - build: ./Foxnouns.Frontend - environment: - - "API_BASE=http://rate:5003/api" - - "INTERNAL_API_BASE=http://backend:5000/api" + build: + context: ./ + dockerfile: ./Foxnouns.Frontend/Dockerfile restart: unless-stopped - volumes: - - ./docker/frontend.env:/app/.env + env_file: ./docker/frontend.env + environment: + - "PRIVATE_API_HOST=http://rate:5003/api" + - "PRIVATE_INTERNAL_API_HOST=http://backend:5000/api" rate: image: rate @@ -32,16 +33,6 @@ services: volumes: - ./docker/proxy-config.json:/app/proxy-config.json - pgbouncer: - image: docker.io/edoburu/pgbouncer:latest - environment: - - "DATABASE_URL=postgres://postgres:postgres@postgres/postgres" - - "AUTH_TYPE=scram-sha-256" - - "MAX_CLIENT_CONN=100" - - "DEFAULT_POOL_SIZE=100" - - "MIN_POOL_SIZE=10" - restart: unless-stopped - postgres: image: docker.io/postgres:16 command: [ "postgres", @@ -61,6 +52,7 @@ services: restart: unless-stopped ports: - "5004:80" + - "5005:81" volumes: - ./docker/Caddyfile:/etc/caddy/Caddyfile - caddy_data:/data diff --git a/docker/Caddyfile b/docker/Caddyfile index 6e12647..9132068 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -1,4 +1,11 @@ -http:// { +# Frontend and API +http://localhost:80 { reverse_proxy /api/* http://rate:5003 reverse_proxy http://frontend:3000 -} \ No newline at end of file +} + +# prns.cc (profile URL shortener) +http://localhost:81 { + rewrite * /sid{uri} + reverse_proxy http://backend:5000 +} diff --git a/docker/frontend.env b/docker/frontend.env deleted file mode 100644 index e69de29..0000000 diff --git a/docker/frontend.example.env b/docker/frontend.example.env new file mode 100644 index 0000000..b68a330 --- /dev/null +++ b/docker/frontend.example.env @@ -0,0 +1,4 @@ +PUBLIC_LANGUAGE=en +PUBLIC_BASE_URL=https://pronouns.cc +PUBLIC_SHORT_URL=https://prns.cc +PUBLIC_API_BASE=https://pronouns.cc/api From 8a8b4caa183280c0b3af34d6e17ef1c2f3de5161 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 21:07:53 +0100 Subject: [PATCH 159/261] feat: log in with google --- .../Authentication/AuthController.cs | 13 +- .../Authentication/GoogleAuthController.cs | 164 ++++++++++++++++++ .../Auth/RemoteAuthService.Discord.cs | 90 ++++++++++ .../Services/Auth/RemoteAuthService.Google.cs | 80 +++++++++ .../Services/Auth/RemoteAuthService.cs | 71 +------- Foxnouns.Frontend/src/hooks.server.ts | 1 - .../src/lib/i18n/locales/en.json | 4 +- Foxnouns.Frontend/src/lib/index.ts | 3 +- .../auth/callback/google/+page.server.ts | 8 + .../routes/auth/callback/google/+page.svelte | 31 ++++ .../settings/auth/add-google/+page.server.ts | 12 ++ 11 files changed, 403 insertions(+), 74 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs create mode 100644 Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs create mode 100644 Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/google/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/google/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/auth/add-google/+page.server.ts diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 8016a1f..172ef9a 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -33,6 +33,7 @@ public class AuthController( ); string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); string? discord = null; + string? google = null; if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) { discord = @@ -42,7 +43,17 @@ public class AuthController( + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/discord")}"; } - return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, null, null)); + 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")}"; + } + + return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, google, null)); } [HttpPost("force-log-out")] diff --git a/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs new file mode 100644 index 0000000..04c0b7d --- /dev/null +++ b/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs @@ -0,0 +1,164 @@ +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")] +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/Services/Auth/RemoteAuthService.Discord.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs new file mode 100644 index 0000000..d4d6f6a --- /dev/null +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs @@ -0,0 +1,90 @@ +// 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 JetBrains.Annotations; + +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 _httpClient.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"); + } + + DiscordTokenResponse? 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.token_type} {token.access_token}"); + + HttpResponseMessage resp2 = await _httpClient.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); + } + + [SuppressMessage( + "ReSharper", + "InconsistentNaming", + Justification = "Easier to use snake_case here, rather than passing in JSON converter options" + )] + [UsedImplicitly] + private record DiscordTokenResponse(string access_token, string token_type); + + [SuppressMessage( + "ReSharper", + "InconsistentNaming", + Justification = "Easier to use snake_case here, rather than passing in JSON converter options" + )] + [UsedImplicitly] + private record DiscordUserResponse(string id, string username); +} diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs new file mode 100644 index 0000000..bcd881d --- /dev/null +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs @@ -0,0 +1,80 @@ +// 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; +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 _httpClient.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); + } + + [SuppressMessage("ReSharper", "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.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs index c3ca685..98fb61a 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs @@ -10,7 +10,7 @@ using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Services.Auth; -public class RemoteAuthService( +public partial class RemoteAuthService( Config config, ILogger logger, DatabaseContext db, @@ -20,75 +20,6 @@ public class RemoteAuthService( private readonly ILogger _logger = logger.ForContext(); private readonly HttpClient _httpClient = new(); - 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 _httpClient.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"); - } - - resp.EnsureSuccessStatusCode(); - DiscordTokenResponse? 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.token_type} {token.access_token}"); - - HttpResponseMessage resp2 = await _httpClient.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); - } - - [SuppressMessage( - "ReSharper", - "InconsistentNaming", - Justification = "Easier to use snake_case here, rather than passing in JSON converter options" - )] - [UsedImplicitly] - private record DiscordTokenResponse(string access_token, string token_type); - - [SuppressMessage( - "ReSharper", - "InconsistentNaming", - Justification = "Easier to use snake_case here, rather than passing in JSON converter options" - )] - [UsedImplicitly] - private record DiscordUserResponse(string id, string username); - public record RemoteUser(string Id, string Username); /// diff --git a/Foxnouns.Frontend/src/hooks.server.ts b/Foxnouns.Frontend/src/hooks.server.ts index eb1f93d..e8ec723 100644 --- a/Foxnouns.Frontend/src/hooks.server.ts +++ b/Foxnouns.Frontend/src/hooks.server.ts @@ -3,7 +3,6 @@ import { PUBLIC_API_BASE } from "$env/static/public"; import type { HandleFetch } from "@sveltejs/kit"; export const handleFetch: HandleFetch = async ({ request, fetch }) => { - console.log(PUBLIC_API_BASE, PRIVATE_INTERNAL_API_HOST, PRIVATE_API_HOST); 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)) { diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index f3a7d87..df0fd1b 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -52,7 +52,9 @@ "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-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" }, "error": { "bad-request-header": "Something was wrong with your input", diff --git a/Foxnouns.Frontend/src/lib/index.ts b/Foxnouns.Frontend/src/lib/index.ts index fdf8885..3e7b36e 100644 --- a/Foxnouns.Frontend/src/lib/index.ts +++ b/Foxnouns.Frontend/src/lib/index.ts @@ -3,7 +3,8 @@ import type { Cookies } from "@sveltejs/kit"; import { DateTime } from "luxon"; -export const TOKEN_COOKIE_NAME = "__Host-pronounscc-token"; +// export const TOKEN_COOKIE_NAME = "__Host-pronounscc-token"; +export const TOKEN_COOKIE_NAME = "pronounscc-token"; export const setToken = (cookies: Cookies, token: string) => cookies.set(TOKEN_COOKIE_NAME, token, { path: "/" }); diff --git a/Foxnouns.Frontend/src/routes/auth/callback/google/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/google/+page.server.ts new file mode 100644 index 0000000..49f963c --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/google/+page.server.ts @@ -0,0 +1,8 @@ +import createCallbackLoader from "$lib/actions/callback"; +import createRegisterAction from "$lib/actions/register"; + +export const load = createCallbackLoader("google"); + +export const actions = { + default: createRegisterAction("/auth/google/register"), +}; diff --git a/Foxnouns.Frontend/src/routes/auth/callback/google/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/google/+page.svelte new file mode 100644 index 0000000..284806a --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/google/+page.svelte @@ -0,0 +1,31 @@ + + + + {$t("auth.register-with-google")} • pronouns.cc + + +
    + {#if data.error} +

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

    + + {:else if data.isLinkRequest} + + {:else} + + {/if} +
    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 new file mode 100644 index 0000000..ca07805 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/auth/add-google/+page.server.ts @@ -0,0 +1,12 @@ +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); +}; From d30ebacc72b564428d3bcf2e2c05355498999a52 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 21:11:46 +0100 Subject: [PATCH 160/261] chore: add license headers to all c# files --- Foxnouns.Backend/BuildInfo.cs | 14 ++++++++++++++ Foxnouns.Backend/Config.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/ApiControllerBase.cs | 14 ++++++++++++++ .../Controllers/Authentication/AuthController.cs | 14 ++++++++++++++ .../Authentication/DiscordAuthController.cs | 14 ++++++++++++++ .../Authentication/EmailAuthController.cs | 14 ++++++++++++++ .../Authentication/FediverseAuthController.cs | 14 ++++++++++++++ .../Authentication/GoogleAuthController.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/ExportsController.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/FlagsController.cs | 14 ++++++++++++++ .../Controllers/InternalController.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/MembersController.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/MetaController.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/SidController.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/UsersController.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/BaseModel.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/DatabaseContext.cs | 14 ++++++++++++++ .../Database/DatabaseQueryExtensions.cs | 14 ++++++++++++++ .../Database/DatabaseServiceExtensions.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/FlagQueryExtensions.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/ISnowflakeGenerator.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/Application.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/AuthMethod.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/DataExport.cs | 14 ++++++++++++++ .../Database/Models/FediverseApplication.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/Field.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/Member.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/PrideFlag.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/TemporaryKey.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/Token.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Models/User.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/Snowflake.cs | 14 ++++++++++++++ Foxnouns.Backend/Database/SnowflakeGenerator.cs | 14 ++++++++++++++ Foxnouns.Backend/Dto/Auth.cs | 15 +++++++++++++++ Foxnouns.Backend/Dto/DataExport.cs | 15 +++++++++++++++ Foxnouns.Backend/Dto/Flag.cs | 15 +++++++++++++++ Foxnouns.Backend/Dto/Internal.cs | 15 +++++++++++++++ Foxnouns.Backend/Dto/Member.cs | 15 +++++++++++++++ Foxnouns.Backend/Dto/Meta.cs | 15 +++++++++++++++ Foxnouns.Backend/Dto/User.cs | 15 +++++++++++++++ Foxnouns.Backend/ExpectedError.cs | 14 ++++++++++++++ .../Extensions/ImageObjectExtensions.cs | 14 ++++++++++++++ Foxnouns.Backend/Extensions/KeyCacheExtensions.cs | 14 ++++++++++++++ .../Extensions/WebApplicationExtensions.cs | 14 ++++++++++++++ Foxnouns.Backend/FoxnounsMetrics.cs | 14 ++++++++++++++ Foxnouns.Backend/GlobalUsing.cs | 14 ++++++++++++++ .../Jobs/CreateDataExportInvocable.cs | 14 ++++++++++++++ Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 14 ++++++++++++++ .../Jobs/MemberAvatarUpdateInvocable.cs | 14 ++++++++++++++ Foxnouns.Backend/Jobs/Payloads.cs | 14 ++++++++++++++ .../Jobs/UserAvatarUpdateInvocable.cs | 14 ++++++++++++++ .../Mailables/AccountCreationMailable.cs | 14 ++++++++++++++ Foxnouns.Backend/Mailables/AddEmailMailable.cs | 14 ++++++++++++++ Foxnouns.Backend/Mailables/BaseView.cs | 14 ++++++++++++++ .../Middleware/AuthenticationMiddleware.cs | 14 ++++++++++++++ .../Middleware/AuthorizationMiddleware.cs | 14 ++++++++++++++ .../Middleware/ErrorHandlerMiddleware.cs | 14 ++++++++++++++ Foxnouns.Backend/Program.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/Auth/AuthService.cs | 14 ++++++++++++++ .../Auth/FediverseAuthService.Mastodon.cs | 14 ++++++++++++++ .../Services/Auth/FediverseAuthService.cs | 14 ++++++++++++++ .../Services/Auth/RemoteAuthService.Discord.cs | 1 - .../Services/Auth/RemoteAuthService.Google.cs | 1 - .../Services/Auth/RemoteAuthService.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/DataCleanupService.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/KeyCacheService.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/MailService.cs | 14 ++++++++++++++ .../Services/MemberRendererService.cs | 14 ++++++++++++++ .../Services/MetricsCollectionService.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/ObjectStorageService.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/PeriodicTasksService.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/UserRendererService.cs | 14 ++++++++++++++ Foxnouns.Backend/Utils/AuthUtils.cs | 14 ++++++++++++++ Foxnouns.Backend/Utils/BootstrapIcons.cs | 14 ++++++++++++++ Foxnouns.Backend/Utils/Limits.cs | 14 ++++++++++++++ Foxnouns.Backend/Utils/PatchRequest.cs | 14 ++++++++++++++ .../Utils/ScreamingSnakeCaseEnumConverter.cs | 14 ++++++++++++++ Foxnouns.Backend/Utils/ValidationUtils.Fields.cs | 1 - .../Utils/ValidationUtils.Preferences.cs | 1 - Foxnouns.Backend/Utils/ValidationUtils.Strings.cs | 1 - Foxnouns.Backend/Utils/ValidationUtils.cs | 14 ++++++++++++++ 81 files changed, 1071 insertions(+), 5 deletions(-) diff --git a/Foxnouns.Backend/BuildInfo.cs b/Foxnouns.Backend/BuildInfo.cs index 2d58277..1e27874 100644 --- a/Foxnouns.Backend/BuildInfo.cs +++ b/Foxnouns.Backend/BuildInfo.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 2f73d4e..3874204 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -1,3 +1,17 @@ +// 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 Serilog.Events; namespace Foxnouns.Backend; diff --git a/Foxnouns.Backend/Controllers/ApiControllerBase.cs b/Foxnouns.Backend/Controllers/ApiControllerBase.cs index 9fffa67..75a256e 100644 --- a/Foxnouns.Backend/Controllers/ApiControllerBase.cs +++ b/Foxnouns.Backend/Controllers/ApiControllerBase.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 172ef9a..09bd152 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index 2d66ede..a3894e6 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index f8d78b0..006a8ed 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 2587621..5496094 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs index 04c0b7d..ba9f6cc 100644 --- a/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs index 06844a1..cd70661 100644 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ b/Foxnouns.Backend/Controllers/ExportsController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 4bc947b..df0214a 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 2f4baf7..1180a34 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 269817e..e42fffa 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -1,3 +1,17 @@ +// 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 EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; diff --git a/Foxnouns.Backend/Controllers/MetaController.cs b/Foxnouns.Backend/Controllers/MetaController.cs index 2c5bc45..8552164 100644 --- a/Foxnouns.Backend/Controllers/MetaController.cs +++ b/Foxnouns.Backend/Controllers/MetaController.cs @@ -1,3 +1,17 @@ +// 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; using Foxnouns.Backend.Utils; using Microsoft.AspNetCore.Mvc; diff --git a/Foxnouns.Backend/Controllers/SidController.cs b/Foxnouns.Backend/Controllers/SidController.cs index c89b120..a0067bf 100644 --- a/Foxnouns.Backend/Controllers/SidController.cs +++ b/Foxnouns.Backend/Controllers/SidController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 74149b7..4a3be72 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -1,3 +1,17 @@ +// 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 EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; diff --git a/Foxnouns.Backend/Database/BaseModel.cs b/Foxnouns.Backend/Database/BaseModel.cs index d87cf22..a6ea612 100644 --- a/Foxnouns.Backend/Database/BaseModel.cs +++ b/Foxnouns.Backend/Database/BaseModel.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index f79f4fe..a5dede8 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index dc25b55..790b9df 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs b/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs index 878927b..282d454 100644 --- a/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseServiceExtensions.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/FlagQueryExtensions.cs b/Foxnouns.Backend/Database/FlagQueryExtensions.cs index a6354d8..953c94a 100644 --- a/Foxnouns.Backend/Database/FlagQueryExtensions.cs +++ b/Foxnouns.Backend/Database/FlagQueryExtensions.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/ISnowflakeGenerator.cs b/Foxnouns.Backend/Database/ISnowflakeGenerator.cs index cfe6085..62b0dda 100644 --- a/Foxnouns.Backend/Database/ISnowflakeGenerator.cs +++ b/Foxnouns.Backend/Database/ISnowflakeGenerator.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/Models/Application.cs b/Foxnouns.Backend/Database/Models/Application.cs index ed8929f..e10d1c8 100644 --- a/Foxnouns.Backend/Database/Models/Application.cs +++ b/Foxnouns.Backend/Database/Models/Application.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/Models/AuthMethod.cs b/Foxnouns.Backend/Database/Models/AuthMethod.cs index b5602ec..07cfb79 100644 --- a/Foxnouns.Backend/Database/Models/AuthMethod.cs +++ b/Foxnouns.Backend/Database/Models/AuthMethod.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Database/Models/DataExport.cs b/Foxnouns.Backend/Database/Models/DataExport.cs index 582ffd8..464a54a 100644 --- a/Foxnouns.Backend/Database/Models/DataExport.cs +++ b/Foxnouns.Backend/Database/Models/DataExport.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/Models/FediverseApplication.cs b/Foxnouns.Backend/Database/Models/FediverseApplication.cs index 6dc813d..13e1318 100644 --- a/Foxnouns.Backend/Database/Models/FediverseApplication.cs +++ b/Foxnouns.Backend/Database/Models/FediverseApplication.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Database/Models/Field.cs b/Foxnouns.Backend/Database/Models/Field.cs index 9b5dd31..7c44b2f 100644 --- a/Foxnouns.Backend/Database/Models/Field.cs +++ b/Foxnouns.Backend/Database/Models/Field.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Database/Models/Member.cs b/Foxnouns.Backend/Database/Models/Member.cs index c1e5bde..b9793e0 100644 --- a/Foxnouns.Backend/Database/Models/Member.cs +++ b/Foxnouns.Backend/Database/Models/Member.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Database/Models/PrideFlag.cs b/Foxnouns.Backend/Database/Models/PrideFlag.cs index d5437f8..f103610 100644 --- a/Foxnouns.Backend/Database/Models/PrideFlag.cs +++ b/Foxnouns.Backend/Database/Models/PrideFlag.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Database/Models/TemporaryKey.cs b/Foxnouns.Backend/Database/Models/TemporaryKey.cs index 607eb4a..f83e515 100644 --- a/Foxnouns.Backend/Database/Models/TemporaryKey.cs +++ b/Foxnouns.Backend/Database/Models/TemporaryKey.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/Models/Token.cs b/Foxnouns.Backend/Database/Models/Token.cs index 3ecd39d..ba9b016 100644 --- a/Foxnouns.Backend/Database/Models/Token.cs +++ b/Foxnouns.Backend/Database/Models/Token.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index f75acde..c60430a 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index 07a37a4..8cf9141 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Database/SnowflakeGenerator.cs b/Foxnouns.Backend/Database/SnowflakeGenerator.cs index e532e42..fd188f5 100644 --- a/Foxnouns.Backend/Database/SnowflakeGenerator.cs +++ b/Foxnouns.Backend/Database/SnowflakeGenerator.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Dto/Auth.cs b/Foxnouns.Backend/Dto/Auth.cs index 71ebd91..05308a5 100644 --- a/Foxnouns.Backend/Dto/Auth.cs +++ b/Foxnouns.Backend/Dto/Auth.cs @@ -1,3 +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 . + // ReSharper disable NotAccessedPositionalProperty.Global // ReSharper disable ClassNeverInstantiated.Global using Foxnouns.Backend.Database; diff --git a/Foxnouns.Backend/Dto/DataExport.cs b/Foxnouns.Backend/Dto/DataExport.cs index 91413ab..9fc0a7d 100644 --- a/Foxnouns.Backend/Dto/DataExport.cs +++ b/Foxnouns.Backend/Dto/DataExport.cs @@ -1,3 +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 . + // ReSharper disable NotAccessedPositionalProperty.Global using NodaTime; diff --git a/Foxnouns.Backend/Dto/Flag.cs b/Foxnouns.Backend/Dto/Flag.cs index 882613a..955d8bd 100644 --- a/Foxnouns.Backend/Dto/Flag.cs +++ b/Foxnouns.Backend/Dto/Flag.cs @@ -1,3 +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 . + // ReSharper disable NotAccessedPositionalProperty.Global using Foxnouns.Backend.Database; using Foxnouns.Backend.Utils; diff --git a/Foxnouns.Backend/Dto/Internal.cs b/Foxnouns.Backend/Dto/Internal.cs index aed0411..eecfb3b 100644 --- a/Foxnouns.Backend/Dto/Internal.cs +++ b/Foxnouns.Backend/Dto/Internal.cs @@ -1,3 +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 . + // ReSharper disable NotAccessedPositionalProperty.Global using Foxnouns.Backend.Database; diff --git a/Foxnouns.Backend/Dto/Member.cs b/Foxnouns.Backend/Dto/Member.cs index 2925efe..b0c4bfa 100644 --- a/Foxnouns.Backend/Dto/Member.cs +++ b/Foxnouns.Backend/Dto/Member.cs @@ -1,3 +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 . + // ReSharper disable NotAccessedPositionalProperty.Global using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; diff --git a/Foxnouns.Backend/Dto/Meta.cs b/Foxnouns.Backend/Dto/Meta.cs index 80826ca..0ff6e80 100644 --- a/Foxnouns.Backend/Dto/Meta.cs +++ b/Foxnouns.Backend/Dto/Meta.cs @@ -1,3 +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 . + // ReSharper disable NotAccessedPositionalProperty.Global namespace Foxnouns.Backend.Dto; diff --git a/Foxnouns.Backend/Dto/User.cs b/Foxnouns.Backend/Dto/User.cs index c0604fb..0ab9511 100644 --- a/Foxnouns.Backend/Dto/User.cs +++ b/Foxnouns.Backend/Dto/User.cs @@ -1,3 +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 . + // ReSharper disable NotAccessedPositionalProperty.Global // ReSharper disable ClassNeverInstantiated.Global using Foxnouns.Backend.Database; diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index c8d4d44..6c5c7e9 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -1,3 +1,17 @@ +// 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 Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json; diff --git a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs index f87aa0e..db0797c 100644 --- a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index fa3a3d2..1d99830 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 892eef6..30d97d8 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/FoxnounsMetrics.cs b/Foxnouns.Backend/FoxnounsMetrics.cs index 71cfd39..c2fe8da 100644 --- a/Foxnouns.Backend/FoxnounsMetrics.cs +++ b/Foxnouns.Backend/FoxnounsMetrics.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/GlobalUsing.cs b/Foxnouns.Backend/GlobalUsing.cs index 5275c8d..e9f359b 100644 --- a/Foxnouns.Backend/GlobalUsing.cs +++ b/Foxnouns.Backend/GlobalUsing.cs @@ -1,2 +1,16 @@ +// 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/CreateDataExportInvocable.cs b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs index c07ea7f..cd5c97f 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs @@ -1,3 +1,17 @@ +// 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 Coravel.Invocable; diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 93a4e0c..1b8905b 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -1,3 +1,17 @@ +// 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.Invocable; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs index 0a9792e..01ec9e3 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs @@ -1,3 +1,17 @@ +// 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.Invocable; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; diff --git a/Foxnouns.Backend/Jobs/Payloads.cs b/Foxnouns.Backend/Jobs/Payloads.cs index d67f1b2..192a6fa 100644 --- a/Foxnouns.Backend/Jobs/Payloads.cs +++ b/Foxnouns.Backend/Jobs/Payloads.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs index 014fbed..862d0da 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs @@ -1,3 +1,17 @@ +// 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.Invocable; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; diff --git a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs index d1f1f3c..ee86b48 100644 --- a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs +++ b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Mailables/AddEmailMailable.cs b/Foxnouns.Backend/Mailables/AddEmailMailable.cs index ee5792d..f40ffc0 100644 --- a/Foxnouns.Backend/Mailables/AddEmailMailable.cs +++ b/Foxnouns.Backend/Mailables/AddEmailMailable.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Mailables/BaseView.cs b/Foxnouns.Backend/Mailables/BaseView.cs index c934fd3..bc3d2bf 100644 --- a/Foxnouns.Backend/Mailables/BaseView.cs +++ b/Foxnouns.Backend/Mailables/BaseView.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs index f63d711..6a1b631 100644 --- a/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthenticationMiddleware.cs @@ -1,3 +1,17 @@ +// 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.Utils; diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 114e870..1132dc1 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs index f033ede..db08916 100644 --- a/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs +++ b/Foxnouns.Backend/Middleware/ErrorHandlerMiddleware.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index bc56ef9..02a289a 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -1,3 +1,17 @@ +// 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; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Services; diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs index 0317c4a..89248cd 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs index 29c27b6..25d5327 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Mastodon.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index 43dab01..7e67fa7 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs index d4d6f6a..c06a6f6 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs @@ -12,7 +12,6 @@ // // 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 JetBrains.Annotations; diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs index bcd881d..a6800cc 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs @@ -12,7 +12,6 @@ // // 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; using System.Text.Json; diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs index 98fb61a..f50d15c 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs @@ -1,3 +1,17 @@ +// 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.Web; using Foxnouns.Backend.Database; diff --git a/Foxnouns.Backend/Services/DataCleanupService.cs b/Foxnouns.Backend/Services/DataCleanupService.cs index b9d3afe..2b42f86 100644 --- a/Foxnouns.Backend/Services/DataCleanupService.cs +++ b/Foxnouns.Backend/Services/DataCleanupService.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 9d048f1..0163516 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs index 888f5fb..9ae61bd 100644 --- a/Foxnouns.Backend/Services/MailService.cs +++ b/Foxnouns.Backend/Services/MailService.cs @@ -1,3 +1,17 @@ +// 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.Interfaces; using Coravel.Queuing.Interfaces; using Foxnouns.Backend.Mailables; diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 06de060..39f73ba 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/MetricsCollectionService.cs b/Foxnouns.Backend/Services/MetricsCollectionService.cs index fdd888e..30568d5 100644 --- a/Foxnouns.Backend/Services/MetricsCollectionService.cs +++ b/Foxnouns.Backend/Services/MetricsCollectionService.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs index eac7ade..abfeafc 100644 --- a/Foxnouns.Backend/Services/ObjectStorageService.cs +++ b/Foxnouns.Backend/Services/ObjectStorageService.cs @@ -1,3 +1,17 @@ +// 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; using Minio.DataModel.Args; diff --git a/Foxnouns.Backend/Services/PeriodicTasksService.cs b/Foxnouns.Backend/Services/PeriodicTasksService.cs index 84ae354..bf0f4af 100644 --- a/Foxnouns.Backend/Services/PeriodicTasksService.cs +++ b/Foxnouns.Backend/Services/PeriodicTasksService.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 6f33583..b5b1c50 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index bb9b05c..bcc2700 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Utils/BootstrapIcons.cs b/Foxnouns.Backend/Utils/BootstrapIcons.cs index 9eafe9c..60461b0 100644 --- a/Foxnouns.Backend/Utils/BootstrapIcons.cs +++ b/Foxnouns.Backend/Utils/BootstrapIcons.cs @@ -1,3 +1,17 @@ +// 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 diff --git a/Foxnouns.Backend/Utils/Limits.cs b/Foxnouns.Backend/Utils/Limits.cs index e396637..3010d46 100644 --- a/Foxnouns.Backend/Utils/Limits.cs +++ b/Foxnouns.Backend/Utils/Limits.cs @@ -1,3 +1,17 @@ +// 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 class Limits diff --git a/Foxnouns.Backend/Utils/PatchRequest.cs b/Foxnouns.Backend/Utils/PatchRequest.cs index 025eeae..2197b92 100644 --- a/Foxnouns.Backend/Utils/PatchRequest.cs +++ b/Foxnouns.Backend/Utils/PatchRequest.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs b/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs index 4bfa2e5..3269c59 100644 --- a/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs +++ b/Foxnouns.Backend/Utils/ScreamingSnakeCaseEnumConverter.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs index 9cc08e0..8a7cad5 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs @@ -12,7 +12,6 @@ // // 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; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs index a6ec90e..da6c61c 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs @@ -12,7 +12,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . - using Foxnouns.Backend.Controllers; using Foxnouns.Backend.Dto; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs index 4d5b444..ea12043 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs @@ -12,7 +12,6 @@ // // 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.Utils; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.cs b/Foxnouns.Backend/Utils/ValidationUtils.cs index e5940d3..a17d331 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.cs @@ -1,3 +1,17 @@ +// 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; /// From 3338243ceac60cc2f0f364fd997ad7d05c89dee6 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 9 Dec 2024 21:48:07 +0100 Subject: [PATCH 161/261] feat: log in with tumblr --- .../Authentication/AuthController.cs | 12 +- .../Authentication/TumblrAuthController.cs | 163 ++++++++++++++++++ .../Auth/RemoteAuthService.Discord.cs | 6 +- .../Services/Auth/RemoteAuthService.Tumblr.cs | 111 ++++++++++++ .../Services/Auth/RemoteAuthService.cs | 1 - .../src/lib/i18n/locales/en.json | 4 +- Foxnouns.Frontend/src/lib/index.ts | 3 +- .../auth/callback/tumblr/+page.server.ts | 8 + .../routes/auth/callback/tumblr/+page.svelte | 31 ++++ .../settings/auth/add-tumblr/+page.server.ts | 12 ++ 10 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs create mode 100644 Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/auth/add-tumblr/+page.server.ts diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 09bd152..99d4c3d 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -48,6 +48,7 @@ public class AuthController( string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); string? discord = null; string? google = null; + string? tumblr = null; if (config.DiscordAuth is { ClientId: not null, ClientSecret: not null }) { discord = @@ -67,7 +68,16 @@ public class AuthController( + $"&redirect_uri={HttpUtility.UrlEncode($"{config.BaseUrl}/auth/callback/google")}"; } - return Ok(new UrlsResponse(config.EmailAuth.Enabled, discord, google, null)); + 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")] diff --git a/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs new file mode 100644 index 0000000..02abddd --- /dev/null +++ b/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs @@ -0,0 +1,163 @@ +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")] +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/Services/Auth/RemoteAuthService.Discord.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs index c06a6f6..e43ea21 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs @@ -53,14 +53,12 @@ public partial class RemoteAuthService throw new FoxnounsError("Invalid Discord OAuth response"); } - DiscordTokenResponse? token = await resp.Content.ReadFromJsonAsync( - ct - ); + 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.token_type} {token.access_token}"); + req.Headers.Add("Authorization", $"{token.TokenType} {token.AccessToken}"); HttpResponseMessage resp2 = await _httpClient.SendAsync(req, ct); resp2.EnsureSuccessStatusCode(); diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs new file mode 100644 index 0000000..be752af --- /dev/null +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs @@ -0,0 +1,111 @@ +// 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 _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 _httpClient.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 _httpClient.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); + } + + private record OauthTokenResponse( + [property: JsonPropertyName("access_token")] string AccessToken, + [property: JsonPropertyName("token_type")] string TokenType + ); + + // 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 index f50d15c..083faf3 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs @@ -19,7 +19,6 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Extensions; using Foxnouns.Backend.Utils; using Humanizer; -using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Services.Auth; diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index df0fd1b..659007e 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -54,7 +54,9 @@ "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" + "remote-google-account-label": "Your Google account", + "register-with-tumblr": "Register with a Tumblr account", + "remote-tumblr-account-label": "Your Tumblr account" }, "error": { "bad-request-header": "Something was wrong with your input", diff --git a/Foxnouns.Frontend/src/lib/index.ts b/Foxnouns.Frontend/src/lib/index.ts index 3e7b36e..fdf8885 100644 --- a/Foxnouns.Frontend/src/lib/index.ts +++ b/Foxnouns.Frontend/src/lib/index.ts @@ -3,8 +3,7 @@ import type { Cookies } from "@sveltejs/kit"; import { DateTime } from "luxon"; -// export const TOKEN_COOKIE_NAME = "__Host-pronounscc-token"; -export const TOKEN_COOKIE_NAME = "pronounscc-token"; +export const TOKEN_COOKIE_NAME = "__Host-pronounscc-token"; export const setToken = (cookies: Cookies, token: string) => cookies.set(TOKEN_COOKIE_NAME, token, { path: "/" }); diff --git a/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.server.ts new file mode 100644 index 0000000..49346f1 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.server.ts @@ -0,0 +1,8 @@ +import createCallbackLoader from "$lib/actions/callback"; +import createRegisterAction from "$lib/actions/register"; + +export const load = createCallbackLoader("tumblr"); + +export const actions = { + default: createRegisterAction("/auth/tumblr/register"), +}; diff --git a/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.svelte b/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.svelte new file mode 100644 index 0000000..c7c53e9 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/callback/tumblr/+page.svelte @@ -0,0 +1,31 @@ + + + + {$t("auth.register-with-tumblr")} • pronouns.cc + + +
    + {#if data.error} +

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

    + + {:else if data.isLinkRequest} + + {:else} + + {/if} +
    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 new file mode 100644 index 0000000..75421b8 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/auth/add-tumblr/+page.server.ts @@ -0,0 +1,12 @@ +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); +}; From 80b7f192f102e32f0b004cde4d4cba1aefc4a193 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Dec 2024 14:09:32 +0100 Subject: [PATCH 162/261] clean up RemoteAuthService --- .../Auth/RemoteAuthService.Discord.cs | 25 ++++++------------- .../Services/Auth/RemoteAuthService.Google.cs | 3 +-- .../Services/Auth/RemoteAuthService.Tumblr.cs | 7 ++---- .../Services/Auth/RemoteAuthService.cs | 7 ++++++ 4 files changed, 17 insertions(+), 25 deletions(-) diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs index e43ea21..de16d2f 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Discord.cs @@ -12,8 +12,7 @@ // // 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 JetBrains.Annotations; +using System.Text.Json.Serialization; namespace Foxnouns.Backend.Services.Auth; @@ -66,22 +65,12 @@ public partial class RemoteAuthService if (user == null) throw new FoxnounsError("Discord user response was null"); - return new RemoteUser(user.id, user.username); + return new RemoteUser(user.Id, user.Username); } - [SuppressMessage( - "ReSharper", - "InconsistentNaming", - Justification = "Easier to use snake_case here, rather than passing in JSON converter options" - )] - [UsedImplicitly] - private record DiscordTokenResponse(string access_token, string token_type); - - [SuppressMessage( - "ReSharper", - "InconsistentNaming", - Justification = "Easier to use snake_case here, rather than passing in JSON converter options" - )] - [UsedImplicitly] - private record DiscordUserResponse(string id, string 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 index a6800cc..3245858 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Google.cs @@ -12,7 +12,6 @@ // // 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; using System.Text.Json; using System.Text.Json.Serialization; @@ -69,7 +68,7 @@ public partial class RemoteAuthService return new RemoteUser(user.Id, user.Email); } - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] + // ReSharper disable once ClassNeverInstantiated.Local private record GoogleTokenResponse([property: JsonPropertyName("id_token")] string IdToken); private record GoogleUser( diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs index be752af..d63ee1a 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.Tumblr.cs @@ -14,6 +14,8 @@ // 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 @@ -83,11 +85,6 @@ public partial class RemoteAuthService return new RemoteUser(blog.Uuid, blog.Name); } - private record OauthTokenResponse( - [property: JsonPropertyName("access_token")] string AccessToken, - [property: JsonPropertyName("token_type")] string TokenType - ); - // tumblr why private record TumblrData( [property: JsonPropertyName("meta")] TumblrMeta Meta, diff --git a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs index 083faf3..6e0ba76 100644 --- a/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/RemoteAuthService.cs @@ -13,6 +13,7 @@ // 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; @@ -35,6 +36,12 @@ public partial class RemoteAuthService( 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. /// From 7e6698c3fb742319c98af43e2e236a313bbf94f1 Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 10 Dec 2024 15:28:44 +0100 Subject: [PATCH 163/261] update to .net 9 and add new OpenAPI packages --- .../Authentication/AuthController.cs | 1 + .../Authentication/DiscordAuthController.cs | 1 + .../Authentication/EmailAuthController.cs | 1 + .../Authentication/FediverseAuthController.cs | 1 + .../Authentication/GoogleAuthController.cs | 1 + .../Authentication/TumblrAuthController.cs | 1 + .../Controllers/ExportsController.cs | 1 + .../Controllers/FlagsController.cs | 1 + .../Controllers/InternalController.cs | 1 + .../Controllers/MembersController.cs | 1 + Foxnouns.Backend/Controllers/SidController.cs | 1 + Foxnouns.Backend/Database/Snowflake.cs | 17 + Foxnouns.Backend/Foxnouns.Backend.csproj | 38 +- Foxnouns.Backend/Program.cs | 32 +- .../OpenApi/PropertyKeySchemaTransformer.cs | 69 + Foxnouns.Backend/packages.lock.json | 1275 ++++------------- migrators/NetImporter/NetImporter.csproj | 10 +- 17 files changed, 451 insertions(+), 1001 deletions(-) create mode 100644 Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 99d4c3d..0d95eb2 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -26,6 +26,7 @@ using Microsoft.EntityFrameworkCore; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth")] +[ApiExplorerSettings(IgnoreApi = true)] public class AuthController( Config config, DatabaseContext db, diff --git a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs index a3894e6..a82956a 100644 --- a/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/DiscordAuthController.cs @@ -31,6 +31,7 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth/discord")] +[ApiExplorerSettings(IgnoreApi = true)] public class DiscordAuthController( [UsedImplicitly] Config config, ILogger logger, diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 006a8ed..cedf962 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -30,6 +30,7 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth/email")] +[ApiExplorerSettings(IgnoreApi = true)] public class EmailAuthController( [UsedImplicitly] Config config, DatabaseContext db, diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index 5496094..d95c622 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -28,6 +28,7 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth/fediverse")] +[ApiExplorerSettings(IgnoreApi = true)] public class FediverseAuthController( ILogger logger, DatabaseContext db, diff --git a/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs index ba9f6cc..0f386d7 100644 --- a/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/GoogleAuthController.cs @@ -31,6 +31,7 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth/google")] +[ApiExplorerSettings(IgnoreApi = true)] public class GoogleAuthController( [UsedImplicitly] Config config, ILogger logger, diff --git a/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs index 02abddd..919ca3f 100644 --- a/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs @@ -17,6 +17,7 @@ using NodaTime; namespace Foxnouns.Backend.Controllers.Authentication; [Route("/api/internal/auth/tumblr")] +[ApiExplorerSettings(IgnoreApi = true)] public class TumblrAuthController( [UsedImplicitly] Config config, ILogger logger, diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs index cd70661..315efbc 100644 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ b/Foxnouns.Backend/Controllers/ExportsController.cs @@ -26,6 +26,7 @@ namespace Foxnouns.Backend.Controllers; [Route("/api/internal/data-exports")] [Authorize("identify")] +[ApiExplorerSettings(IgnoreApi = true)] public class ExportsController( ILogger logger, Config config, diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index df0214a..0c35afd 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -80,6 +80,7 @@ public class FlagsController( [HttpPatch("{id}")] [Authorize("user.update")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) { ValidationUtils.Validate(ValidateFlag(req.Name, req.Description, null)); diff --git a/Foxnouns.Backend/Controllers/InternalController.cs b/Foxnouns.Backend/Controllers/InternalController.cs index 1180a34..85bc774 100644 --- a/Foxnouns.Backend/Controllers/InternalController.cs +++ b/Foxnouns.Backend/Controllers/InternalController.cs @@ -25,6 +25,7 @@ namespace Foxnouns.Backend.Controllers; [ApiController] [Route("/api/internal")] +[ApiExplorerSettings(IgnoreApi = true)] public partial class InternalController(DatabaseContext db) : ControllerBase { [GeneratedRegex(@"(\{\w+\})")] diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index e42fffa..534c51f 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -144,6 +144,7 @@ public class MembersController( } [HttpPatch("/api/v2/users/@me/members/{memberRef}")] + [ProducesResponseType(statusCode: StatusCodes.Status200OK)] [Authorize("member.update")] public async Task UpdateMemberAsync( string memberRef, diff --git a/Foxnouns.Backend/Controllers/SidController.cs b/Foxnouns.Backend/Controllers/SidController.cs index a0067bf..3a8db19 100644 --- a/Foxnouns.Backend/Controllers/SidController.cs +++ b/Foxnouns.Backend/Controllers/SidController.cs @@ -25,6 +25,7 @@ namespace Foxnouns.Backend.Controllers; "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}")] diff --git a/Foxnouns.Backend/Database/Snowflake.cs b/Foxnouns.Backend/Database/Snowflake.cs index 8cf9141..fc2188d 100644 --- a/Foxnouns.Backend/Database/Snowflake.cs +++ b/Foxnouns.Backend/Database/Snowflake.cs @@ -15,6 +15,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Text.Json; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Newtonsoft.Json; using NodaTime; @@ -23,6 +24,7 @@ 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 { @@ -96,6 +98,21 @@ public readonly struct Snowflake(ulong value) : IEquatable // ReSharper disable once ClassNeverInstantiated.Global public class ValueConverter() : ValueConverter(x => x, x => x); + private class SystemJsonConverter : System.Text.Json.Serialization.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( diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 794f2b9..c6f9605 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -1,6 +1,6 @@ - net8.0 + net9.0 enable enable true @@ -8,39 +8,39 @@ - - - - + + + + - - - - - + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + + - - + diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 02a289a..3597ae7 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -12,14 +12,18 @@ // // 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.Extensions; using Foxnouns.Backend.Services; using Foxnouns.Backend.Utils; +using Foxnouns.Backend.Utils.OpenApi; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Prometheus; +using Scalar.AspNetCore; using Sentry.Extensibility; using Serilog; @@ -46,6 +50,13 @@ builder builder .Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + options.JsonSerializerOptions.Converters.Add( + new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseUpper) + ); + }) .AddNewtonsoftJson(options => { options.SerializerSettings.ContractResolver = new PatchRequestContractResolver @@ -60,6 +71,16 @@ builder ); }); +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 @@ -70,7 +91,7 @@ JsonConvert.DefaultSettings = () => }, }; -builder.AddServices(config).AddCustomMiddleware().AddEndpointsApiExplorer().AddSwaggerGen(); +builder.AddServices(config).AddCustomMiddleware(); WebApplication app = builder.Build(); @@ -83,11 +104,16 @@ app.UseRouting(); // 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.MapOpenApi("/api-docs/openapi/{documentName}.json"); +app.MapScalarApiReference(options => +{ + options.Title = "pronouns.cc API"; + options.OpenApiRoutePattern = "/api-docs/openapi/{documentName}.json"; + options.EndpointPathPrefix = "/api-docs/{documentName}"; +}); app.Urls.Clear(); app.Urls.Add(config.Address); diff --git a/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs b/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs new file mode 100644 index 0000000..5be20cd --- /dev/null +++ b/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs @@ -0,0 +1,69 @@ +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/packages.lock.json b/Foxnouns.Backend/packages.lock.json index 901341c..03170c1 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -1,14 +1,15 @@ { "version": 1, "dependencies": { - "net8.0": { + "net9.0": { "Coravel": { "type": "Direct", - "requested": "[5.0.4, )", - "resolved": "5.0.4", - "contentHash": "Bp3G5tmJgTpTYAYB86OljIbNZb2qt5iByWLUMYUZGA/hccoavKg2SWl5gs29WcrlHn96DVA+O+9NUEjVq3nUng==", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "U16V/IxGL2TcpU9sT1gUA3pqoVIlz+WthC4idn8OTPiEtLElTcmNF6sHt+gOx8DRU8TBgN5vjfL4AHetjacOWQ==", "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" @@ -16,33 +17,33 @@ }, "Coravel.Mailer": { "type": "Direct", - "requested": "[5.0.1, )", - "resolved": "5.0.1", - "contentHash": "tU6CXDqZRZZGNM8yLPoz91OnvEQaougEqEPp0ZKjR5xa9bLUOujlX3xU2CfxVbb3kS+nMjY2Y3vlOgmbO6qGFA==", + "requested": "[7.0.0, )", + "resolved": "7.0.0", + "contentHash": "mxSlOOBxPjCAZruOpgXtubnZA9lD0DRgutApQmAsts7DoRfe0wTzqWrYjeZTiIzgVJZKZxJglN8duTvbPrw3jQ==", "dependencies": { - "MailKit": "2.5.1", - "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.0" + "MailKit": "4.3.0", + "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.27" } }, "EFCore.NamingConventions": { "type": "Direct", - "requested": "[8.0.3, )", - "resolved": "8.0.3", - "contentHash": "TdDarM6kyIS2oVIhrs3W+r+xL/76ooFJxIXxfhzsNJQu0pB9VdFZwuyKvKJnhoc7OHYFNTBP08AN37kr4CPc+Q==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "heKIYzPdEWx+Ba4xuG6jfEssW9rEi7I0lX38eoN7wo7qgg9uw7nn8UEmDQfwGEYPzSDpetCVANnDr5tqt2Asjg==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[8.0.0, 9.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[8.0.0, 9.0.0)", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "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.2, )", - "resolved": "8.1.2", - "contentHash": "SIIHSTcfN04sCY5YMC3azD3lTvGLIT+VjO5/8zaqSqxzAvAaJ9x7ZVr5M6j8ORAxeezIe9+X/XRRZ+mRNbM89w==", + "requested": "[8.1.3, )", + "resolved": "8.1.3", + "contentHash": "hqTsPy2SeupzawK/AeH5/8/K7+KEdZjQbyKVlxBX45ei86eU8D14X9E06uS6MX+J5TdSKY6o+WXGS7G2k8Xvyw==", "dependencies": { - "EntityFrameworkCore.Exceptions.Common": "8.1.2", - "Npgsql": "8.0.1" + "EntityFrameworkCore.Exceptions.Common": "8.1.3", + "Npgsql": "8.0.3" } }, "Humanizer.Core": { @@ -53,53 +54,61 @@ }, "JetBrains.Annotations": { "type": "Direct", - "requested": "[2024.2.0, )", - "resolved": "2024.2.0", - "contentHash": "GNnqCFW/163p1fOehKx0CnAqjmpPrUSqrgfHM6qca+P+RN39C9rhlfZHQpJhxmQG/dkOYe/b3Z0P8b6Kv5m1qw==" + "requested": "[2024.3.0, )", + "resolved": "2024.3.0", + "contentHash": "ox5pkeLQXjvJdyAB4b2sBYAlqZGLh3PjSnP1bQNVx72ONuTJ9+34/+Rq91Fc0dG29XG9RgZur9+NcP4riihTug==" }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "Y8v0z0Zrc28wKxY98sGBwjnHqXeSDFh3VQ5ZEsIxGHD/1g9z2zulDJ8ue6Vww1F4gcUkY9XCQUHljNQu3my35A==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "pTFDEmZi3GheCSPrBxzyE63+d5unln2vYldo/nOm1xet/4rpEk2oJYcwpclPQ13E+LZBF9XixkgwYTUwqznlWg==", "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "8.0.7", + "Microsoft.AspNetCore.JsonPatch": "9.0.0", "Newtonsoft.Json": "13.0.3", "Newtonsoft.Json.Bson": "1.0.2" } }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "9SBDNvlwA88r5oD7yUbTmwr9ylkmZWdPQgohBWCdz6cESDAo6JgCD5vEOZS/nq2WIL5SCn3/RamAStcdiRzd4g==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "FqUK5j1EOPNuFT7IafltZQ3cakqhSwVzH5ZW1MhZDe4pPXs9sJ2M5jom1Omsu+mwF2tNKKlRAzLRHQTZzbd+6Q==", "dependencies": { - "Microsoft.OpenApi": "1.4.3" + "Microsoft.OpenApi": "1.6.17" } }, "Microsoft.EntityFrameworkCore": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "UOyPNAgyzw/E4hUCurqvZxi0WWVLQAGZuntFPzkTXtvJLTqRjKvokvhv+XazAUSODLsU1DZ67GjZ4mT9d82+0g==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "wpG+nfnfDAw87R3ovAsUmjr3MZ4tYXf6bFqEPVAIKE6IfPml3DS//iX0DBnf8kWn5ZHSO5oi1m4d/Jf+1LifJQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "8.0.7", - "Microsoft.EntityFrameworkCore.Analyzers": "8.0.7", - "Microsoft.Extensions.Caching.Memory": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0" + "Microsoft.EntityFrameworkCore.Abstractions": "9.0.0", + "Microsoft.EntityFrameworkCore.Analyzers": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0" } }, "Microsoft.EntityFrameworkCore.Design": { "type": "Direct", - "requested": "[8.0.7, )", - "resolved": "8.0.7", - "contentHash": "EUPY49Hi5BbpnkiX9ik/2fD9GPEbvKx6wvDmDNZTHZGlXAg1kcR9vt2QA2af1mIoa7gG1wqEvyQRWf9/A8gWqQ==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "Pqo8I+yHJ3VQrAoY0hiSncf+5P7gN/RkNilK5e+/K/yKh+yAWxdUAI6t0TG26a9VPlCa9FhyklzyFvRyj3YG9A==", "dependencies": { "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp.Workspaces": "4.5.0", - "Microsoft.EntityFrameworkCore.Relational": "8.0.7", - "Microsoft.Extensions.DependencyModel": "8.0.1", - "Mono.TextTemplating": "2.2.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.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyModel": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Mono.TextTemplating": "3.0.0", + "System.Text.Json": "9.0.0" } }, "Microsoft.Extensions.Caching.Memory": { @@ -136,43 +145,39 @@ }, "NodaTime": { "type": "Direct", - "requested": "[3.1.11, )", - "resolved": "3.1.11", - "contentHash": "AYSiCHp1PLzWKVf7hEL3MJ0q9kzOWMNIaTVysXk4XKrDBzK5PF2wpd4LsAl+EIQ2Hbvu+vw4oFaexcXzCuY1lQ==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.7.1" - } + "requested": "[3.2.0, )", + "resolved": "3.2.0", + "contentHash": "yoRA3jEJn8NM0/rQm78zuDNPA3DonNSZdsorMUj+dltc1D+/Lc5h9YXGqbEEZozMGr37lAoYkcSM/KjTVqD0ow==" }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", - "requested": "[8.0.4, )", - "resolved": "8.0.4", - "contentHash": "/hHd9MqTRVDgIpsToCcxMDxZqla0HAQACiITkq1+L9J2hmHKV6lBAPlauF+dlNSfHpus7rrljWx4nAanKD6qAw==", + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "cYdOGplIvr9KgsG8nJ8xnzBTImeircbgetlzS1OmepS5dAQW6PuGpVrLOKBNEwEvGYZPsV8037X5vZ/Dmpwz7Q==", "dependencies": { - "Microsoft.EntityFrameworkCore": "8.0.4", - "Microsoft.EntityFrameworkCore.Abstractions": "8.0.4", - "Microsoft.EntityFrameworkCore.Relational": "8.0.4", - "Npgsql": "8.0.3" + "Microsoft.EntityFrameworkCore": "[9.0.0, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.0, 10.0.0)", + "Npgsql": "9.0.2" } }, "Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": { "type": "Direct", - "requested": "[8.0.4, )", - "resolved": "8.0.4", - "contentHash": "IJqmyj4BkqbCAm1MDZEwaUuxzYVbhtqghfkP2B9u089uCQgOtdcGbJYQwN2dyxO1ze16VDhTLUZwiq7Us1jdvg==", + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "+mfwiRCK+CAKTkeBZCuQuMaOwM/yMX8B65515PS1le9TUjlG8DobuAmb48MSR/Pr/YMvU1tV8FFEFlyQviQzrg==", "dependencies": { - "Npgsql.EntityFrameworkCore.PostgreSQL": "8.0.4", - "Npgsql.NodaTime": "8.0.3" + "Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.2", + "Npgsql.NodaTime": "9.0.2" } }, "Npgsql.Json.NET": { "type": "Direct", - "requested": "[8.0.3, )", - "resolved": "8.0.3", - "contentHash": "5YqEP+RDlbP4ecQ2ndw7+NeiLOD2PxnJE5JnyBendn5ipuOGD6iVt+rS7pxyPKRyNAmrRRT3iOJrUPUDRW/Ncw==", + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "E81dvvpNtS4WigxZu16OAFxVvPvbEkXI7vJXZzEp7GQ03MArF5V4HBb7KXDzTaE5ZQ0bhCUFoMTODC6Z8mu27g==", "dependencies": { "Newtonsoft.Json": "13.0.3", - "Npgsql": "8.0.3" + "Npgsql": "9.0.2" } }, "prometheus-net": { @@ -200,38 +205,41 @@ "resolved": "4.12.9", "contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w==" }, + "Scalar.AspNetCore": { + "type": "Direct", + "requested": "[1.2.51, )", + "resolved": "1.2.51", + "contentHash": "3eA0doeYYWwLKHUsStHslTmSLmvuEEatmRanb4CJyhBQQOF7u3dVF2Mx7ZAc7b3GBiMfaB/QxYvFdxxOfgRV9Q==" + }, "Sentry.AspNetCore": { "type": "Direct", - "requested": "[4.9.0, )", - "resolved": "4.9.0", - "contentHash": "927iNu4O4f+q8mVXNmekRHil0KIZGhFH7w8TJ4wHXxrZiP0z7Q21yWfYamyWO8z2rXFwm4G7WlSaEjU6RIZ3Uw==", + "requested": "[4.13.0, )", + "resolved": "4.13.0", + "contentHash": "1cH9hSvjRbTkcpjUejFTrTC3jMIiOrcZ0DIvt16+AYqXhuxPEnI56npR1nhv+7WUGyhyp5cHFIZqrKnyrrGP0w==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Sentry.Extensions.Logging": "4.9.0" + "Sentry.Extensions.Logging": "4.13.0" } }, "Serilog": { "type": "Direct", - "requested": "[4.0.1, )", - "resolved": "4.0.1", - "contentHash": "pzeDRXdpSLSsgBHpZcmpIDxqMy845Ab4s+dfnBg0sN9h8q/4Wo3vAoe0QCGPze1Q06EVtEPupS+UvLm8iXQmTQ==" + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "gmoWVOvKgbME8TYR+gwMf7osROiWAURterc6Rt2dQyX7wtjZYpqFiA/pY6ztjGQKKV62GGCyOcmtP1UKMHgSmA==" }, "Serilog.AspNetCore": { "type": "Direct", - "requested": "[8.0.1, )", - "resolved": "8.0.1", - "contentHash": "B/X+wAfS7yWLVOTD83B+Ip9yl4MkhioaXj90JSoWi1Ayi8XHepEnsBdrkojg08eodCnmOKmShFUN2GgEc6c0CQ==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "JslDajPlBsn3Pww1554flJFTqROvK9zz9jONNQgn0D8Lx2Trw8L0A8/n6zEQK1DAZWXrJwiVLw8cnTR3YFuYsg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Serilog": "3.1.1", - "Serilog.Extensions.Hosting": "8.0.0", - "Serilog.Extensions.Logging": "8.0.0", - "Serilog.Formatting.Compact": "2.0.0", - "Serilog.Settings.Configuration": "8.0.0", - "Serilog.Sinks.Console": "5.0.0", - "Serilog.Sinks.Debug": "2.0.0", - "Serilog.Sinks.File": "5.0.0" + "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": { @@ -255,31 +263,15 @@ }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[3.1.5, )", - "resolved": "3.1.5", - "contentHash": "lNtlq7dSI/QEbYey+A0xn48z5w4XHSffF8222cC4F4YwTXfEImuiBavQcWjr49LThT/pRmtWJRcqA/PlL+eJ6g==" - }, - "Swashbuckle.AspNetCore": { - "type": "Direct", - "requested": "[6.6.2, )", - "resolved": "6.6.2", - "contentHash": "+NB4UYVYN6AhDSjW0IJAd1AGD8V33gemFNLPaxKTtPkHB+HaKAKf9MGAEUPivEWvqeQfcKIw8lJaHq6LHljRuw==", - "dependencies": { - "Microsoft.Extensions.ApiDescription.Server": "6.0.5", - "Swashbuckle.AspNetCore.Swagger": "6.6.2", - "Swashbuckle.AspNetCore.SwaggerGen": "6.6.2", - "Swashbuckle.AspNetCore.SwaggerUI": "6.6.2" - } + "requested": "[3.1.6, )", + "resolved": "3.1.6", + "contentHash": "dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA==" }, "System.Text.Json": { "type": "Direct", "requested": "[9.0.0, )", "resolved": "9.0.0", - "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==", - "dependencies": { - "System.IO.Pipelines": "9.0.0", - "System.Text.Encodings.Web": "9.0.0" - } + "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==" }, "System.Text.RegularExpressions": { "type": "Direct", @@ -290,6 +282,11 @@ "System.Runtime": "4.3.1" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" + }, "CommunityToolkit.HighPerformance": { "type": "Transitive", "resolved": "8.2.2", @@ -297,27 +294,24 @@ }, "EntityFrameworkCore.Exceptions.Common": { "type": "Transitive", - "resolved": "8.1.2", - "contentHash": "Yy1qw+mdXhHyptH42o2suEaNDZlcmwiaQZ56v8tUVUxUq33GQSYyTJ6wE2WuB2AjunTa4tPhieu2E+m6z/GcTg==", + "resolved": "8.1.3", + "contentHash": "nweeiVHx4HbDi6+TqendOe0QmN0a9v0AB5FaL83eToqFFztwGIhOqLfveKqJDYSCU51CJShW8kbU1onZLdZZSg==", "dependencies": { "Microsoft.EntityFrameworkCore.Relational": "8.0.0" } }, "MailKit": { "type": "Transitive", - "resolved": "2.5.1", - "contentHash": "0sLYQsWszoqdQorVnfCMsRswBYwodpO03fS5Ij1m7Qx6B02IlgXYLwsLqIJHkvUOoEOazrwxIerIZwujV1/Mbw==", + "resolved": "4.3.0", + "contentHash": "jVmB3Nr0JpqhyMiXOGWMin+QvRKpucGpSFBCav9dG6jEJPdBV+yp1RHVpKzxZPfT+0adaBuZlMFdbIciZo1EWA==", "dependencies": { - "MimeKit": "2.5.1", - "System.Net.NameResolution": "4.3.0", - "System.Net.Security": "4.3.2", - "System.Runtime.Serialization.Primitives": "4.3.0" + "MimeKit": "4.3.0" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", - "resolved": "8.0.7", - "contentHash": "/cNcH8Qd+3yH4yVUlOsZnxPcsmwnYSnkybKX7PMgrJkaH7ilp2IrcqJN4M376As+y1f+MVUyRqRtosB+PWWQbg==", + "resolved": "9.0.0", + "contentHash": "/4UONYoAIeexPoAmbzBPkVGA6KAY7t0BM+1sr0fKss2V1ERCdcM+Llub4X5Ma+LJ60oPp6KzM0e3j+Pp/JHCNw==", "dependencies": { "Microsoft.CSharp": "4.7.0", "Newtonsoft.Json": "13.0.3" @@ -325,90 +319,110 @@ }, "Microsoft.AspNetCore.Mvc.Razor.Extensions": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "M0h+ChPgydX2xY17agiphnAVa/Qh05RAP8eeuqGGhQKT10claRBlLNO6d2/oSV8zy0RLHzwLnNZm5xuC/gckGA==", + "resolved": "6.0.27", + "contentHash": "trwJhFrTQuJTImmixMsDnDgRE8zuTzAUAot7WqiUlmjNzlJWLOaXXBpeA/xfNJvZuOsyGjC7RIzEyNyDGhDTLg==", "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.0", - "Microsoft.CodeAnalysis.Razor": "6.0.0" + "Microsoft.AspNetCore.Razor.Language": "6.0.27", + "Microsoft.CodeAnalysis.Razor": "6.0.27" } }, "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "bf4kbla/8Qiu53MgFPz1u3H1ThoookPpFy+Ya9Q9p531wXK1pZ3tfz/Gtx8SKy41yz99jhZHTUM1QqLl7eJRgQ==", + "resolved": "6.0.27", + "contentHash": "C6Gh/sAuUACxNtllcH4ZniWtPcGbixJuB1L5RXwoUe1a1wM6rpQ2TVMWpX2+cgeBj8U/izJyWY+nJ4Lz8mmMKA==", "dependencies": { - "Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.0", - "Microsoft.CodeAnalysis.Razor": "6.0.0", + "Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.27", + "Microsoft.CodeAnalysis.Razor": "6.0.27", "Microsoft.Extensions.DependencyModel": "6.0.0" } }, "Microsoft.AspNetCore.Razor.Language": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "yCtBr1GSGzJrrp1NJUb4ltwFYMKHw/tJLnIDvg9g/FnkGIEzmE19tbCQqXARIJv5kdtBgsoVIdGLL+zmjxvM/A==" + "resolved": "6.0.27", + "contentHash": "bI1kIZBgx7oJIB7utPrw4xIgcj7Pdx1jnHMTdsG54U602OcGpBzbfAuKaWs+LVdj+zZVuZsCSoRIZNJKTDP7Hw==" }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "UcSjPsst+DfAdJGVDsu346FX0ci0ah+lw3WRtn18NUwEqRt70HaOQ7lI72vy3+1LxtqI3T5GWwV39rQSrCzAeg==" + "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.3", - "contentHash": "j/rOZtLMVJjrfLRlAMckJLPW/1rze9MT1yfWqSIbUPGRu1m1P0fuo9PmqapwsmePfGB5PJrudQLvmUOAMF0DqQ==" + "resolved": "3.3.4", + "contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==" }, "Microsoft.CodeAnalysis.Common": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "lwAbIZNdnY0SUNoDmZHkVUwLO8UyNnyyh1t/4XsbFxi4Ounb3xszIYZaWhyj5ZjyfcwqwmtMbE7fUTVCqQEIdQ==", + "resolved": "4.8.0", + "contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==", "dependencies": { - "Microsoft.CodeAnalysis.Analyzers": "3.3.3", - "System.Collections.Immutable": "6.0.0", - "System.Reflection.Metadata": "6.0.1", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encoding.CodePages": "6.0.0" + "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.5.0", - "contentHash": "cM59oMKAOxvdv76bdmaKPy5hfj+oR+zxikWoueEB7CwTko7mt9sVKZI8Qxlov0C/LuKEG+WQwifepqL3vuTiBQ==", + "resolved": "4.8.0", + "contentHash": "+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==", "dependencies": { - "Microsoft.CodeAnalysis.Common": "[4.5.0]" + "Microsoft.CodeAnalysis.Common": "[4.8.0]" } }, "Microsoft.CodeAnalysis.CSharp.Workspaces": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "h74wTpmGOp4yS4hj+EvNzEiPgg/KVs2wmSfTZ81upJZOtPkJsVkgfsgtxxqmAeapjT/vLKfmYV0bS8n5MNVP+g==", + "resolved": "4.8.0", + "contentHash": "3amm4tq4Lo8/BGvg9p3BJh3S9nKq2wqCXfS7138i69TUpo/bD+XvD0hNurpEBtcNZhi1FyutiomKJqVF39ugYA==", "dependencies": { "Humanizer.Core": "2.14.1", - "Microsoft.CodeAnalysis.CSharp": "[4.5.0]", - "Microsoft.CodeAnalysis.Common": "[4.5.0]", - "Microsoft.CodeAnalysis.Workspaces.Common": "[4.5.0]" + "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.0", - "contentHash": "uqdzuQXxD7XrJCbIbbwpI/LOv0PBJ9VIR0gdvANTHOfK5pjTaCir+XcwvYvBZ5BIzd0KGzyiamzlEWw1cK1q0w==", + "resolved": "6.0.27", + "contentHash": "NAUvSjH8QY8gPp/fXjHhi3MnQEGtSJA0iRT/dT3RKO3AdGACPJyGmKEKxLag9+Kf2On51yGHT9DEPPnK3hyezg==", "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.0", + "Microsoft.AspNetCore.Razor.Language": "6.0.27", "Microsoft.CodeAnalysis.CSharp": "4.0.0", "Microsoft.CodeAnalysis.Common": "4.0.0" } }, "Microsoft.CodeAnalysis.Workspaces.Common": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "l4dDRmGELXG72XZaonnOeORyD/T5RpEu5LGHOUIhnv+MmUWDY/m1kWXGwtcgQ5CJ5ynkFiRnIYzTKXYjUs7rbw==", + "resolved": "4.8.0", + "contentHash": "LXyV+MJKsKRu3FGJA3OmSk40OUIa/dQCFLOnm5X8MNcujx7hzGu8o+zjXlb/cy5xUdZK2UKYb9YaQ2E8m9QehQ==", "dependencies": { "Humanizer.Core": "2.14.1", - "Microsoft.Bcl.AsyncInterfaces": "6.0.0", - "Microsoft.CodeAnalysis.Common": "[4.5.0]", - "System.Composition": "6.0.0", - "System.IO.Pipelines": "6.0.3", - "System.Threading.Channels": "6.0.0" + "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": { @@ -418,28 +432,25 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "8.0.7", - "contentHash": "DHX6nxcg4/tpWfTjAleKrXveDiNFY/OGOK6nm27GipUXNI2Uofev9cH5SYXmtGIgHWxlvfn754TXN4WnrixOwg==" + "resolved": "9.0.0", + "contentHash": "fnmifFL8KaA4ZNLCVgfjCWhZUFxkrDInx5hR4qG7Q8IEaSiy/6VOSRFyx55oH7MV4y7wM3J3EE90nSpcVBI44Q==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "8.0.7", - "contentHash": "nerD0vEOYJVhVapamRVH9DrUYbDNMJ5bPfWze4SibDDaDaekzgwQqBht97/tV+8pgdKoPAXmtiJsB+lDajwVrQ==" + "resolved": "9.0.0", + "contentHash": "Qje+DzXJOKiXF72SL0XxNlDtTkvWWvmwknuZtFahY5hIQpRKO59qnGuERIQ3qlzuq5x4bAJ8WMbgU5DLhBgeOQ==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "8.0.7", - "contentHash": "Hn86yScnW+VXb+A2LGrVGkGmjsQ9KLWR0T8GQBEcESWk8u9JYhBiRtdxz76Aq0ir82Ei48sLEZTN4VE0sJ3yIg==", + "resolved": "9.0.0", + "contentHash": "j+msw6fWgAE9M3Q/5B9Uhv7pdAdAQUvFPJAiBJmoy+OXvehVbfbCE8ftMAa51Uo2ZeiqVnHShhnv4Y4UJJmUzA==", "dependencies": { - "Microsoft.EntityFrameworkCore": "8.0.7", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + "Microsoft.EntityFrameworkCore": "9.0.0", + "Microsoft.Extensions.Caching.Memory": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0" } }, - "Microsoft.Extensions.ApiDescription.Server": { - "type": "Transitive", - "resolved": "6.0.5", - "contentHash": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==" - }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", "resolved": "9.0.0", @@ -459,26 +470,26 @@ }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3lE/iLSutpgX1CC0NOW70FJoGARRHbyKmG7dc0klnUZ9Dd9hS6N/POPWhKhMLCEuNN5nXEY5agmlFtH562vqhQ==", + "resolved": "9.0.0", + "contentHash": "lqvd7W3FGKUO1+ZoUEMaZ5XDJeWvjpy2/M/ptCGz3tXLD4HWVaSzjufsAsjemasBEg+2SxXVtYVvGt5r2nKDlg==", "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Configuration.Binder": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "mBMoXLsr5s1y2zOHWmKsE9veDcx8h1x/c3rz4baEdQKTeDcmQAPNbB54Pi/lhFO3K431eEq6PFbMgLaa6PHFfA==", + "resolved": "9.0.0", + "contentHash": "RiScL99DcyngY9zJA2ROrri7Br8tn5N4hP4YNvGdTN/bvg1A3dwvDOxHnNZ3Im7x2SJ5i4LkX1uPiR/MfSFBLQ==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0" } }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "V8S3bsm50ig6JSyrbcJJ8bW2b9QLGouz+G1miK3UTaOWmMtFwNNNzUf4AleyDWUmTrWMLNnFSLEQtxmxgNQnNQ==", + "resolved": "9.0.0", + "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { @@ -488,12 +499,8 @@ }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "8.0.1", - "contentHash": "5Ou6varcxLBzQ+Agfm0k0pnH7vrEITYlXMDuE6s7ZHlZHz6/G8XJ3iISZDr5rfwfge6RnXJ1+Wc479mMn52vjA==", - "dependencies": { - "System.Text.Encodings.Web": "8.0.0", - "System.Text.Json": "8.0.4" - } + "resolved": "9.0.0", + "contentHash": "saxr2XzwgDU77LaQfYFXmddEDRUKHF4DaGMZkNB3qjdVSZlax3//dGJagJkKrGMIPNZs2jVFXITyCCR6UHJNdA==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", @@ -507,32 +514,31 @@ }, "Microsoft.Extensions.Diagnostics.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "JHYCQG7HmugNYUhOl368g+NMxYE/N/AiclCYRNlgCY9eVyiBkOHMwK4x60RYMxv9EL3+rmj1mqHvdCiPpC+D4Q==", + "resolved": "9.0.0", + "contentHash": "1K8P7XzuzX8W8pmXcZjcrqS6x5eSSdvhQohmcpgiQNY/HlDAlnrhR9dvlURfFz428A+RTCJpUyB+aKTA6AgVcQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "System.Diagnostics.DiagnosticSource": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" } }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ZbaMlhJlpisjuWbvXr4LdAst/1XxH3vZ6A0BsgTphZ2L4PGuxRLz7Jr/S7mkAAnOn78Vu0fKhEgNF5JO3zfjqQ==", + "resolved": "9.0.0", + "contentHash": "uK439QzYR0q2emLVtYzwyK3x+T5bTY4yWsd/k/ZUS9LR6Sflp8MIdhGXW8kQCd86dQD4tLqvcbLkku8qHY263Q==", "dependencies": { - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Hosting.Abstractions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "AG7HWwVRdCHlaA++1oKDxLsXIBxmDpMPb3VoyOoAghEWnkUvEAdYQUwnV4jJbAaa/nMYNiEh5ByoLauZBEiovg==", + "resolved": "9.0.0", + "contentHash": "yUKJgu81ExjvqbNWqZKshBbLntZMbMVz/P7Way2SBx7bMqA08Mfdc9O7hWDKAiSp+zPUGT6LKcSCQIPeDK+CCw==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0" } }, "Microsoft.Extensions.Http": { @@ -550,12 +556,12 @@ }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "tvRkov9tAJ3xP51LCv3FJ2zINmv1P8Hi8lhhtcKGqM+ImiTCC84uOPEI4z8Cdq2C3o9e+Aa0Gw0rmrsJD77W+w==", + "resolved": "9.0.0", + "contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { @@ -563,8 +569,7 @@ "resolved": "9.0.0", "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "System.Diagnostics.DiagnosticSource": "9.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" } }, "Microsoft.Extensions.Logging.Configuration": { @@ -625,37 +630,26 @@ }, "Microsoft.OpenApi": { "type": "Transitive", - "resolved": "1.6.14", - "contentHash": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==" - }, - "Microsoft.Win32.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } + "resolved": "1.6.17", + "contentHash": "Le+kehlmrlQfuDFUt1zZ2dVwrhFQtKREdKBo+rexOwaCoYP0/qpgT9tLxCsZjsgR5Itk1UKPcbgO+FyaNid/bA==" }, "MimeKit": { "type": "Transitive", - "resolved": "2.5.1", - "contentHash": "KcySIws+ZT1ubER/Z09T9FQ2fsliqFORuLayKCr3mj0Sk4mqXKMvuX2B/W7FHMrLYocYrnU6ngvwVExxwxFrmA==", + "resolved": "4.3.0", + "contentHash": "39KDXuERDy5VmHIn7NnCWvIVp/Ar4qnxZWg9m06DfRqDbW1B6zFv9o3Tdoa4CCu71tE/0SRqRCN5Z+bbffw6uw==", "dependencies": { - "Portable.BouncyCastle": "1.8.5", - "System.Data.Common": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.Reflection.TypeExtensions": "4.3.0", - "System.Text.Encoding.CodePages": "4.3.0" + "BouncyCastle.Cryptography": "2.2.1", + "System.Runtime.CompilerServices.Unsafe": "6.0.0", + "System.Security.Cryptography.Pkcs": "7.0.3", + "System.Text.Encoding.CodePages": "7.0.0" } }, "Mono.TextTemplating": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "KZYeKBET/2Z0gY1WlTAK7+RHTl7GSbtvTLDXEZZojUdAPqpQNDL6tHv7VUpqfX5VEOh+uRGKaZXkuD253nEOBQ==", + "resolved": "3.0.0", + "contentHash": "YqueG52R/Xej4VVbKuRIodjiAhV0HR/XVbLbNrJhCZnzjnSjgMJ/dCdV0akQQxavX6hp/LC6rqLGLcXeQYU7XA==", "dependencies": { - "System.CodeDom": "4.4.0" + "System.CodeDom": "6.0.0" } }, "Newtonsoft.Json.Bson": { @@ -668,421 +662,154 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "8.0.3", - "contentHash": "6WEmzsQJCZAlUG1pThKg/RmeF6V+I0DmBBBE/8YzpRtEzhyZzKcK7ulMANDm5CkxrALBEC8H+5plxHWtIL7xnA==", + "resolved": "9.0.2", + "contentHash": "hCbO8box7i/XXiTFqCJ3GoowyLqx3JXxyrbOJ6om7dr+eAknvBNhhUHeJVGAQo44sySZTfdVffp4BrtPeLZOAA==", "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, "Npgsql.NodaTime": { "type": "Transitive", - "resolved": "8.0.3", - "contentHash": "C0TzQcc4+/6jpRGb/YxphmCwRwSuWde9Yz9GWap1xfX2m6/QkYBhluv/EjsPCTWq3/UZaP9UgiUZKscTLNZt4g==", + "resolved": "9.0.2", + "contentHash": "jURb6VGmmR3pPae2N3HrUixSZ/U5ovqZgg/qo3m5Rq/q0m2fpxbZcsHZo21s5MLa/AfJAx4hcFMY98D4RtLdcg==", "dependencies": { - "NodaTime": "3.1.9", - "Npgsql": "8.0.3" + "NodaTime": "3.2.0", + "Npgsql": "9.0.2" } }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.8.5", - "contentHash": "EaCgmntbH1sOzemRTqyXSqYjB6pLH7VCYHhhDYZ59guHSD5qPwhIYa7kfy0QUlmTRt9IXhaXdFhNuBUArp70Ng==" - }, - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7VSGO0URRKoMEAq0Sc9cRz8mb6zbyx/BZDEWhgPdzzpmFhkam3fJ1DAGWFXBI4nGlma+uPKpfuMQP5LXRnOH5g==" - }, - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "0oAaTAm6e2oVH+/Zttt0cuhGaePQYKII1dY8iaqP7CvOpVKgLybKRFvQjXR2LtxXOXTVPNv14j0ot8uV+HrUmw==" - }, - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "G24ibsCNi5Kbz0oXWynBoRgtGvsw5ZSVEWjv13/KiCAM8C6wz9zzcCniMeQFIkJ2tasjo2kXlvlBZhplL51kGg==" - }, - "runtime.native.System": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Net.Http": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Net.Security": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "M2nN92ePS8BgQ2oi6Jj3PlTUzadYSIWLdZrHY1n1ZcW9o4wAQQ6W+aQ2lfq1ysZQfVCgDwY58alUdowrzezztg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0" - } - }, - "runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", - "dependencies": { - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" - } - }, - "runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "QR1OwtwehHxSeQvZKXe+iSd+d3XZNkEcuWMFYa2i0aG1l+lR739HPicKMlTbJst3spmeekDVBUS7SeS26s4U/g==", - "dependencies": { - "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2", - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" - } - }, - "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "I+GNKGg2xCHueRd1m9PzeEW7WLbNNLznmTuEi8/vZX71HudUbx1UTwlGkiwMri7JLl8hGaIAWnA/GONhu+LOyQ==" - }, - "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "1Z3TAq1ytS1IBRtPXJvEUZdVsfWfeNEhBkbiOCGEl9wwAfsjP2lz3ZFDx5tq8p60/EqbS0HItG5piHuB71RjoA==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" - }, - "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "6mU/cVmmHtQiDXhnzUImxIcDL48GbTk+TsptXyJA+MIOG9LRjPoAQC/qBFB7X+UNyK86bmvGwC8t+M66wsYC8w==" - }, - "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "vjwG0GGcTW/PPg6KVud8F9GLWYuAV1rrw1BKAqY0oh4jcUqg15oYF1+qkGR2x2ZHM4DQnWKQ7cJgYbfncz/lYg==" - }, - "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "7KMFpTkHC/zoExs+PwP8jDCWcrK9H6L7soowT80CUx3e+nxP/AFnq0AQAW5W76z2WYbLAYCRyPfwYFG6zkvQRw==" - }, - "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "xrlmRCnKZJLHxyyLIqkZjNXqgxnKdZxfItrPkjI+6pkRo5lHX8YvSZlWrSI5AVwLMi4HbNWP7064hcAWeZKp5w==" - }, - "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "leXiwfiIkW7Gmn7cgnNcdtNAU70SjmKW3jxGj1iKHOvdn0zRWsgv/l2OJUO5zdGdiv2VRFnAsxxhDgMzofPdWg==" - }, "Sentry": { "type": "Transitive", - "resolved": "4.9.0", - "contentHash": "dvafAJjs1nyv/sXPjHjMiIq/XN1jjRCvTvn6BDdkeavjZXi0eX6JMpAKwGU5C8IlHY6Xhh13nk2u5e8AHQwwrw==" + "resolved": "4.13.0", + "contentHash": "Wfw3M1WpFcrYaGzPm7QyUTfIOYkVXQ1ry6p4WYjhbLz9fPwV23SGQZTFDpdox67NHM0V0g1aoQ4YKLm4ANtEEg==" }, "Sentry.Extensions.Logging": { "type": "Transitive", - "resolved": "4.9.0", - "contentHash": "P+9y+rxE5YPHw1sWWMUcSDF5tHfm+vP9yxJiALE85RXMkzT/H7tp7Pstclbnht38KiPsC3sFUtMBdYFdiMvHKg==", + "resolved": "4.13.0", + "contentHash": "yZ5+TtJKWcss6cG17YjnovImx4X56T8O6Qy6bsMC8tMDttYy8J7HJ2F+WdaZNyjOCo0Rfi6N2gc+Clv/5pf+TQ==", "dependencies": { "Microsoft.Extensions.Configuration.Binder": "8.0.0", "Microsoft.Extensions.Http": "8.0.0", "Microsoft.Extensions.Logging.Configuration": "8.0.0", - "Sentry": "4.9.0" + "Sentry": "4.13.0" } }, "Serilog.Extensions.Hosting": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "db0OcbWeSCvYQkHWu6n0v40N4kKaTAXNjlM3BKvcbwvNzYphQFcBR+36eQ/7hMMwOkJvAyLC2a9/jNdUL5NjtQ==", + "resolved": "9.0.0", + "contentHash": "u2TRxuxbjvTAldQn7uaAwePkWxTHIqlgjelekBtilAGL5sYyF3+65NWctN4UrwwGLsDC7c3Vz3HnOlu+PcoxXg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Serilog": "3.1.1", - "Serilog.Extensions.Logging": "8.0.0" + "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": "8.0.0", - "contentHash": "YEAMWu1UnWgf1c1KP85l1SgXGfiVo0Rz6x08pCiPOIBt2Qe18tcZLvdBUuV5o1QHvrs8FAry9wTIhgBRtjIlEg==", + "resolved": "9.0.0", + "contentHash": "NwSSYqPJeKNzl5AuXVHpGbr6PkZJFlNa14CdIebVjK3k/76kYj/mz5kiTRNVSsSaxM8kAIa1kpy/qyT9E4npRQ==", "dependencies": { - "Microsoft.Extensions.Logging": "8.0.0", - "Serilog": "3.1.1" + "Microsoft.Extensions.Logging": "9.0.0", + "Serilog": "4.2.0" } }, "Serilog.Formatting.Compact": { "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "ob6z3ikzFM3D1xalhFuBIK1IOWf+XrQq+H4KeH4VqBcPpNcmUgZlRQ2h3Q7wvthpdZBBoY86qZOI2LCXNaLlNA==", + "resolved": "3.0.0", + "contentHash": "wQsv14w9cqlfB5FX2MZpNsTawckN4a8dryuNGbebB/3Nh1pXnROHZov3swtu3Nj5oNG7Ba+xdu7Et/ulAUPanQ==", "dependencies": { - "Serilog": "3.1.0" + "Serilog": "4.0.0" } }, "Serilog.Settings.Configuration": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "nR0iL5HwKj5v6ULo3/zpP8NMcq9E2pxYA6XKTSWCbugVs4YqPyvaqaKOY+OMpPivKp7zMEpax2UKHnDodbRB0Q==", + "resolved": "9.0.0", + "contentHash": "4/Et4Cqwa+F88l5SeFeNZ4c4Z6dEAIKbu3MaQb2Zz9F/g27T5a3wvfMcmCOaAiACjfUb4A6wrlTVfyYUZk3RRQ==", "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyModel": "8.0.0", - "Serilog": "3.1.1" + "Microsoft.Extensions.Configuration.Binder": "9.0.0", + "Microsoft.Extensions.DependencyModel": "9.0.0", + "Serilog": "4.2.0" } }, "Serilog.Sinks.Debug": { "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "Y6g3OBJ4JzTyyw16fDqtFcQ41qQAydnEvEqmXjhwhgjsnG/FaJ8GUqF5ldsC/bVkK8KYmqrPhDO+tm4dF6xx4A==", + "resolved": "3.0.0", + "contentHash": "4BzXcdrgRX7wde9PmHuYd9U6YqycCC28hhpKonK7hx0wb19eiuRj16fPcPSVp0o/Y1ipJuNLYQ00R3q2Zs8FDA==", "dependencies": { - "Serilog": "2.10.0" + "Serilog": "4.0.0" } }, "Serilog.Sinks.File": { "type": "Transitive", - "resolved": "5.0.0", - "contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==", + "resolved": "6.0.0", + "contentHash": "lxjg89Y8gJMmFxVkbZ+qDgjl+T4yC5F7WSLTvA+5q0R04tfKVLRL/EHpYoJ/MEQd2EeCKDuylBIVnAYMotmh2A==", "dependencies": { - "Serilog": "2.10.0" + "Serilog": "4.0.0" } }, - "Swashbuckle.AspNetCore.Swagger": { - "type": "Transitive", - "resolved": "6.6.2", - "contentHash": "ovgPTSYX83UrQUWiS5vzDcJ8TEX1MAxBgDFMK45rC24MorHEPQlZAHlaXj/yth4Zf6xcktpUgTEBvffRQVwDKA==", - "dependencies": { - "Microsoft.OpenApi": "1.6.14" - } - }, - "Swashbuckle.AspNetCore.SwaggerGen": { - "type": "Transitive", - "resolved": "6.6.2", - "contentHash": "zv4ikn4AT1VYuOsDCpktLq4QDq08e7Utzbir86M5/ZkRaLXbCPF11E1/vTmOiDzRTl0zTZINQU2qLKwTcHgfrA==", - "dependencies": { - "Swashbuckle.AspNetCore.Swagger": "6.6.2" - } - }, - "Swashbuckle.AspNetCore.SwaggerUI": { - "type": "Transitive", - "resolved": "6.6.2", - "contentHash": "mBBb+/8Hm2Q3Wygag+hu2jj69tZW5psuv0vMRXY07Wy+Rrj40vRP8ZTbKBhs91r45/HXT4aY4z0iSBYx1h6JvA==" - }, "System.CodeDom": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "2sCCb7doXEwtYAbqzbF/8UAeDRMNmPaQbU2q50Psg1J9KzumyVVCgKQY8s53WIPTufNT0DpSe9QRvVjOzfDWBA==" - }, - "System.Collections": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Collections.Concurrent": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } + "resolved": "6.0.0", + "contentHash": "CPc6tWO1LAer3IzfZufDBRL+UZQcj5uS207NHALQzP84Vp/z6wF0Aa0YZImOQY8iStY0A2zI/e3ihKNPfUm8XA==" }, "System.Collections.Immutable": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "l4zZJ1WU2hqpQQHXz1rvC3etVZN+2DLmQMO79FhOTZHMn8tDRr+WU287sbomD0BETlmKDn0ygUgVy9k5xkkJdA==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==" }, "System.Composition": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "d7wMuKQtfsxUa7S13tITC8n1cQzewuhD5iDjZtK2prwFfKVzdYtgrTHgjaV03Zq7feGQ5gkP85tJJntXwInsJA==", + "resolved": "7.0.0", + "contentHash": "tRwgcAkDd85O8Aq6zHDANzQaq380cek9lbMg5Qma46u5BZXq/G+XvIYmu+UI+BIIZ9zssXLYrkTykEqxxvhcmg==", "dependencies": { - "System.Composition.AttributedModel": "6.0.0", - "System.Composition.Convention": "6.0.0", - "System.Composition.Hosting": "6.0.0", - "System.Composition.Runtime": "6.0.0", - "System.Composition.TypedParts": "6.0.0" + "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": "6.0.0", - "contentHash": "WK1nSDLByK/4VoC7fkNiFuTVEiperuCN/Hyn+VN30R+W2ijO1d0Z2Qm0ScEl9xkSn1G2MyapJi8xpf4R8WRa/w==" + "resolved": "7.0.0", + "contentHash": "2QzClqjElKxgI1jK1Jztnq44/8DmSuTSGGahXqQ4TdEV0h9s2KikQZIgcEqVzR7OuWDFPGLHIprBJGQEPr8fAQ==" }, "System.Composition.Convention": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "XYi4lPRdu5bM4JVJ3/UIHAiG6V6lWWUlkhB9ab4IOq0FrRsp0F4wTyV4Dj+Ds+efoXJ3qbLqlvaUozDO7OLeXA==", + "resolved": "7.0.0", + "contentHash": "IMhTlpCs4HmlD8B+J8/kWfwX7vrBBOs6xyjSTzBlYSs7W4OET4tlkR/Sg9NG8jkdJH9Mymq0qGdYS1VPqRTBnQ==", "dependencies": { - "System.Composition.AttributedModel": "6.0.0" + "System.Composition.AttributedModel": "7.0.0" } }, "System.Composition.Hosting": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "w/wXjj7kvxuHPLdzZ0PAUt++qJl03t7lENmb2Oev0n3zbxyNULbWBlnd5J5WUMMv15kg5o+/TCZFb6lSwfaUUQ==", + "resolved": "7.0.0", + "contentHash": "eB6gwN9S+54jCTBJ5bpwMOVerKeUfGGTYCzz3QgDr1P55Gg/Wb27ShfPIhLMjmZ3MoAKu8uUSv6fcCdYJTN7Bg==", "dependencies": { - "System.Composition.Runtime": "6.0.0" + "System.Composition.Runtime": "7.0.0" } }, "System.Composition.Runtime": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "qkRH/YBaMPTnzxrS5RDk1juvqed4A6HOD/CwRcDGyPpYps1J27waBddiiq1y93jk2ZZ9wuA/kynM+NO0kb3PKg==" + "resolved": "7.0.0", + "contentHash": "aZJ1Zr5Txe925rbo4742XifEyW0MIni1eiUebmcrP3HwLXZ3IbXUj4MFMUH/RmnJOAQiS401leg/2Sz1MkApDw==" }, "System.Composition.TypedParts": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "iUR1eHrL8Cwd82neQCJ00MpwNIBs4NZgXzrPqx8NJf/k4+mwBO0XCRmHYJT4OLSwDDqh5nBLJWkz5cROnrGhRA==", + "resolved": "7.0.0", + "contentHash": "ZK0KNPfbtxVceTwh+oHNGUOYV2WNOHReX2AXipuvkURC7s/jPwoWfsu3SnDBDgofqbiWr96geofdQ2erm/KTHg==", "dependencies": { - "System.Composition.AttributedModel": "6.0.0", - "System.Composition.Hosting": "6.0.0", - "System.Composition.Runtime": "6.0.0" + "System.Composition.AttributedModel": "7.0.0", + "System.Composition.Hosting": "7.0.0", + "System.Composition.Runtime": "7.0.0" } }, - "System.Data.Common": { + "System.Formats.Asn1": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "lm6E3T5u7BOuEH0u18JpbJHxBfOJPuCyl4Kg1RH10ktYLp5uEEE1xKrHW56/We4SnZpGAuCc9N0MJpSDhTHZGQ==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Text.RegularExpressions": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Diagnostics.Debug": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Diagnostics.DiagnosticSource": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "ddppcFpnbohLWdYKr/ZeLZHmmI+DXFgZ3Snq+/E7SwcdW4UnvxmaugkwGywvGVWkHPGCSZjCP+MLzu23AL5SDw==" - }, - "System.Diagnostics.Tracing": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Calendars": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Globalization.Extensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.InteropServices": "4.3.0" - } - }, - "System.IO": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.IO.FileSystem.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", - "dependencies": { - "System.Runtime": "4.3.0" - } + "resolved": "7.0.0", + "contentHash": "+nfpV0afLmvJW8+pLlHxRjz3oZJw4fkyU9MMEaMhCsHi/SN9bGF9q79ROubDiwTiCHezmK0uCWkPP7tGFP/4yg==" }, "System.IO.Hashing": { "type": "Transitive", @@ -1091,142 +818,20 @@ }, "System.IO.Pipelines": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "eA3cinogwaNB4jdjQHOP3Z3EuyiDII7MT35jgtnsA4vkn0LUrrSHsU0nzHTzFzmaFYeKV7MYyMxOocFzsBHpTw==" - }, - "System.Linq": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Net.NameResolution": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "AFYl08R7MrsrEjqpQWTZWBadqXyTzNDaWpMqyxhb0d6sGhV6xMDKueuBXlLL30gz+DIRY6MpdgnHWlCh5wmq9w==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Principal.Windows": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "runtime.native.System": "4.3.0" - } - }, - "System.Net.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Net.Security": { - "type": "Transitive", - "resolved": "4.3.2", - "contentHash": "xT2jbYpbBo3ha87rViHoTA6WdvqOAW37drmqyx/6LD8p7HEPT2qgdxoimRzWtPg8Jh4X5G9BV2seeTv4x6FYlA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Diagnostics.Tracing": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Extensions": "4.3.0", - "System.IO": "4.3.0", - "System.Net.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Claims": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Security.Cryptography.X509Certificates": "4.3.0", - "System.Security.Principal": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0", - "System.Threading.ThreadPool": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Security": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.2" - } + "resolved": "7.0.0", + "contentHash": "jRn6JYnNPW6xgQazROBLSfpdoczRw694vO5kKvMcNnpXuolEixUyw6IBuBs2Y2mlSX/LdLvyyWmfXhaI3ND1Yg==" }, "System.Reactive": { "type": "Transitive", "resolved": "6.0.1", "contentHash": "rHaWtKDwCi9qJ3ObKo8LHPMuuwv33YbmQi7TcUK1C264V3MFnOr5Im7QgCTdLniztP3GJyeiSg5x8NqYJFqRmg==" }, - "System.Reflection": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0" - } - }, "System.Reflection.Metadata": { "type": "Transitive", - "resolved": "6.0.1", - "contentHash": "III/lNMSn0ZRBuM9m5Cgbiho5j81u0FAEagFX5ta2DKbljZ3T0IpD8j+BIiHQPeKqJppWS9bGEp6JnKnWKze0g==", + "resolved": "7.0.0", + "contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==", "dependencies": { - "System.Collections.Immutable": "6.0.0" - } - }, - "System.Reflection.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Reflection.TypeExtensions": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", - "dependencies": { - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Resources.ResourceManager": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Globalization": "4.3.0", - "System.Reflection": "4.3.0", - "System.Runtime": "4.3.0" + "System.Collections.Immutable": "7.0.0" } }, "System.Runtime": { @@ -1243,301 +848,23 @@ "resolved": "6.0.0", "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" }, - "System.Runtime.Extensions": { + "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "resolved": "7.0.3", + "contentHash": "yhwEHH5Gzl/VoADrXtt5XC95OFoSjNSWLHNutE7GwdOgefZVRvEXRSooSpL8HHm3qmdd9epqzsWg28UJemt22w==", "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.Handles": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Runtime.InteropServices": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Reflection": "4.3.0", - "System.Reflection.Primitives": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } - }, - "System.Runtime.Numerics": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", - "dependencies": { - "System.Globalization": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0" - } - }, - "System.Runtime.Serialization.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==", - "dependencies": { - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0" - } - }, - "System.Security.Claims": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "P/+BR/2lnc4PNDHt/TPBAWHVMLMRHsyYZbU1NphW4HIWzCggz8mJbTQQ3MKljFE7LS3WagmVFuBgoLcFzYXlkA==", - "dependencies": { - "System.Collections": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Security.Principal": "4.3.0" - } - }, - "System.Security.Cryptography.Algorithms": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.Apple": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Cng": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "03idZOqFlsKRL4W+LuCpJ6dBYDUWReug6lZjBa3uJWnk5sPCUXckocevTaUA8iT/MFSrY/2HXkOt753xQ/cf8g==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0" - } - }, - "System.Security.Cryptography.Csp": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.IO": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Security.Cryptography.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Collections.Concurrent": "4.3.0", - "System.Linq": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.OpenSsl": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", - "dependencies": { - "System.Collections": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Cryptography.Primitives": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", - "dependencies": { - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.IO": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Threading": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } - }, - "System.Security.Cryptography.X509Certificates": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Globalization": "4.3.0", - "System.Globalization.Calendars": "4.3.0", - "System.IO": "4.3.0", - "System.IO.FileSystem": "4.3.0", - "System.IO.FileSystem.Primitives": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Runtime.Numerics": "4.3.0", - "System.Security.Cryptography.Algorithms": "4.3.0", - "System.Security.Cryptography.Cng": "4.3.0", - "System.Security.Cryptography.Csp": "4.3.0", - "System.Security.Cryptography.Encoding": "4.3.0", - "System.Security.Cryptography.OpenSsl": "4.3.0", - "System.Security.Cryptography.Primitives": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0", - "runtime.native.System": "4.3.0", - "runtime.native.System.Net.Http": "4.3.0", - "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" - } - }, - "System.Security.Principal": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "I1tkfQlAoMM2URscUtpcRo/hX0jinXx6a/KUtEQoz3owaYwl3qwsO8cbzYVVnjxrzxjHo3nJC+62uolgeGIS9A==", - "dependencies": { - "System.Runtime": "4.3.0" - } - }, - "System.Security.Principal.Windows": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "HVL1rvqYtnRCxFsYag/2le/ZfKLK4yMw79+s6FmKXbSCNN0JeAhrYxnRAHFoWRa0dEojsDcbBSpH3l22QxAVyw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.Win32.Primitives": "4.3.0", - "System.Collections": "4.3.0", - "System.Diagnostics.Debug": "4.3.0", - "System.Reflection": "4.3.0", - "System.Resources.ResourceManager": "4.3.0", - "System.Runtime": "4.3.0", - "System.Runtime.Extensions": "4.3.0", - "System.Runtime.Handles": "4.3.0", - "System.Runtime.InteropServices": "4.3.0", - "System.Security.Claims": "4.3.0", - "System.Security.Principal": "4.3.0", - "System.Text.Encoding": "4.3.0", - "System.Threading": "4.3.0" - } - }, - "System.Text.Encoding": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" + "System.Formats.Asn1": "7.0.0" } }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } - }, - "System.Text.Encodings.Web": { - "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "e2hMgAErLbKyUUwt18qSBf9T5Y+SFAL3ZedM8fLupkVj8Rj2PZ9oxQ37XX2LF8fTO1wNIxvKpihD7Of7D/NxZw==" - }, - "System.Threading": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Threading.Tasks": "4.3.0" - } + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==" }, "System.Threading.Channels": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "TY8/9+tI0mNaUMgntOxxaq2ndTkdXqLSxvPmas7XEqOlv9lQtB7wLjYGd756lOaO7Dvb5r/WXhluM+0Xe87v5Q==" - }, - "System.Threading.Tasks": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0", - "Microsoft.NETCore.Targets": "1.1.0", - "System.Runtime": "4.3.0" - } - }, - "System.Threading.ThreadPool": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "k/+g4b7vjdd4aix83sTgC9VG6oXYKAktSfNIJUNGxPEj7ryEOfzHHhfnmsZvjxawwcD9HyWXKCXmPjX8U4zeSw==", - "dependencies": { - "System.Runtime": "4.3.0", - "System.Runtime.Handles": "4.3.0" - } + "resolved": "7.0.0", + "contentHash": "qmeeYNROMsONF6ndEZcIQ+VxR4Q/TX/7uIVLJqtwIWL7dDWeh0l1UIqgo4wYyjG//5lUNhwkLDSFl+pAWO6oiA==" } } } diff --git a/migrators/NetImporter/NetImporter.csproj b/migrators/NetImporter/NetImporter.csproj index 08a1aa3..d27f214 100644 --- a/migrators/NetImporter/NetImporter.csproj +++ b/migrators/NetImporter/NetImporter.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net9.0 enable enable @@ -14,13 +14,13 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + From a29d1fdb78789efc060148891cbada96a999711c Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Dec 2024 01:44:00 +0100 Subject: [PATCH 164/261] feat: plain text emails --- Foxnouns.Backend/Foxnouns.Backend.csproj | 1 + .../Mailables/AccountCreationMailable.cs | 14 ++++++- .../Mailables/AddEmailMailable.cs | 14 ++++++- Foxnouns.Backend/packages.lock.json | 42 ++++++++----------- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index c6f9605..4ed5c44 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -22,6 +22,7 @@ all + diff --git a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs index ee86b48..9c33213 100644 --- a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs +++ b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs @@ -19,9 +19,21 @@ 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!).View("~/Views/Mail/AccountCreation.cshtml", view); + To(view.To) + .From(config.EmailAuth.From!) + .View("~/Views/Mail/AccountCreation.cshtml", view) + .Text(PlainText()); } } diff --git a/Foxnouns.Backend/Mailables/AddEmailMailable.cs b/Foxnouns.Backend/Mailables/AddEmailMailable.cs index f40ffc0..1d29d0f 100644 --- a/Foxnouns.Backend/Mailables/AddEmailMailable.cs +++ b/Foxnouns.Backend/Mailables/AddEmailMailable.cs @@ -19,9 +19,21 @@ 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!).View("~/Views/Mail/AddEmail.cshtml", view); + To(view.To) + .From(config.EmailAuth.From!) + .View("~/Views/Mail/AddEmail.cshtml", view) + .Text(PlainText()); } } diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index 03170c1..801e650 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -124,6 +124,17 @@ "Microsoft.Extensions.Primitives": "9.0.0" } }, + "MimeKit": { + "type": "Direct", + "requested": "[4.9.0, )", + "resolved": "4.9.0", + "contentHash": "DZXXMZzmAABDxFhOSMb6SE8KKxcRd/sk1E6aJTUE5ys2FWOQhznYV2Gl3klaaSfqKn27hQ32haqquH1J8Z6kJw==", + "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.3, )", @@ -284,8 +295,8 @@ }, "BouncyCastle.Cryptography": { "type": "Transitive", - "resolved": "2.2.1", - "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" + "resolved": "2.5.0", + "contentHash": "rc7vRCq/KD3GtIwSgRtjanGaBwTb9nLenFDZnEcauWlssuuEoxcbMfWA3QWWho6QDMSOSkWjs657McdHzEtEcw==" }, "CommunityToolkit.HighPerformance": { "type": "Transitive", @@ -633,17 +644,6 @@ "resolved": "1.6.17", "contentHash": "Le+kehlmrlQfuDFUt1zZ2dVwrhFQtKREdKBo+rexOwaCoYP0/qpgT9tLxCsZjsgR5Itk1UKPcbgO+FyaNid/bA==" }, - "MimeKit": { - "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "39KDXuERDy5VmHIn7NnCWvIVp/Ar4qnxZWg9m06DfRqDbW1B6zFv9o3Tdoa4CCu71tE/0SRqRCN5Z+bbffw6uw==", - "dependencies": { - "BouncyCastle.Cryptography": "2.2.1", - "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Security.Cryptography.Pkcs": "7.0.3", - "System.Text.Encoding.CodePages": "7.0.0" - } - }, "Mono.TextTemplating": { "type": "Transitive", "resolved": "3.0.0", @@ -808,8 +808,8 @@ }, "System.Formats.Asn1": { "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "+nfpV0afLmvJW8+pLlHxRjz3oZJw4fkyU9MMEaMhCsHi/SN9bGF9q79ROubDiwTiCHezmK0uCWkPP7tGFP/4yg==" + "resolved": "8.0.1", + "contentHash": "XqKba7Mm/koKSjKMfW82olQdmfbI5yqeoLV/tidRp7fbh5rmHAQ5raDI/7SU0swTzv+jgqtUGkzmFxuUg0it1A==" }, "System.IO.Hashing": { "type": "Transitive", @@ -850,16 +850,8 @@ }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "7.0.3", - "contentHash": "yhwEHH5Gzl/VoADrXtt5XC95OFoSjNSWLHNutE7GwdOgefZVRvEXRSooSpL8HHm3qmdd9epqzsWg28UJemt22w==", - "dependencies": { - "System.Formats.Asn1": "7.0.0" - } - }, - "System.Text.Encoding.CodePages": { - "type": "Transitive", - "resolved": "7.0.0", - "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==" + "resolved": "8.0.1", + "contentHash": "CoCRHFym33aUSf/NtWSVSZa99dkd0Hm7OCZUxORBjRB16LNhIEOf8THPqzIYlvKM0nNDAPTRBa1FxEECrgaxxA==" }, "System.Threading.Channels": { "type": "Transitive", From a9ccc12671a09bfa2d9492145932804bce55278f Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Dec 2024 01:44:12 +0100 Subject: [PATCH 165/261] add favicon --- Foxnouns.Frontend/static/favicon.svg | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Foxnouns.Frontend/static/favicon.svg diff --git a/Foxnouns.Frontend/static/favicon.svg b/Foxnouns.Frontend/static/favicon.svg new file mode 100644 index 0000000..11e664f --- /dev/null +++ b/Foxnouns.Frontend/static/favicon.svg @@ -0,0 +1,2 @@ + +image/svg+xml \ No newline at end of file From 7f8e72e857f67d6dbc3b607cffc4dcdc432eb28a Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Dec 2024 01:48:07 +0100 Subject: [PATCH 166/261] fix backend dockerfile, Caddyfile, and email controller --- Dockerfile.backend | 4 ++-- .../Controllers/Authentication/EmailAuthController.cs | 2 +- docker/Caddyfile | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Dockerfile.backend b/Dockerfile.backend index b7dd859..69f90b0 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -1,9 +1,9 @@ -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base USER $APP_UID WORKDIR /app EXPOSE 5000 -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src COPY ["Foxnouns.Backend/Foxnouns.Backend.csproj", "Foxnouns.Backend/"] diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index cedf962..a3854a6 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -73,7 +73,7 @@ public class EmailAuthController( } [HttpPost("callback")] - public async Task CallbackAsync([FromBody] CallbackRequest req) + public async Task CallbackAsync([FromBody] EmailCallbackRequest req) { CheckRequirements(); diff --git a/docker/Caddyfile b/docker/Caddyfile index 9132068..a729fa8 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -1,11 +1,11 @@ # Frontend and API -http://localhost:80 { +http://:80 { reverse_proxy /api/* http://rate:5003 reverse_proxy http://frontend:3000 } # prns.cc (profile URL shortener) -http://localhost:81 { +http://:81 { rewrite * /sid{uri} reverse_proxy http://backend:5000 } From 5cb3faa92ba7d67690392cd783762baab4c51911 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Dec 2024 16:54:06 +0100 Subject: [PATCH 167/261] feat(backend): allow suspended users to access some endpoints, add flag scopes --- .../Controllers/FlagsController.cs | 9 ++-- .../Controllers/MembersController.cs | 2 + .../Controllers/UsersController.cs | 1 + .../Database/DatabaseQueryExtensions.cs | 14 +++--- .../Middleware/AuthorizationMiddleware.cs | 45 +++++++++++++------ Foxnouns.Backend/Program.cs | 8 ++-- Foxnouns.Backend/Utils/AuthUtils.cs | 3 ++ 7 files changed, 57 insertions(+), 25 deletions(-) diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index 0c35afd..c68fb96 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -34,7 +34,8 @@ public class FlagsController( ) : ApiControllerBase { [HttpGet] - [Authorize("identify")] + [Limit(UsableBySuspendedUsers = true)] + [Authorize("user.read_flags")] [ProducesResponseType>(statusCode: StatusCodes.Status200OK)] public async Task GetFlagsAsync(CancellationToken ct = default) { @@ -50,7 +51,7 @@ public class FlagsController( public const int MaxFlagCount = 500; [HttpPost] - [Authorize("user.update")] + [Authorize("user.update_flags")] [ProducesResponseType(statusCode: StatusCodes.Status202Accepted)] public async Task CreateFlagAsync([FromBody] CreateFlagRequest req) { @@ -79,7 +80,7 @@ public class FlagsController( } [HttpPatch("{id}")] - [Authorize("user.update")] + [Authorize("user.create_flags")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] public async Task UpdateFlagAsync(Snowflake id, [FromBody] UpdateFlagRequest req) { @@ -104,7 +105,7 @@ public class FlagsController( } [HttpDelete("{id}")] - [Authorize("user.update")] + [Authorize("user.update_flags")] public async Task DeleteFlagAsync(Snowflake id) { PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 534c51f..9b94b30 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -44,6 +44,7 @@ public class MembersController( [HttpGet] [ProducesResponseType>(StatusCodes.Status200OK)] + [Limit(UsableBySuspendedUsers = true)] public async Task GetMembersAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); @@ -52,6 +53,7 @@ public class MembersController( [HttpGet("{memberRef}")] [ProducesResponseType(StatusCodes.Status200OK)] + [Limit(UsableBySuspendedUsers = true)] public async Task GetMemberAsync( string userRef, string memberRef, diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 4a3be72..d567bdb 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -42,6 +42,7 @@ public class UsersController( [HttpGet("{userRef}")] [ProducesResponseType(statusCode: StatusCodes.Status200OK)] + [Limit(UsableBySuspendedUsers = true)] public async Task GetUserAsync(string userRef, CancellationToken ct = default) { User user = await db.ResolveUserAsync(userRef, CurrentToken, ct); diff --git a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs index 790b9df..d804dfe 100644 --- a/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs +++ b/Foxnouns.Backend/Database/DatabaseQueryExtensions.cs @@ -31,6 +31,7 @@ public static class DatabaseQueryExtensions { 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( @@ -43,14 +44,14 @@ public static class DatabaseQueryExtensions if (Snowflake.TryParse(userRef, out Snowflake? snowflake)) { user = await context - .Users.Where(u => !u.Deleted) + .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.Where(u => !u.Deleted) + .Users.Where(u => !u.Deleted || (token != null && token.UserId == u.Id)) .FirstOrDefaultAsync(u => u.Username == userRef, ct); if (user != null) return user; @@ -98,13 +99,14 @@ public static class DatabaseQueryExtensions ) { User user = await context.ResolveUserAsync(userRef, token, ct); - return await context.ResolveMemberAsync(user.Id, memberRef, ct); + return await context.ResolveMemberAsync(user.Id, memberRef, token, ct); } public static async Task ResolveMemberAsync( this DatabaseContext context, Snowflake userId, string memberRef, + Token? token = null, CancellationToken ct = default ) { @@ -114,7 +116,8 @@ public static class DatabaseQueryExtensions member = await context .Members.Include(m => m.User) .Include(m => m.ProfileFlags) - .Where(m => !m.User.Deleted) + // 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; @@ -123,7 +126,8 @@ public static class DatabaseQueryExtensions member = await context .Members.Include(m => m.User) .Include(m => m.ProfileFlags) - .Where(m => !m.User.Deleted) + // 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; diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 1132dc1..908598a 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -14,6 +14,7 @@ // along with this program. If not, see . using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; +using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Foxnouns.Backend.Middleware; @@ -22,9 +23,11 @@ public class AuthorizationMiddleware : IMiddleware public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { Endpoint? endpoint = ctx.GetEndpoint(); - AuthorizeAttribute? attribute = endpoint?.Metadata.GetMetadata(); + AuthorizeAttribute? authorizeAttribute = + endpoint?.Metadata.GetMetadata(); + LimitAttribute? limitAttribute = endpoint?.Metadata.GetMetadata(); - if (attribute == null) + if (authorizeAttribute == null || authorizeAttribute.Scopes.Length == 0) { await next(ctx); return; @@ -39,24 +42,35 @@ public class AuthorizationMiddleware : IMiddleware ); } + // Users who got suspended by a moderator can still access *some* endpoints. if ( - attribute.Scopes.Length > 0 - && attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any() + token.User.Deleted + && (limitAttribute?.UsableBySuspendedUsers != true || token.User.DeletedBy == null) + ) + { + throw new ApiError.Forbidden("Deleted users cannot access this endpoint."); + } + + if ( + authorizeAttribute.Scopes.Length > 0 + && authorizeAttribute.Scopes.Except(token.Scopes.ExpandScopes()).Any() ) { throw new ApiError.Forbidden( "This endpoint requires ungranted scopes.", - attribute.Scopes.Except(token.Scopes.ExpandScopes()), + authorizeAttribute.Scopes.Except(token.Scopes.ExpandScopes()), ErrorCode.MissingScopes ); } - if (attribute.RequireAdmin && token.User.Role != UserRole.Admin) + if (limitAttribute?.RequireAdmin == true && token.User.Role != UserRole.Admin) + { throw new ApiError.Forbidden("This endpoint can only be used by admins."); + } + if ( - attribute.RequireModerator - && token.User.Role != UserRole.Admin - && token.User.Role != UserRole.Moderator + limitAttribute?.RequireModerator == true + && token.User.Role is not (UserRole.Admin or UserRole.Moderator) ) { throw new ApiError.Forbidden("This endpoint can only be used by moderators."); @@ -69,8 +83,13 @@ public class AuthorizationMiddleware : IMiddleware [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AuthorizeAttribute(params string[] scopes) : Attribute { - public readonly bool RequireAdmin = scopes.Contains(":admin"); - public readonly bool RequireModerator = scopes.Contains(":moderator"); - - public readonly string[] Scopes = scopes.Except([":admin", ":moderator"]).ToArray(); + public readonly string[] Scopes = scopes.Except([":admin", ":moderator", ":deleted"]).ToArray(); +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class LimitAttribute : Attribute +{ + public bool UsableBySuspendedUsers { 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 3597ae7..66e57a6 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -66,9 +66,11 @@ builder }) .ConfigureApiBehaviorOptions(options => { - options.InvalidModelStateResponseFactory = actionContext => new BadRequestObjectResult( - new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() - ); + // the type isn't needed but without it, rider keeps complaining for no reason (it compiles just fine) + options.InvalidModelStateResponseFactory = (ActionContext actionContext) => + new BadRequestObjectResult( + new ApiError.AspBadRequest("Bad request", actionContext.ModelState).ToJson() + ); }); builder.Services.AddOpenApi( diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index bcc2700..491694a 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -35,6 +35,9 @@ public static class AuthUtils "user.read_hidden", "user.read_privileged", "user.update", + "user.read_flags", + "user.create_flags", + "user.update_flags", ]; public static readonly string[] MemberScopes = From ff8d53814deb56d4c227ab8f8653f20db41da5fe Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Dec 2024 20:42:48 +0100 Subject: [PATCH 168/261] feat: rate limit emails to two per address per hour --- Foxnouns.Backend/Database/DatabaseContext.cs | 6 +- .../20241211193653_AddSentEmailCache.cs | 53 +++++++++ .../DatabaseContextModelSnapshot.cs | 40 +++++-- Foxnouns.Backend/Database/Models/SentEmail.cs | 13 ++ .../Services/DataCleanupService.cs | 15 ++- Foxnouns.Backend/Services/MailService.cs | 111 ++++++++++++------ 6 files changed, 189 insertions(+), 49 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs create mode 100644 Foxnouns.Backend/Database/Models/SentEmail.cs diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index a5dede8..7407c5b 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -66,6 +66,7 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) public DbSet Applications { get; init; } = null!; public DbSet TemporaryKeys { get; init; } = null!; public DbSet DataExports { get; init; } = null!; + public DbSet SentEmails { get; init; } = null!; public DbSet PrideFlags { get; init; } = null!; public DbSet UserFlags { get; init; } = null!; @@ -84,6 +85,10 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) modelBuilder.Entity().HasIndex(m => new { m.UserId, m.Name }).IsUnique(); modelBuilder.Entity().HasIndex(m => m.Sid).IsUnique(); modelBuilder.Entity().HasIndex(k => k.Key).IsUnique(); + modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); + modelBuilder.Entity().HasIndex(e => new { e.Email, e.SentAt }); + + // Two indexes on auth_methods, one for fediverse auth and one for all other types. modelBuilder .Entity() .HasIndex(m => new @@ -94,7 +99,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) }) .HasFilter("fediverse_application_id IS NOT NULL") .IsUnique(); - modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); modelBuilder .Entity() diff --git a/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs b/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs new file mode 100644 index 0000000..e0fe00d --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20241211193653_AddSentEmailCache.cs @@ -0,0 +1,53 @@ +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 4bd1ede..a9f59e6 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -1,5 +1,4 @@ // -using System; using System.Collections.Generic; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -20,7 +19,7 @@ namespace Foxnouns.Backend.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -46,12 +45,12 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("text") .HasColumnName("name"); - b.Property("RedirectUris") + b.PrimitiveCollection("RedirectUris") .IsRequired() .HasColumnType("text[]") .HasColumnName("redirect_uris"); - b.Property("Scopes") + b.PrimitiveCollection("Scopes") .IsRequired() .HasColumnType("text[]") .HasColumnName("scopes"); @@ -193,7 +192,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("jsonb") .HasColumnName("fields"); - b.Property("Links") + b.PrimitiveCollection("Links") .IsRequired() .HasColumnType("text[]") .HasColumnName("links"); @@ -303,6 +302,33 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("pride_flags", (string)null); }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.SentEmail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Email") + .IsRequired() + .HasColumnType("text") + .HasColumnName("email"); + + b.Property("SentAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("sent_at"); + + b.HasKey("Id") + .HasName("pk_sent_emails"); + + b.HasIndex("Email", "SentAt") + .HasDatabaseName("ix_sent_emails_email_sent_at"); + + b.ToTable("sent_emails", (string)null); + }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b => { b.Property("Id") @@ -359,7 +385,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("boolean") .HasColumnName("manually_expired"); - b.Property("Scopes") + b.PrimitiveCollection("Scopes") .IsRequired() .HasColumnType("text[]") .HasColumnName("scopes"); @@ -428,7 +454,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasColumnType("timestamp with time zone") .HasColumnName("last_sid_reroll"); - b.Property("Links") + b.PrimitiveCollection("Links") .IsRequired() .HasColumnType("text[]") .HasColumnName("links"); diff --git a/Foxnouns.Backend/Database/Models/SentEmail.cs b/Foxnouns.Backend/Database/Models/SentEmail.cs new file mode 100644 index 0000000..09f03d9 --- /dev/null +++ b/Foxnouns.Backend/Database/Models/SentEmail.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations.Schema; +using NodaTime; + +namespace Foxnouns.Backend.Database.Models; + +public class SentEmail +{ + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; init; } + + public required string Email { get; init; } + public required Instant SentAt { get; init; } +} diff --git a/Foxnouns.Backend/Services/DataCleanupService.cs b/Foxnouns.Backend/Services/DataCleanupService.cs index 2b42f86..5e40c08 100644 --- a/Foxnouns.Backend/Services/DataCleanupService.cs +++ b/Foxnouns.Backend/Services/DataCleanupService.cs @@ -33,13 +33,24 @@ public class DataCleanupService( public async Task InvokeAsync(CancellationToken ct = default) { - _logger.Information("Cleaning up expired users"); + _logger.Debug("Cleaning up sent email cache"); + await CleanEmailsAsync(ct); + + _logger.Debug("Cleaning up expired users"); await CleanUsersAsync(ct); - _logger.Information("Cleaning up expired data exports"); + _logger.Debug("Cleaning up expired data exports"); await CleanExportsAsync(ct); } + private async Task CleanEmailsAsync(CancellationToken ct = default) + { + Instant expiry = clock.GetCurrentInstant() - Duration.FromHours(2); + int count = await db.SentEmails.Where(e => e.SentAt < expiry).ExecuteDeleteAsync(ct); + if (count != 0) + _logger.Information("Deleted {Count} entries from the sent email cache", expiry); + } + private async Task CleanUsersAsync(CancellationToken ct = default) { Instant selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter; diff --git a/Foxnouns.Backend/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs index 9ae61bd..e162b33 100644 --- a/Foxnouns.Backend/Services/MailService.cs +++ b/Foxnouns.Backend/Services/MailService.cs @@ -12,13 +12,25 @@ // // 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.Database; +using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Mailables; +using Microsoft.EntityFrameworkCore; +using NodaTime; namespace Foxnouns.Backend.Services; -public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config config) +public class MailService( + ILogger logger, + IMailer mailer, + IQueue queue, + IClock clock, + Config config, + IServiceProvider serviceProvider +) { private readonly ILogger _logger = logger.ForContext(); @@ -26,25 +38,18 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co { queue.QueueAsyncTask(async () => { - _logger.Debug("Sending account creation email to {ToEmail}", to); - try - { - await mailer.SendAsync( - new AccountCreationMailable( - config, - new AccountCreationMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - } - ) - ); - } - catch (Exception exc) - { - _logger.Error(exc, "Sending account creation email"); - } + await SendEmailAsync( + to, + new AccountCreationMailable( + config, + new AccountCreationMailableView + { + BaseUrl = config.BaseUrl, + To = to, + Code = code, + } + ) + ); }); } @@ -53,25 +58,53 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co _logger.Debug("Sending add email address email to {ToEmail}", to); queue.QueueAsyncTask(async () => { - try - { - await mailer.SendAsync( - new AddEmailMailable( - config, - new AddEmailMailableView - { - BaseUrl = config.BaseUrl, - To = to, - Code = code, - Username = username, - } - ) - ); - } - catch (Exception exc) - { - _logger.Error(exc, "Sending add email address email"); - } + await SendEmailAsync( + to, + new AddEmailMailable( + config, + new AddEmailMailableView + { + BaseUrl = config.BaseUrl, + To = to, + Code = code, + Username = username, + } + ) + ); }); } + + private async Task SendEmailAsync(string to, Mailable mailable) + { + try + { + // ReSharper disable SuggestVarOrType_SimpleTypes + await using var scope = serviceProvider.CreateAsyncScope(); + await using var db = scope.ServiceProvider.GetRequiredService(); + // ReSharper restore SuggestVarOrType_SimpleTypes + + Instant now = clock.GetCurrentInstant(); + + int count = await db.SentEmails.CountAsync(e => + e.Email == to && e.SentAt > (now - Duration.FromHours(1)) + ); + if (count >= 2) + { + _logger.Information( + "Have already sent 2 or more emails to {ToAddress} in the past hour, not sending new email", + to + ); + return; + } + + await mailer.SendAsync(mailable); + + db.SentEmails.Add(new SentEmail { Email = to, SentAt = now }); + await db.SaveChangesAsync(); + } + catch (Exception exc) + { + _logger.Error(exc, "Sending email"); + } + } } From 1ce4f9d278edc128655321192231147de8b47af7 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 11 Dec 2024 20:43:55 +0100 Subject: [PATCH 169/261] fix: favicon --- Foxnouns.Frontend/src/app.html | 2 +- Foxnouns.Frontend/static/favicon.png | Bin 1571 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 Foxnouns.Frontend/static/favicon.png diff --git a/Foxnouns.Frontend/src/app.html b/Foxnouns.Frontend/src/app.html index 77a5ff5..1391f88 100644 --- a/Foxnouns.Frontend/src/app.html +++ b/Foxnouns.Frontend/src/app.html @@ -2,7 +2,7 @@ - + %sveltekit.head% diff --git a/Foxnouns.Frontend/static/favicon.png b/Foxnouns.Frontend/static/favicon.png deleted file mode 100644 index 825b9e65af7c104cfb07089bb28659393b4f2097..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1571 zcmV+;2Hg3HP)Px)-AP12RCwC$UE6KzI1p6{F2N z1VK2vi|pOpn{~#djwYcWXTI_im_u^TJgMZ4JMOsSj!0ma>B?-(Hr@X&W@|R-$}W@Z zgj#$x=!~7LGqHW?IO8+*oE1MyDp!G=L0#^lUx?;!fXv@l^6SvTnf^ac{5OurzC#ZMYc20lI%HhX816AYVs1T3heS1*WaWH z%;x>)-J}YB5#CLzU@GBR6sXYrD>Vw(Fmt#|JP;+}<#6b63Ike{Fuo!?M{yEffez;| zp!PfsuaC)>h>-AdbnwN13g*1LowNjT5?+lFVd#9$!8Z9HA|$*6dQ8EHLu}U|obW6f z2%uGv?vr=KNq7YYa2Roj;|zooo<)lf=&2yxM@e`kM$CmCR#x>gI>I|*Ubr({5Y^rb zghxQU22N}F51}^yfDSt786oMTc!W&V;d?76)9KXX1 z+6Okem(d}YXmmOiZq$!IPk5t8nnS{%?+vDFz3BevmFNgpIod~R{>@#@5x9zJKEHLHv!gHeK~n)Ld!M8DB|Kfe%~123&Hz1Z(86nU7*G5chmyDe ziV7$pB7pJ=96hpxHv9rCR29%bLOXlKU<_13_M8x)6;P8E1Kz6G<&P?$P^%c!M5`2` zfY2zg;VK5~^>TJGQzc+33-n~gKt{{of8GzUkWmU110IgI0DLxRIM>0US|TsM=L|@F z0Bun8U!cRB7-2apz=y-7*UxOxz@Z0)@QM)9wSGki1AZ38ceG7Q72z5`i;i=J`ILzL z@iUO?SBBG-0cQuo+an4TsLy-g-x;8P4UVwk|D8{W@U1Zi z!M)+jqy@nQ$p?5tsHp-6J304Q={v-B>66$P0IDx&YT(`IcZ~bZfmn11#rXd7<5s}y zBi9eim&zQc0Dk|2>$bs0PnLmDfMP5lcXRY&cvJ=zKxI^f0%-d$tD!`LBf9^jMSYUA zI8U?CWdY@}cRq6{5~y+)#h1!*-HcGW@+gZ4B};0OnC~`xQOyH19z*TA!!BJ%9s0V3F?CAJ{hTd#*tf+ur-W9MOURF-@B77_-OshsY}6 zOXRY=5%C^*26z?l)1=$bz30!so5tfABdSYzO+H=CpV~aaUefmjvfZ3Ttu9W&W3Iu6 zROlh0MFA5h;my}8lB0tAV-Rvc2Zs_CCSJnx@d`**$idgy-iMob4dJWWw|21b4NB=LfsYp0Aeh{Ov)yztQi;eL4y5 zMi>8^SzKqk8~k?UiQK^^-5d8c%bV?$F8%X~czyiaKCI2=UH Date: Wed, 11 Dec 2024 21:17:46 +0100 Subject: [PATCH 170/261] feat: use a FixedWindowRateLimiter keyed by IP to rate limit emails we don't talk about the sent_emails table :) --- .../Authentication/EmailAuthController.cs | 37 +++++++++++++++++++ Foxnouns.Backend/Database/DatabaseContext.cs | 2 - .../DatabaseContextModelSnapshot.cs | 27 -------------- Foxnouns.Backend/Database/Models/SentEmail.cs | 13 ------- .../Extensions/WebApplicationExtensions.cs | 1 + .../Services/DataCleanupService.cs | 11 ------ Foxnouns.Backend/Services/EmailRateLimiter.cs | 36 ++++++++++++++++++ Foxnouns.Backend/Services/MailService.cs | 35 +----------------- 8 files changed, 75 insertions(+), 87 deletions(-) delete mode 100644 Foxnouns.Backend/Database/Models/SentEmail.cs create mode 100644 Foxnouns.Backend/Services/EmailRateLimiter.cs diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index a3854a6..1587f87 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -36,6 +36,7 @@ public class EmailAuthController( DatabaseContext db, AuthService authService, MailService mailService, + EmailRateLimiter rateLimiter, KeyCacheService keyCacheService, UserRendererService userRenderer, IClock clock, @@ -68,6 +69,9 @@ public class EmailAuthController( return NoContent(); } + if (IsRateLimited()) + return NoContent(); + mailService.QueueAccountCreationEmail(req.Email, state); return NoContent(); } @@ -221,6 +225,9 @@ public class EmailAuthController( return NoContent(); } + if (IsRateLimited()) + return NoContent(); + mailService.QueueAddEmailAddressEmail(req.Email, state, CurrentUser.Username); return NoContent(); } @@ -274,4 +281,34 @@ public class EmailAuthController( 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/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index 7407c5b..ddf7853 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -66,7 +66,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) public DbSet Applications { get; init; } = null!; public DbSet TemporaryKeys { get; init; } = null!; public DbSet DataExports { get; init; } = null!; - public DbSet SentEmails { get; init; } = null!; public DbSet PrideFlags { get; init; } = null!; public DbSet UserFlags { get; init; } = null!; @@ -86,7 +85,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) modelBuilder.Entity().HasIndex(m => m.Sid).IsUnique(); modelBuilder.Entity().HasIndex(k => k.Key).IsUnique(); modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); - modelBuilder.Entity().HasIndex(e => new { e.Email, e.SentAt }); // Two indexes on auth_methods, one for fediverse auth and one for all other types. modelBuilder diff --git a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index a9f59e6..cfe2513 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -302,33 +302,6 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("pride_flags", (string)null); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.SentEmail", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Email") - .IsRequired() - .HasColumnType("text") - .HasColumnName("email"); - - b.Property("SentAt") - .HasColumnType("timestamp with time zone") - .HasColumnName("sent_at"); - - b.HasKey("Id") - .HasName("pk_sent_emails"); - - b.HasIndex("Email", "SentAt") - .HasDatabaseName("ix_sent_emails_email_sent_at"); - - b.ToTable("sent_emails", (string)null); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b => { b.Property("Id") diff --git a/Foxnouns.Backend/Database/Models/SentEmail.cs b/Foxnouns.Backend/Database/Models/SentEmail.cs deleted file mode 100644 index 09f03d9..0000000 --- a/Foxnouns.Backend/Database/Models/SentEmail.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel.DataAnnotations.Schema; -using NodaTime; - -namespace Foxnouns.Backend.Database.Models; - -public class SentEmail -{ - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; init; } - - public required string Email { get; init; } - public required Instant SentAt { get; init; } -} diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 30d97d8..41c9712 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -110,6 +110,7 @@ public static class WebApplicationExtensions .AddSingleton(SystemClock.Instance) .AddSnowflakeGenerator() .AddSingleton() + .AddSingleton() .AddScoped() .AddScoped() .AddScoped() diff --git a/Foxnouns.Backend/Services/DataCleanupService.cs b/Foxnouns.Backend/Services/DataCleanupService.cs index 5e40c08..3d60462 100644 --- a/Foxnouns.Backend/Services/DataCleanupService.cs +++ b/Foxnouns.Backend/Services/DataCleanupService.cs @@ -33,9 +33,6 @@ public class DataCleanupService( public async Task InvokeAsync(CancellationToken ct = default) { - _logger.Debug("Cleaning up sent email cache"); - await CleanEmailsAsync(ct); - _logger.Debug("Cleaning up expired users"); await CleanUsersAsync(ct); @@ -43,14 +40,6 @@ public class DataCleanupService( await CleanExportsAsync(ct); } - private async Task CleanEmailsAsync(CancellationToken ct = default) - { - Instant expiry = clock.GetCurrentInstant() - Duration.FromHours(2); - int count = await db.SentEmails.Where(e => e.SentAt < expiry).ExecuteDeleteAsync(ct); - if (count != 0) - _logger.Information("Deleted {Count} entries from the sent email cache", expiry); - } - private async Task CleanUsersAsync(CancellationToken ct = default) { Instant selfDeleteExpires = clock.GetCurrentInstant() - User.DeleteAfter; diff --git a/Foxnouns.Backend/Services/EmailRateLimiter.cs b/Foxnouns.Backend/Services/EmailRateLimiter.cs new file mode 100644 index 0000000..9e73792 --- /dev/null +++ b/Foxnouns.Backend/Services/EmailRateLimiter.cs @@ -0,0 +1,36 @@ +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/MailService.cs b/Foxnouns.Backend/Services/MailService.cs index e162b33..a1444d9 100644 --- a/Foxnouns.Backend/Services/MailService.cs +++ b/Foxnouns.Backend/Services/MailService.cs @@ -15,22 +15,11 @@ using Coravel.Mailer.Mail; using Coravel.Mailer.Mail.Interfaces; using Coravel.Queuing.Interfaces; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Mailables; -using Microsoft.EntityFrameworkCore; -using NodaTime; namespace Foxnouns.Backend.Services; -public class MailService( - ILogger logger, - IMailer mailer, - IQueue queue, - IClock clock, - Config config, - IServiceProvider serviceProvider -) +public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config config) { private readonly ILogger _logger = logger.ForContext(); @@ -78,29 +67,7 @@ public class MailService( { try { - // ReSharper disable SuggestVarOrType_SimpleTypes - await using var scope = serviceProvider.CreateAsyncScope(); - await using var db = scope.ServiceProvider.GetRequiredService(); - // ReSharper restore SuggestVarOrType_SimpleTypes - - Instant now = clock.GetCurrentInstant(); - - int count = await db.SentEmails.CountAsync(e => - e.Email == to && e.SentAt > (now - Duration.FromHours(1)) - ); - if (count >= 2) - { - _logger.Information( - "Have already sent 2 or more emails to {ToAddress} in the past hour, not sending new email", - to - ); - return; - } - await mailer.SendAsync(mailable); - - db.SentEmails.Add(new SentEmail { Email = to, SentAt = now }); - await db.SaveChangesAsync(); } catch (Exception exc) { From 77c3047b1ef02b9112ba2f90c11b4bb1ff9229bc Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 12 Dec 2024 16:44:01 +0100 Subject: [PATCH 171/261] feat: misskey auth --- .../Authentication/FediverseAuthController.cs | 9 +- Foxnouns.Backend/Dto/Auth.cs | 2 +- .../Extensions/KeyCacheExtensions.cs | 30 ++++ .../Auth/FediverseAuthService.Misskey.cs | 170 ++++++++++++++++++ .../Services/Auth/FediverseAuthService.cs | 14 +- .../mastodon/[instance]/+page.server.ts | 5 +- 6 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs diff --git a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs index d95c622..3dcc817 100644 --- a/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/FediverseAuthController.cs @@ -161,20 +161,13 @@ public class FediverseAuthController( [FromBody] FediverseCallbackRequest req ) { - await remoteAuthService.ValidateAddAccountStateAsync( - req.State, - CurrentUser!.Id, - AuthType.Fediverse, - req.Instance - ); - FediverseApplication app = await fediverseAuthService.GetApplicationAsync(req.Instance); FediverseAuthService.FediverseUser remoteUser = await fediverseAuthService.GetRemoteFediverseUserAsync(app, req.Code); try { AuthMethod authMethod = await authService.AddAuthMethodAsync( - CurrentUser.Id, + CurrentUser!.Id, AuthType.Fediverse, remoteUser.Id, remoteUser.Username, diff --git a/Foxnouns.Backend/Dto/Auth.cs b/Foxnouns.Backend/Dto/Auth.cs index 05308a5..ea9e67d 100644 --- a/Foxnouns.Backend/Dto/Auth.cs +++ b/Foxnouns.Backend/Dto/Auth.cs @@ -59,4 +59,4 @@ public record EmailCallbackRequest(string State); public record EmailChangePasswordRequest(string Current, string New); -public record FediverseCallbackRequest(string Instance, string Code, string State); +public record FediverseCallbackRequest(string Instance, string Code, string? State = null); diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index 1d99830..d7e8784 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -91,6 +91,34 @@ public static class KeyCacheExtensions string state, CancellationToken ct = default ) => await keyCacheService.GetKeyAsync($"add_account:{state}", true, ct); + + public static async Task GenerateForgotPasswordStateAsync( + this KeyCacheService keyCacheService, + string email, + Snowflake userId, + CancellationToken ct = default + ) + { + string state = AuthUtils.RandomToken(); + await keyCacheService.SetKeyAsync( + $"forgot_password:{state}", + new ForgotPasswordState(email, userId), + Duration.FromHours(1), + ct + ); + return state; + } + + public static async Task GetForgotPasswordStateAsync( + this KeyCacheService keyCacheService, + string state, + CancellationToken ct = default + ) => + await keyCacheService.GetKeyAsync( + $"forgot_password:{state}", + true, + ct + ); } public record RegisterEmailState( @@ -98,4 +126,6 @@ public record RegisterEmailState( [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/Services/Auth/FediverseAuthService.Misskey.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs new file mode 100644 index 0000000..beff74a --- /dev/null +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs @@ -0,0 +1,170 @@ +// 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 System.Web; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Extensions; + +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, + string? state = null + ) + { + 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 index 7e67fa7..250455a 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -81,11 +81,11 @@ public partial class FediverseAuthService string softwareName = await GetSoftwareNameAsync(instance); if (IsMastodonCompatible(softwareName)) - { return await CreateMastodonApplicationAsync(instance); - } + if (IsMisskeyCompatible(softwareName)) + return await CreateMisskeyApplicationAsync(instance); - throw new NotImplementedException(); + throw new ApiError.BadRequest($"{softwareName} is not a supported instance type, sorry."); } private async Task GetSoftwareNameAsync(string instance) @@ -129,7 +129,11 @@ public partial class FediverseAuthService forceRefresh, state ), - FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), + FediverseInstanceType.MisskeyApi => await GenerateMisskeyAuthUrlAsync( + app, + forceRefresh, + state + ), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; @@ -141,7 +145,7 @@ public partial class FediverseAuthService app.InstanceType switch { FediverseInstanceType.MastodonApi => await GetMastodonUserAsync(app, code, state), - FediverseInstanceType.MisskeyApi => throw new NotImplementedException(), + FediverseInstanceType.MisskeyApi => await GetMisskeyUserAsync(app, code), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; 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 index fff5322..b092b1e 100644 --- a/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/auth/callback/mastodon/[instance]/+page.server.ts @@ -5,9 +5,10 @@ import createRegisterAction from "$lib/actions/register"; 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; - if (!code || !state) throw new ApiError(undefined, ErrorCode.BadRequest).obj; + const token = url.searchParams.get("token") as string | null; + if ((!code || !state) && !token) throw new ApiError(undefined, ErrorCode.BadRequest).obj; - return { code, state, instance: params.instance! }; + return { code: code || token, state, instance: params.instance! }; }); export const actions = { From 1cf2619393d4a89de7c6d99dc4fbe2051f770933 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 13 Dec 2024 21:25:41 +0100 Subject: [PATCH 172/261] feat: add email to existing account, change password --- .../Authentication/EmailAuthController.cs | 7 +- .../Mailables/AccountCreationMailable.cs | 1 + .../Mailables/AddEmailMailable.cs | 1 + .../Views/Mail/AccountCreation.cshtml | 2 +- Foxnouns.Backend/Views/Mail/AddEmail.cshtml | 2 +- .../components/settings/AuthMethodRow.svelte | 2 +- .../components/settings/EmailSettings.svelte | 73 +++++++++++++++++++ .../components/settings/NewAuthMethod.svelte | 2 + .../src/lib/i18n/locales/en.json | 7 +- .../src/routes/settings/auth/+page.server.ts | 39 +++++++++- .../src/routes/settings/auth/+page.svelte | 18 ++--- .../settings/auth/add-email/+page.server.ts | 44 +++++++++++ .../settings/auth/add-email/+page.svelte | 49 +++++++++++++ 13 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.svelte diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index 1587f87..bbf41f5 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -183,7 +183,7 @@ public class EmailAuthController( return NoContent(); } - [HttpPost("add-email")] + [HttpPost("add-account")] [Authorize("*")] public async Task AddEmailAddressAsync([FromBody] AddEmailAddressRequest req) { @@ -208,6 +208,9 @@ public class EmailAuthController( } else { + ValidationUtils.Validate( + [("password", ValidationUtils.ValidatePassword(req.Password))] + ); await authService.SetUserPasswordAsync(CurrentUser!, req.Password); await db.SaveChangesAsync(); } @@ -232,7 +235,7 @@ public class EmailAuthController( return NoContent(); } - [HttpPost("add-email/callback")] + [HttpPost("add-account/callback")] [Authorize("*")] public async Task AddEmailCallbackAsync([FromBody] EmailCallbackRequest req) { diff --git a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs index 9c33213..41a6609 100644 --- a/Foxnouns.Backend/Mailables/AccountCreationMailable.cs +++ b/Foxnouns.Backend/Mailables/AccountCreationMailable.cs @@ -32,6 +32,7 @@ public class AccountCreationMailable(Config config, AccountCreationMailableView { To(view.To) .From(config.EmailAuth.From!) + .Subject("Create an account") .View("~/Views/Mail/AccountCreation.cshtml", view) .Text(PlainText()); } diff --git a/Foxnouns.Backend/Mailables/AddEmailMailable.cs b/Foxnouns.Backend/Mailables/AddEmailMailable.cs index 1d29d0f..1c381f2 100644 --- a/Foxnouns.Backend/Mailables/AddEmailMailable.cs +++ b/Foxnouns.Backend/Mailables/AddEmailMailable.cs @@ -32,6 +32,7 @@ public class AddEmailMailable(Config config, AddEmailMailableView view) { 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()); } diff --git a/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml b/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml index cf8d1bc..fb85a65 100644 --- a/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml +++ b/Foxnouns.Backend/Views/Mail/AccountCreation.cshtml @@ -3,7 +3,7 @@

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

    diff --git a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml b/Foxnouns.Backend/Views/Mail/AddEmail.cshtml index 2423434..fcdd2b2 100644 --- a/Foxnouns.Backend/Views/Mail/AddEmail.cshtml +++ b/Foxnouns.Backend/Views/Mail/AddEmail.cshtml @@ -3,7 +3,7 @@

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

    diff --git a/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte b/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte index 62c5d6f..692146a 100644 --- a/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte +++ b/Foxnouns.Frontend/src/lib/components/settings/AuthMethodRow.svelte @@ -8,7 +8,7 @@ let name = $derived( method.type === "EMAIL" ? method.remote_id : (method.remote_username ?? method.remote_id), ); - let showId = $derived(method.type !== "FEDIVERSE"); + let showId = $derived(method.type !== "EMAIL");
    diff --git a/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte b/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte new file mode 100644 index 0000000..29a1197 --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte @@ -0,0 +1,73 @@ + + +

    {$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 index 32018a6..73405bc 100644 --- a/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte +++ b/Foxnouns.Frontend/src/lib/components/settings/NewAuthMethod.svelte @@ -19,6 +19,8 @@ return $t("auth.successful-link-tumblr"); case "FEDIVERSE": return $t("auth.successful-link-fedi"); + case "EMAIL": + return $t("auth.successful-link-email"); default: return ""; } diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 659007e..cd45d49 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -56,7 +56,12 @@ "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" + "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:" }, "error": { "bad-request-header": "Something was wrong with your input", diff --git a/Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts index 4fe52f7..65be131 100644 --- a/Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts +++ b/Foxnouns.Frontend/src/routes/settings/auth/+page.server.ts @@ -1,7 +1,44 @@ -import { apiRequest } from "$api"; +import { apiRequest, fastRequest } from "$api"; +import ApiError, { ErrorCode, type RawApiError } from "$api/error.js"; import type { AuthUrls } from "$api/models/auth"; +import log from "$lib/log"; export const load = async ({ fetch }) => { const urls = await apiRequest("POST", "/auth/urls", { fetch, isInternal: true }); return { urls }; }; + +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 index ca0cf27..c0d6056 100644 --- a/Foxnouns.Frontend/src/routes/settings/auth/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/auth/+page.svelte @@ -1,14 +1,13 @@ {#if data.urls.email_enabled} -

    Email addresses

    - + {/if} {#if data.urls.discord}

    Discord accounts

    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 new file mode 100644 index 0000000..4ad86f6 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.server.ts @@ -0,0 +1,44 @@ +import { fastRequest } from "$api"; +import ApiError, { ErrorCode, type RawApiError } from "$api/error.js"; +import log from "$lib/log.js"; +import { redirect } from "@sveltejs/kit"; + +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 new file mode 100644 index 0000000..fb97c1d --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/auth/add-email/+page.svelte @@ -0,0 +1,49 @@ + + +
    +

    Link a new email address

    + + + +
    +
    + + +
    +
    + + +
    + {#if data.firstEmail} +
    + + +
    + {/if} + + +
    +
    From 39a3098a9978404d88bd7ed5bc185f40fd07861a Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Dec 2024 00:46:27 +0100 Subject: [PATCH 173/261] fix: fix all eslint errors --- Foxnouns.Frontend/eslint.config.js | 12 + Foxnouns.Frontend/package.json | 30 +- Foxnouns.Frontend/pnpm-lock.yaml | 758 +++++++++--------- Foxnouns.Frontend/src/lib/actions/callback.ts | 2 +- Foxnouns.Frontend/src/lib/api/error.ts | 1 + Foxnouns.Frontend/src/lib/api/index.ts | 2 +- .../lib/components/editor/BioEditor.svelte | 2 + .../components/profile/ProfileHeader.svelte | 2 + Foxnouns.Frontend/src/lib/errorCodes.ts | 1 + Foxnouns.Frontend/src/lib/i18n/index.ts | 1 + .../src/routes/auth/register/+page.svelte | 6 +- .../src/routes/settings/+page.svelte | 2 +- .../settings/auth/add-email/+page.server.ts | 1 - .../settings/auth/add-email/+page.svelte | 1 - .../src/routes/settings/export/+page.svelte | 2 - .../src/routes/settings/flags/+page.server.ts | 2 +- .../settings/members/[id]/+page.server.ts | 2 +- .../routes/settings/members/[id]/+page.svelte | 3 +- .../routes/settings/profile/+page.server.ts | 2 +- 19 files changed, 420 insertions(+), 412 deletions(-) diff --git a/Foxnouns.Frontend/eslint.config.js b/Foxnouns.Frontend/eslint.config.js index d676276..c1d1e14 100644 --- a/Foxnouns.Frontend/eslint.config.js +++ b/Foxnouns.Frontend/eslint.config.js @@ -30,4 +30,16 @@ export default ts.config( { ignores: ["build/", ".svelte-kit/", "dist/"], }, + { + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_", + caughtErrorsIgnorePattern: "^_", + }, + ], + }, + }, ); diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index bd5ed64..a3783fe 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -12,29 +12,29 @@ "lint": "prettier --check . && eslint ." }, "devDependencies": { - "@sveltejs/adapter-node": "^5.2.9", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^4.0.0", + "@sveltejs/adapter-node": "^5.2.10", + "@sveltejs/kit": "^2.11.1", + "@sveltejs/vite-plugin-svelte": "^4.0.3", "@sveltestrap/sveltestrap": "^6.2.7", - "@types/eslint": "^9.6.0", + "@types/eslint": "^9.6.1", "@types/luxon": "^3.4.2", "@types/markdown-it": "^14.1.2", "@types/sanitize-html": "^2.13.0", "bootstrap": "^5.3.3", - "eslint": "^9.7.0", + "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", - "eslint-plugin-svelte": "^2.36.0", - "globals": "^15.0.0", - "prettier": "^3.3.2", - "prettier-plugin-svelte": "^3.2.6", - "sass": "^1.81.0", - "svelte": "^5.0.0", + "eslint-plugin-svelte": "^2.46.1", + "globals": "^15.13.0", + "prettier": "^3.4.2", + "prettier-plugin-svelte": "^3.3.2", + "sass": "^1.83.0", + "svelte": "^5.12.0", "svelte-bootstrap-icons": "^3.1.1", - "svelte-check": "^4.0.0", + "svelte-check": "^4.1.1", "sveltekit-i18n": "^2.4.2", - "typescript": "^5.0.0", - "typescript-eslint": "^8.0.0", - "vite": "^5.0.3" + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.0", + "vite": "^5.4.11" }, "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee", "dependencies": { diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index d2289ec..45d0d8c 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -43,19 +43,19 @@ importers: version: 4.9.3 devDependencies: '@sveltejs/adapter-node': - specifier: ^5.2.9 - version: 5.2.9(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))) + specifier: ^5.2.10 + version: 5.2.10(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))) '@sveltejs/kit': - specifier: ^2.0.0 - version: 2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + specifier: ^2.11.1 + version: 2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) '@sveltejs/vite-plugin-svelte': - specifier: ^4.0.0 - version: 4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + specifier: ^4.0.3 + version: 4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) '@sveltestrap/sveltestrap': specifier: ^6.2.7 - version: 6.2.7(svelte@5.2.2) + version: 6.2.7(svelte@5.12.0) '@types/eslint': - specifier: ^9.6.0 + specifier: ^9.6.1 version: 9.6.1 '@types/luxon': specifier: ^3.4.2 @@ -70,47 +70,47 @@ importers: specifier: ^5.3.3 version: 5.3.3(@popperjs/core@2.11.8) eslint: - specifier: ^9.7.0 - version: 9.15.0 + specifier: ^9.17.0 + version: 9.17.0 eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.0(eslint@9.15.0) + version: 9.1.0(eslint@9.17.0) eslint-plugin-svelte: - specifier: ^2.36.0 - version: 2.46.0(eslint@9.15.0)(svelte@5.2.2) + specifier: ^2.46.1 + version: 2.46.1(eslint@9.17.0)(svelte@5.12.0) globals: - specifier: ^15.0.0 - version: 15.12.0 + specifier: ^15.13.0 + version: 15.13.0 prettier: - specifier: ^3.3.2 - version: 3.3.3 + specifier: ^3.4.2 + version: 3.4.2 prettier-plugin-svelte: - specifier: ^3.2.6 - version: 3.2.8(prettier@3.3.3)(svelte@5.2.2) + specifier: ^3.3.2 + version: 3.3.2(prettier@3.4.2)(svelte@5.12.0) sass: - specifier: ^1.81.0 - version: 1.81.0 + specifier: ^1.83.0 + version: 1.83.0 svelte: - specifier: ^5.0.0 - version: 5.2.2 + specifier: ^5.12.0 + version: 5.12.0 svelte-bootstrap-icons: specifier: ^3.1.1 version: 3.1.1 svelte-check: - specifier: ^4.0.0 - version: 4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3) + specifier: ^4.1.1 + version: 4.1.1(picomatch@4.0.2)(svelte@5.12.0)(typescript@5.7.2) sveltekit-i18n: specifier: ^2.4.2 - version: 2.4.2(svelte@5.2.2) + version: 2.4.2(svelte@5.12.0) typescript: - specifier: ^5.0.0 - version: 5.6.3 + specifier: ^5.7.2 + version: 5.7.2 typescript-eslint: - specifier: ^8.0.0 - version: 8.14.0(eslint@9.15.0)(typescript@5.6.3) + specifier: ^8.18.0 + version: 8.18.0(eslint@9.17.0)(typescript@5.7.2) vite: - specifier: ^5.0.3 - version: 5.4.11(sass@1.81.0) + specifier: ^5.4.11 + version: 5.4.11(sass@1.83.0) packages: @@ -266,28 +266,28 @@ packages: resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.19.0': - resolution: {integrity: sha512-zdHg2FPIFNKPdcHWtiNT+jEFCHYVplAXRDlQDyqy0zGx/q2parwh7brGJSiTxRk/TSMkbM//zt/f5CHgyTyaSQ==} + '@eslint/config-array@0.19.1': + resolution: {integrity: sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.9.0': - resolution: {integrity: sha512-7ATR9F0e4W85D/0w7cU0SNj7qkAexMG+bAHEZOjo9akvGuhHE2m7umzWzfnpa0XAg5Kxc1BWmtPMV67jJ+9VUg==} + '@eslint/core@0.9.1': + resolution: {integrity: sha512-GuUdqkyyzQI5RMIWkHhvTWLCyLo1jNK3vzkSyaExH5kHPDHcuL2VOpHjmMY+y3+NC69qAKToBqldTBgYeLSr9Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.2.0': resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.15.0': - resolution: {integrity: sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==} + '@eslint/js@9.17.0': + resolution: {integrity: sha512-Sxc4hqcs1kTu0iID3kcZDW3JHq2a77HO9P8CP6YEA/FpH3Ll8UXE2r/86Rz9YJLKme39S9vU5OWNjC6Xl0Cr3w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/object-schema@2.1.4': - resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} + '@eslint/object-schema@2.1.5': + resolution: {integrity: sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.3': - resolution: {integrity: sha512-2b/g5hRmpbb1o4GnTZax9N9m0FXzz9OV42ZzI4rDDMDuHUqigAiQCEWChBWCY4ztAGVRjoWT19v0yMmc5/L5kA==} + '@eslint/plugin-kit@0.2.4': + resolution: {integrity: sha512-zSkKow6H5Kdm0ZUQUB2kV5JIXqoG0+uH5YADhaEHswm664N9Db8dXSi0nMJpacpMf+MyyglF1vnZohpEg5yUtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@fontsource/firago@5.1.0': @@ -313,8 +313,8 @@ packages: resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} engines: {node: '>=18.18'} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} '@jridgewell/resolve-uri@3.1.2': @@ -467,109 +467,114 @@ packages: rollup: optional: true - '@rollup/rollup-android-arm-eabi@4.27.2': - resolution: {integrity: sha512-Tj+j7Pyzd15wAdSJswvs5CJzJNV+qqSUcr/aCD+jpQSBtXvGnV0pnrjoc8zFTe9fcKCatkpFpOO7yAzpO998HA==} + '@rollup/rollup-android-arm-eabi@4.28.1': + resolution: {integrity: sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.27.2': - resolution: {integrity: sha512-xsPeJgh2ThBpUqlLgRfiVYBEf/P1nWlWvReG+aBWfNv3XEBpa6ZCmxSVnxJgLgkNz4IbxpLy64h2gCmAAQLneQ==} + '@rollup/rollup-android-arm64@4.28.1': + resolution: {integrity: sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.27.2': - resolution: {integrity: sha512-KnXU4m9MywuZFedL35Z3PuwiTSn/yqRIhrEA9j+7OSkji39NzVkgxuxTYg5F8ryGysq4iFADaU5osSizMXhU2A==} + '@rollup/rollup-darwin-arm64@4.28.1': + resolution: {integrity: sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.27.2': - resolution: {integrity: sha512-Hj77A3yTvUeCIx/Vi+4d4IbYhyTwtHj07lVzUgpUq9YpJSEiGJj4vXMKwzJ3w5zp5v3PFvpJNgc/J31smZey6g==} + '@rollup/rollup-darwin-x64@4.28.1': + resolution: {integrity: sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.27.2': - resolution: {integrity: sha512-RjgKf5C3xbn8gxvCm5VgKZ4nn0pRAIe90J0/fdHUsgztd3+Zesb2lm2+r6uX4prV2eUByuxJNdt647/1KPRq5g==} + '@rollup/rollup-freebsd-arm64@4.28.1': + resolution: {integrity: sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.27.2': - resolution: {integrity: sha512-duq21FoXwQtuws+V9H6UZ+eCBc7fxSpMK1GQINKn3fAyd9DFYKPJNcUhdIKOrMFjLEJgQskoMoiuizMt+dl20g==} + '@rollup/rollup-freebsd-x64@4.28.1': + resolution: {integrity: sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.27.2': - resolution: {integrity: sha512-6npqOKEPRZkLrMcvyC/32OzJ2srdPzCylJjiTJT2c0bwwSGm7nz2F9mNQ1WrAqCBZROcQn91Fno+khFhVijmFA==} + '@rollup/rollup-linux-arm-gnueabihf@4.28.1': + resolution: {integrity: sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.27.2': - resolution: {integrity: sha512-V9Xg6eXtgBtHq2jnuQwM/jr2mwe2EycnopO8cbOvpzFuySCGtKlPCI3Hj9xup/pJK5Q0388qfZZy2DqV2J8ftw==} + '@rollup/rollup-linux-arm-musleabihf@4.28.1': + resolution: {integrity: sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.27.2': - resolution: {integrity: sha512-uCFX9gtZJoQl2xDTpRdseYuNqyKkuMDtH6zSrBTA28yTfKyjN9hQ2B04N5ynR8ILCoSDOrG/Eg+J2TtJ1e/CSA==} + '@rollup/rollup-linux-arm64-gnu@4.28.1': + resolution: {integrity: sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.27.2': - resolution: {integrity: sha512-/PU9P+7Rkz8JFYDHIi+xzHabOu9qEWR07L5nWLIUsvserrxegZExKCi2jhMZRd0ATdboKylu/K5yAXbp7fYFvA==} + '@rollup/rollup-linux-arm64-musl@4.28.1': + resolution: {integrity: sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': - resolution: {integrity: sha512-eCHmol/dT5odMYi/N0R0HC8V8QE40rEpkyje/ZAXJYNNoSfrObOvG/Mn+s1F/FJyB7co7UQZZf6FuWnN6a7f4g==} + '@rollup/rollup-linux-loongarch64-gnu@4.28.1': + resolution: {integrity: sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': + resolution: {integrity: sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.27.2': - resolution: {integrity: sha512-DEP3Njr9/ADDln3kNi76PXonLMSSMiCir0VHXxmGSHxCxDfQ70oWjHcJGfiBugzaqmYdTC7Y+8Int6qbnxPBIQ==} + '@rollup/rollup-linux-riscv64-gnu@4.28.1': + resolution: {integrity: sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.27.2': - resolution: {integrity: sha512-NHGo5i6IE/PtEPh5m0yw5OmPMpesFnzMIS/lzvN5vknnC1sXM5Z/id5VgcNPgpD+wHmIcuYYgW+Q53v+9s96lQ==} + '@rollup/rollup-linux-s390x-gnu@4.28.1': + resolution: {integrity: sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.27.2': - resolution: {integrity: sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==} + '@rollup/rollup-linux-x64-gnu@4.28.1': + resolution: {integrity: sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.27.2': - resolution: {integrity: sha512-dOlWEMg2gI91Qx5I/HYqOD6iqlJspxLcS4Zlg3vjk1srE67z5T2Uz91yg/qA8sY0XcwQrFzWWiZhMNERylLrpQ==} + '@rollup/rollup-linux-x64-musl@4.28.1': + resolution: {integrity: sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.27.2': - resolution: {integrity: sha512-euMIv/4x5Y2/ImlbGl88mwKNXDsvzbWUlT7DFky76z2keajCtcbAsN9LUdmk31hAoVmJJYSThgdA0EsPeTr1+w==} + '@rollup/rollup-win32-arm64-msvc@4.28.1': + resolution: {integrity: sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.27.2': - resolution: {integrity: sha512-RsnE6LQkUHlkC10RKngtHNLxb7scFykEbEwOFDjr3CeCMG+Rr+cKqlkKc2/wJ1u4u990urRHCbjz31x84PBrSQ==} + '@rollup/rollup-win32-ia32-msvc@4.28.1': + resolution: {integrity: sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.27.2': - resolution: {integrity: sha512-foJM5vv+z2KQmn7emYdDLyTbkoO5bkHZE1oth2tWbQNGW7mX32d46Hz6T0MqXdWS2vBZhaEtHqdy9WYwGfiliA==} + '@rollup/rollup-win32-x64-msvc@4.28.1': + resolution: {integrity: sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==} cpu: [x64] os: [win32] - '@sveltejs/adapter-node@5.2.9': - resolution: {integrity: sha512-51euNrx0AcaTu8//wDfVh7xmqQSVgU52rfinE/MwvGkJa4nHPJMHmzv6+OIpmxg7gZaF6+5NVlxnieCzxLD59g==} + '@sveltejs/adapter-node@5.2.10': + resolution: {integrity: sha512-U0SCdULhHbSYCDgvvrHRtKUykl9GZkM/b3NyeIUtaxM39upQFd5059pWmXgTNaNTU1HMdj4xx0xvNAvQcIzmXQ==} peerDependencies: '@sveltejs/kit': ^2.4.0 - '@sveltejs/kit@2.8.1': - resolution: {integrity: sha512-uuOfFwZ4xvnfPsiTB6a4H1ljjTUksGhWnYq5X/Y9z4x5+3uM2Md8q/YVeHL+7w+mygAwoEFdgKZ8YkUuk+VKww==} + '@sveltejs/kit@2.11.1': + resolution: {integrity: sha512-dAiHDEd+AOm20eYdMPV1a2eKBOc0s/7XsSs7PCoNv2kKS7BAoVRC9uzR+FQmxLtp8xuEo9z8CtrMQoszkThltQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: - '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 + '@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 + vite: ^5.0.3 || ^6.0.0 '@sveltejs/vite-plugin-svelte-inspector@3.0.1': resolution: {integrity: sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==} @@ -579,8 +584,8 @@ packages: svelte: ^5.0.0-next.96 || ^5.0.0 vite: ^5.0.0 - '@sveltejs/vite-plugin-svelte@4.0.1': - resolution: {integrity: sha512-prXoAE/GleD2C4pKgHa9vkdjpzdYwCSw/kmjw6adIyu0vk5YKCfqIztkLg10m+kOYnzZu3bb0NaPTxlWre2a9Q==} + '@sveltejs/vite-plugin-svelte@4.0.3': + resolution: {integrity: sha512-J7nC5gT5qpmvyD2pmzPUntLUgoinyEaNy9sTpGGE6N7pblggO0A1NyneJJvR2ELlzK6ti28aF2SLXG1yJdnJeA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22} peerDependencies: svelte: ^5.0.0-next.96 || ^5.0.0 @@ -629,61 +634,51 @@ packages: '@types/sanitize-html@2.13.0': resolution: {integrity: sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==} - '@typescript-eslint/eslint-plugin@8.14.0': - resolution: {integrity: sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==} + '@typescript-eslint/eslint-plugin@8.18.0': + resolution: {integrity: sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/parser@8.14.0': - resolution: {integrity: sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==} + '@typescript-eslint/parser@8.18.0': + resolution: {integrity: sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/scope-manager@8.14.0': - resolution: {integrity: sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==} + '@typescript-eslint/scope-manager@8.18.0': + resolution: {integrity: sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.14.0': - resolution: {integrity: sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@8.14.0': - resolution: {integrity: sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.14.0': - resolution: {integrity: sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/utils@8.14.0': - resolution: {integrity: sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==} + '@typescript-eslint/type-utils@8.18.0': + resolution: {integrity: sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' - '@typescript-eslint/visitor-keys@8.14.0': - resolution: {integrity: sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==} + '@typescript-eslint/types@8.18.0': + resolution: {integrity: sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.18.0': + resolution: {integrity: sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.18.0': + resolution: {integrity: sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/visitor-keys@8.18.0': + resolution: {integrity: sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -773,8 +768,8 @@ packages: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - cross-spawn@7.0.5: - resolution: {integrity: sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} cssesc@3.0.0: @@ -782,8 +777,8 @@ packages: engines: {node: '>=4'} hasBin: true - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + debug@4.4.0: + resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -844,8 +839,8 @@ packages: peerDependencies: eslint: '>=7.0.0' - eslint-plugin-svelte@2.46.0: - resolution: {integrity: sha512-1A7iEMkzmCZ9/Iz+EAfOGYL8IoIG6zeKEq1SmpxGeM5SXmoQq+ZNnCpXFVJpsxPWYx8jIVGMerQMzX20cqUl0g==} + eslint-plugin-svelte@2.46.1: + resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0-0 || ^9.0.0-0 @@ -870,8 +865,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.15.0: - resolution: {integrity: sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==} + eslint@9.17.0: + resolution: {integrity: sha512-evtlNcpJg+cZLcnVKwsai8fExnqjGPicK7gnUtlNuzu+Fv9bI0aLpND5T44VLQtoMEnI57LoXO9XAkIXwohKrA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -880,8 +875,8 @@ packages: jiti: optional: true - esm-env@1.1.4: - resolution: {integrity: sha512-oO82nKPHKkzIj/hbtuDYy/JHqBHFlMIW36SDiPCVsj87ntDLcWN+sJ1erdVryd4NxODacFTsdrIE3b7IamqbOg==} + esm-env@1.2.1: + resolution: {integrity: sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==} espree@10.3.0: resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} @@ -895,8 +890,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@1.2.2: - resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==} + esrap@1.2.3: + resolution: {integrity: sha512-ZlQmCCK+n7SGoqo7DnfKaP1sJZa49P01/dXzmjCASSo04p72w8EksT2NMK8CEX8DhKsfJXANioIw8VyHNsBfvQ==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -953,8 +948,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + flatted@3.3.2: + resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} @@ -976,8 +971,8 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globals@15.12.0: - resolution: {integrity: sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==} + globals@15.13.0: + resolution: {integrity: sha512-49TewVEz0UxZjr1WYYsWpPrhyC/B/pA8Bq0fUmet2n+eR7yn0IvNzNaoBwnK6mdkzcN+se7Ez9zUgULTz2QH4g==} engines: {node: '>=18'} globalyzer@0.1.0: @@ -1004,8 +999,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immutable@5.0.2: - resolution: {integrity: sha512-1NU7hWZDkV7hJ4PJ9dur9gTNQ4ePNPN4k9/0YhwjzykTi/+3Q5pF93YU5QoVj8BuOnhLgaY8gs0U2pj4kSYVcw==} + immutable@5.0.3: + resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==} import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -1018,8 +1013,8 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - is-core-module@2.15.1: - resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} + is-core-module@2.16.0: + resolution: {integrity: sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==} engines: {node: '>= 0.4'} is-extglob@2.1.1: @@ -1098,8 +1093,8 @@ packages: resolution: {integrity: sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==} engines: {node: '>=12'} - magic-string@0.30.12: - resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} + magic-string@0.30.15: + resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==} markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} @@ -1138,8 +1133,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1226,14 +1221,14 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier-plugin-svelte@3.2.8: - resolution: {integrity: sha512-PAHmmU5cGZdnhW4mWhmvxuG2PVbbHIxUuPOdUKvfE+d4Qt2d29iU5VWrPdsaW5YqVEE0nqhlvN4eoKmVMpIF3Q==} + prettier-plugin-svelte@3.3.2: + resolution: {integrity: sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==} peerDependencies: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier@3.3.3: - resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} engines: {node: '>=14'} hasBin: true @@ -1260,16 +1255,16 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + resolve@1.22.9: + resolution: {integrity: sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==} hasBin: true reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.27.2: - resolution: {integrity: sha512-KreA+PzWmk2yaFmZVwe6GB2uBD86nXl86OsDkt1bJS9p3vqWuEQ6HnJJ+j/mZi/q0920P99/MVRlB4L3crpF5w==} + rollup@4.28.1: + resolution: {integrity: sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1283,8 +1278,8 @@ packages: sanitize-html@2.13.1: resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==} - sass@1.81.0: - resolution: {integrity: sha512-Q4fOxRfhmv3sqCLoGfvrC9pRV8btc0UtqL9mN6Yrv6Qi9ScL55CVH1vlPP863ISLEEMNLLuu9P+enCeGHlnzhA==} + sass@1.83.0: + resolution: {integrity: sha512-qsSxlayzoOjdvXMVLkzF84DJFc2HZEL/rFyGIKbbilYtAvlCxyuzUeff9LawTn4btVnLKg75Z8MMr1lxU1lfGw==} engines: {node: '>=14.0.0'} hasBin: true @@ -1327,8 +1322,8 @@ packages: svelte-bootstrap-icons@3.1.1: resolution: {integrity: sha512-ghJlt6TX3IX35M7wSvGyrmVgXeT5GMRF+7+q6L4OUT2RJWF09mQIvZTZ04Ii3FBfg10KdzFdvVuoB8M0cVHfzw==} - svelte-check@4.0.9: - resolution: {integrity: sha512-SVNCz2L+9ZELGli7G0n3B3QE5kdf0u27RtKr2ZivWQhcWIXatZxwM4VrQ6AiA2k9zKp2mk5AxkEhdjbpjv7rEw==} + svelte-check@4.1.1: + resolution: {integrity: sha512-NfaX+6Qtc8W/CyVGS/F7/XdiSSyXz+WGYA9ZWV3z8tso14V2vzjfXviKaTFEzB7g8TqfgO2FOzP6XT4ApSTUTw==} engines: {node: '>= 18.0.0'} hasBin: true peerDependencies: @@ -1347,8 +1342,8 @@ packages: svelte-tippy@1.3.2: resolution: {integrity: sha512-41f+85hwhKBRqX0UNYrgFsi34Kk/KDvUkIZXYANxkWoA2NTVTCZbUC2J8hRNZ4TRVxObTshoZRjK2co5+i6LMw==} - svelte@5.2.2: - resolution: {integrity: sha512-eHIJRcvA6iuXdRGMESTmBtWTQCcCiol4gyH9DA60ybS35W1x27cvtbndNvWDqX72blyf+AYeQ4gzZ0XGg3L8sw==} + svelte@5.12.0: + resolution: {integrity: sha512-nOd7uj0D/4A3IrHnltaFYndVPGViYSs0s+Zi3N4uQg3owJt9RoiUdwxYx8qjorj5CtaGsx8dNYsFVbH6czrGNg==} engines: {node: '>=18'} sveltekit-i18n@2.4.2: @@ -1370,8 +1365,8 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - ts-api-utils@1.4.0: - resolution: {integrity: sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==} + ts-api-utils@1.4.3: + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} engines: {node: '>=16'} peerDependencies: typescript: '>=4.2.0' @@ -1384,17 +1379,15 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript-eslint@8.14.0: - resolution: {integrity: sha512-K8fBJHxVL3kxMmwByvz8hNdBJ8a0YqKzKDX6jRlrjMuNXyd5T2V02HIq37+OiWXvUUOXgOOGiSSOh26Mh8pC3w==} + typescript-eslint@8.18.0: + resolution: {integrity: sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.8.0' - typescript@5.6.3: - resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} + typescript@5.7.2: + resolution: {integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==} engines: {node: '>=14.17'} hasBin: true @@ -1438,10 +1431,10 @@ packages: terser: optional: true - vitefu@1.0.3: - resolution: {integrity: sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==} + vitefu@1.0.4: + resolution: {integrity: sha512-y6zEE3PQf6uu/Mt6DTJ9ih+kyJLr4XcSgHR2zUkM8SWDhuixEJxfJ6CZGMHh1Ec3vPLoEA0IHU5oWzVqw8ulow==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 peerDependenciesMeta: vite: optional: true @@ -1470,7 +1463,7 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 '@esbuild/aix-ppc64@0.21.5': @@ -1542,27 +1535,29 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.15.0)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.17.0)': dependencies: - eslint: 9.15.0 + eslint: 9.17.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} - '@eslint/config-array@0.19.0': + '@eslint/config-array@0.19.1': dependencies: - '@eslint/object-schema': 2.1.4 - debug: 4.3.7 + '@eslint/object-schema': 2.1.5 + debug: 4.4.0 minimatch: 3.1.2 transitivePeerDependencies: - supports-color - '@eslint/core@0.9.0': {} + '@eslint/core@0.9.1': + dependencies: + '@types/json-schema': 7.0.15 '@eslint/eslintrc@3.2.0': dependencies: ajv: 6.12.6 - debug: 4.3.7 + debug: 4.4.0 espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -1573,11 +1568,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.15.0': {} + '@eslint/js@9.17.0': {} - '@eslint/object-schema@2.1.4': {} + '@eslint/object-schema@2.1.5': {} - '@eslint/plugin-kit@0.2.3': + '@eslint/plugin-kit@0.2.4': dependencies: levn: 0.4.1 @@ -1596,7 +1591,7 @@ snapshots: '@humanwhocodes/retry@0.4.1': {} - '@jridgewell/gen-mapping@0.3.5': + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.0 @@ -1690,154 +1685,157 @@ snapshots: '@popperjs/core@2.11.8': {} - '@rollup/plugin-commonjs@28.0.1(rollup@4.27.2)': + '@rollup/plugin-commonjs@28.0.1(rollup@4.28.1)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.27.2) + '@rollup/pluginutils': 5.1.3(rollup@4.28.1) commondir: 1.0.1 estree-walker: 2.0.2 fdir: 6.4.2(picomatch@4.0.2) is-reference: 1.2.1 - magic-string: 0.30.12 + magic-string: 0.30.15 picomatch: 4.0.2 optionalDependencies: - rollup: 4.27.2 + rollup: 4.28.1 - '@rollup/plugin-json@6.1.0(rollup@4.27.2)': + '@rollup/plugin-json@6.1.0(rollup@4.28.1)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.27.2) + '@rollup/pluginutils': 5.1.3(rollup@4.28.1) optionalDependencies: - rollup: 4.27.2 + rollup: 4.28.1 - '@rollup/plugin-node-resolve@15.3.0(rollup@4.27.2)': + '@rollup/plugin-node-resolve@15.3.0(rollup@4.28.1)': dependencies: - '@rollup/pluginutils': 5.1.3(rollup@4.27.2) + '@rollup/pluginutils': 5.1.3(rollup@4.28.1) '@types/resolve': 1.20.2 deepmerge: 4.3.1 is-module: 1.0.0 - resolve: 1.22.8 + resolve: 1.22.9 optionalDependencies: - rollup: 4.27.2 + rollup: 4.28.1 - '@rollup/pluginutils@5.1.3(rollup@4.27.2)': + '@rollup/pluginutils@5.1.3(rollup@4.28.1)': dependencies: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 optionalDependencies: - rollup: 4.27.2 + rollup: 4.28.1 - '@rollup/rollup-android-arm-eabi@4.27.2': + '@rollup/rollup-android-arm-eabi@4.28.1': optional: true - '@rollup/rollup-android-arm64@4.27.2': + '@rollup/rollup-android-arm64@4.28.1': optional: true - '@rollup/rollup-darwin-arm64@4.27.2': + '@rollup/rollup-darwin-arm64@4.28.1': optional: true - '@rollup/rollup-darwin-x64@4.27.2': + '@rollup/rollup-darwin-x64@4.28.1': optional: true - '@rollup/rollup-freebsd-arm64@4.27.2': + '@rollup/rollup-freebsd-arm64@4.28.1': optional: true - '@rollup/rollup-freebsd-x64@4.27.2': + '@rollup/rollup-freebsd-x64@4.28.1': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.27.2': + '@rollup/rollup-linux-arm-gnueabihf@4.28.1': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.27.2': + '@rollup/rollup-linux-arm-musleabihf@4.28.1': optional: true - '@rollup/rollup-linux-arm64-gnu@4.27.2': + '@rollup/rollup-linux-arm64-gnu@4.28.1': optional: true - '@rollup/rollup-linux-arm64-musl@4.27.2': + '@rollup/rollup-linux-arm64-musl@4.28.1': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.27.2': + '@rollup/rollup-linux-loongarch64-gnu@4.28.1': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.27.2': + '@rollup/rollup-linux-powerpc64le-gnu@4.28.1': optional: true - '@rollup/rollup-linux-s390x-gnu@4.27.2': + '@rollup/rollup-linux-riscv64-gnu@4.28.1': optional: true - '@rollup/rollup-linux-x64-gnu@4.27.2': + '@rollup/rollup-linux-s390x-gnu@4.28.1': optional: true - '@rollup/rollup-linux-x64-musl@4.27.2': + '@rollup/rollup-linux-x64-gnu@4.28.1': optional: true - '@rollup/rollup-win32-arm64-msvc@4.27.2': + '@rollup/rollup-linux-x64-musl@4.28.1': optional: true - '@rollup/rollup-win32-ia32-msvc@4.27.2': + '@rollup/rollup-win32-arm64-msvc@4.28.1': optional: true - '@rollup/rollup-win32-x64-msvc@4.27.2': + '@rollup/rollup-win32-ia32-msvc@4.28.1': optional: true - '@sveltejs/adapter-node@5.2.9(@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))': + '@rollup/rollup-win32-x64-msvc@4.28.1': + optional: true + + '@sveltejs/adapter-node@5.2.10(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))': dependencies: - '@rollup/plugin-commonjs': 28.0.1(rollup@4.27.2) - '@rollup/plugin-json': 6.1.0(rollup@4.27.2) - '@rollup/plugin-node-resolve': 15.3.0(rollup@4.27.2) - '@sveltejs/kit': 2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) - rollup: 4.27.2 + '@rollup/plugin-commonjs': 28.0.1(rollup@4.28.1) + '@rollup/plugin-json': 6.1.0(rollup@4.28.1) + '@rollup/plugin-node-resolve': 15.3.0(rollup@4.28.1) + '@sveltejs/kit': 2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + rollup: 4.28.1 - '@sveltejs/kit@2.8.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))': + '@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) + '@sveltejs/vite-plugin-svelte': 4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 - esm-env: 1.1.4 + esm-env: 1.2.1 import-meta-resolve: 4.1.0 kleur: 4.1.5 - magic-string: 0.30.12 + magic-string: 0.30.15 mrmime: 2.0.0 sade: 1.8.1 set-cookie-parser: 2.7.1 sirv: 3.0.0 - svelte: 5.2.2 + svelte: 5.12.0 tiny-glob: 0.2.9 - vite: 5.4.11(sass@1.81.0) + vite: 5.4.11(sass@1.83.0) - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) - debug: 4.3.7 - svelte: 5.2.2 - vite: 5.4.11(sass@1.81.0) + '@sveltejs/vite-plugin-svelte': 4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + debug: 4.4.0 + svelte: 5.12.0 + vite: 5.4.11(sass@1.83.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0))': + '@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.1(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)))(svelte@5.2.2)(vite@5.4.11(sass@1.81.0)) - debug: 4.3.7 + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.12 - svelte: 5.2.2 - vite: 5.4.11(sass@1.81.0) - vitefu: 1.0.3(vite@5.4.11(sass@1.81.0)) + magic-string: 0.30.15 + svelte: 5.12.0 + vite: 5.4.11(sass@1.83.0) + vitefu: 1.0.4(vite@5.4.11(sass@1.83.0)) transitivePeerDependencies: - supports-color - '@sveltekit-i18n/base@1.3.7(svelte@5.2.2)': + '@sveltekit-i18n/base@1.3.7(svelte@5.12.0)': dependencies: - svelte: 5.2.2 + svelte: 5.12.0 '@sveltekit-i18n/parser-default@1.1.1': {} - '@sveltestrap/sveltestrap@6.2.7(svelte@5.2.2)': + '@sveltestrap/sveltestrap@6.2.7(svelte@5.12.0)': dependencies: '@popperjs/core': 2.11.8 - svelte: 5.2.2 + svelte: 5.12.0 '@types/cookie@0.6.0': {} @@ -1867,86 +1865,82 @@ snapshots: dependencies: htmlparser2: 8.0.2 - '@typescript-eslint/eslint-plugin@8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.15.0)(typescript@5.6.3))(eslint@9.15.0)(typescript@5.6.3)': + '@typescript-eslint/eslint-plugin@8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.14.0(eslint@9.15.0)(typescript@5.6.3) - '@typescript-eslint/scope-manager': 8.14.0 - '@typescript-eslint/type-utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.14.0 - eslint: 9.15.0 + '@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/type-utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.18.0 + eslint: 9.17.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.14.0(eslint@9.15.0)(typescript@5.6.3)': + '@typescript-eslint/parser@8.18.0(eslint@9.17.0)(typescript@5.7.2)': dependencies: - '@typescript-eslint/scope-manager': 8.14.0 - '@typescript-eslint/types': 8.14.0 - '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) - '@typescript-eslint/visitor-keys': 8.14.0 - debug: 4.3.7 - eslint: 9.15.0 - optionalDependencies: - typescript: 5.6.3 + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + '@typescript-eslint/visitor-keys': 8.18.0 + debug: 4.4.0 + eslint: 9.17.0 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.14.0': + '@typescript-eslint/scope-manager@8.18.0': dependencies: - '@typescript-eslint/types': 8.14.0 - '@typescript-eslint/visitor-keys': 8.14.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 - '@typescript-eslint/type-utils@8.14.0(eslint@9.15.0)(typescript@5.6.3)': + '@typescript-eslint/type-utils@8.18.0(eslint@9.17.0)(typescript@5.7.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) - '@typescript-eslint/utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) - debug: 4.3.7 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + debug: 4.4.0 + eslint: 9.17.0 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 transitivePeerDependencies: - - eslint - supports-color - '@typescript-eslint/types@8.14.0': {} + '@typescript-eslint/types@8.18.0': {} - '@typescript-eslint/typescript-estree@8.14.0(typescript@5.6.3)': + '@typescript-eslint/typescript-estree@8.18.0(typescript@5.7.2)': dependencies: - '@typescript-eslint/types': 8.14.0 - '@typescript-eslint/visitor-keys': 8.14.0 - debug: 4.3.7 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/visitor-keys': 8.18.0 + debug: 4.4.0 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.4.0(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 + ts-api-utils: 1.4.3(typescript@5.7.2) + typescript: 5.7.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.14.0(eslint@9.15.0)(typescript@5.6.3)': + '@typescript-eslint/utils@8.18.0(eslint@9.17.0)(typescript@5.7.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) - '@typescript-eslint/scope-manager': 8.14.0 - '@typescript-eslint/types': 8.14.0 - '@typescript-eslint/typescript-estree': 8.14.0(typescript@5.6.3) - eslint: 9.15.0 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) + '@typescript-eslint/scope-manager': 8.18.0 + '@typescript-eslint/types': 8.18.0 + '@typescript-eslint/typescript-estree': 8.18.0(typescript@5.7.2) + eslint: 9.17.0 + typescript: 5.7.2 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/visitor-keys@8.14.0': + '@typescript-eslint/visitor-keys@8.18.0': dependencies: - '@typescript-eslint/types': 8.14.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.18.0 + eslint-visitor-keys: 4.2.0 acorn-jsx@5.3.2(acorn@8.14.0): dependencies: @@ -2021,7 +2015,7 @@ snapshots: cookie@0.6.0: {} - cross-spawn@7.0.5: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 @@ -2029,7 +2023,7 @@ snapshots: cssesc@3.0.0: {} - debug@4.3.7: + debug@4.4.0: dependencies: ms: 2.1.3 @@ -2090,21 +2084,21 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-compat-utils@0.5.1(eslint@9.15.0): + eslint-compat-utils@0.5.1(eslint@9.17.0): dependencies: - eslint: 9.15.0 + eslint: 9.17.0 semver: 7.6.3 - eslint-config-prettier@9.1.0(eslint@9.15.0): + eslint-config-prettier@9.1.0(eslint@9.17.0): dependencies: - eslint: 9.15.0 + eslint: 9.17.0 - eslint-plugin-svelte@2.46.0(eslint@9.15.0)(svelte@5.2.2): + eslint-plugin-svelte@2.46.1(eslint@9.17.0)(svelte@5.12.0): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) '@jridgewell/sourcemap-codec': 1.5.0 - eslint: 9.15.0 - eslint-compat-utils: 0.5.1(eslint@9.15.0) + eslint: 9.17.0 + eslint-compat-utils: 0.5.1(eslint@9.17.0) esutils: 2.0.3 known-css-properties: 0.35.0 postcss: 8.4.49 @@ -2112,9 +2106,9 @@ snapshots: postcss-safe-parser: 6.0.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 semver: 7.6.3 - svelte-eslint-parser: 0.43.0(svelte@5.2.2) + svelte-eslint-parser: 0.43.0(svelte@5.12.0) optionalDependencies: - svelte: 5.2.2 + svelte: 5.12.0 transitivePeerDependencies: - ts-node @@ -2132,15 +2126,15 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.15.0: + eslint@9.17.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.15.0) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.19.0 - '@eslint/core': 0.9.0 + '@eslint/config-array': 0.19.1 + '@eslint/core': 0.9.1 '@eslint/eslintrc': 3.2.0 - '@eslint/js': 9.15.0 - '@eslint/plugin-kit': 0.2.3 + '@eslint/js': 9.17.0 + '@eslint/plugin-kit': 0.2.4 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.1 @@ -2148,8 +2142,8 @@ snapshots: '@types/json-schema': 7.0.15 ajv: 6.12.6 chalk: 4.1.2 - cross-spawn: 7.0.5 - debug: 4.3.7 + cross-spawn: 7.0.6 + debug: 4.4.0 escape-string-regexp: 4.0.0 eslint-scope: 8.2.0 eslint-visitor-keys: 4.2.0 @@ -2171,7 +2165,7 @@ snapshots: transitivePeerDependencies: - supports-color - esm-env@1.1.4: {} + esm-env@1.2.1: {} espree@10.3.0: dependencies: @@ -2189,7 +2183,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@1.2.2: + esrap@1.2.3: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 '@types/estree': 1.0.6 @@ -2241,10 +2235,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.1 + flatted: 3.3.2 keyv: 4.5.4 - flatted@3.3.1: {} + flatted@3.3.2: {} fsevents@2.3.3: optional: true @@ -2261,7 +2255,7 @@ snapshots: globals@14.0.0: {} - globals@15.12.0: {} + globals@15.13.0: {} globalyzer@0.1.0: {} @@ -2284,7 +2278,7 @@ snapshots: ignore@5.3.2: {} - immutable@5.0.2: {} + immutable@5.0.3: {} import-fresh@3.3.0: dependencies: @@ -2295,7 +2289,7 @@ snapshots: imurmurhash@0.1.4: {} - is-core-module@2.15.1: + is-core-module@2.16.0: dependencies: hasown: 2.0.2 @@ -2360,7 +2354,7 @@ snapshots: luxon@3.5.0: {} - magic-string@0.30.12: + magic-string@0.30.15: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -2398,7 +2392,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.7: {} + nanoid@3.3.8: {} natural-compare@1.4.0: {} @@ -2462,18 +2456,18 @@ snapshots: postcss@8.4.49: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.1.1 source-map-js: 1.2.1 prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.2.8(prettier@3.3.3)(svelte@5.2.2): + prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.12.0): dependencies: - prettier: 3.3.3 - svelte: 5.2.2 + prettier: 3.4.2 + svelte: 5.12.0 - prettier@3.3.3: {} + prettier@3.4.2: {} pretty-bytes@6.1.1: {} @@ -2487,36 +2481,37 @@ snapshots: resolve-from@4.0.0: {} - resolve@1.22.8: + resolve@1.22.9: dependencies: - is-core-module: 2.15.1 + is-core-module: 2.16.0 path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 reusify@1.0.4: {} - rollup@4.27.2: + rollup@4.28.1: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.27.2 - '@rollup/rollup-android-arm64': 4.27.2 - '@rollup/rollup-darwin-arm64': 4.27.2 - '@rollup/rollup-darwin-x64': 4.27.2 - '@rollup/rollup-freebsd-arm64': 4.27.2 - '@rollup/rollup-freebsd-x64': 4.27.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.27.2 - '@rollup/rollup-linux-arm-musleabihf': 4.27.2 - '@rollup/rollup-linux-arm64-gnu': 4.27.2 - '@rollup/rollup-linux-arm64-musl': 4.27.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.27.2 - '@rollup/rollup-linux-riscv64-gnu': 4.27.2 - '@rollup/rollup-linux-s390x-gnu': 4.27.2 - '@rollup/rollup-linux-x64-gnu': 4.27.2 - '@rollup/rollup-linux-x64-musl': 4.27.2 - '@rollup/rollup-win32-arm64-msvc': 4.27.2 - '@rollup/rollup-win32-ia32-msvc': 4.27.2 - '@rollup/rollup-win32-x64-msvc': 4.27.2 + '@rollup/rollup-android-arm-eabi': 4.28.1 + '@rollup/rollup-android-arm64': 4.28.1 + '@rollup/rollup-darwin-arm64': 4.28.1 + '@rollup/rollup-darwin-x64': 4.28.1 + '@rollup/rollup-freebsd-arm64': 4.28.1 + '@rollup/rollup-freebsd-x64': 4.28.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.28.1 + '@rollup/rollup-linux-arm-musleabihf': 4.28.1 + '@rollup/rollup-linux-arm64-gnu': 4.28.1 + '@rollup/rollup-linux-arm64-musl': 4.28.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.28.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.28.1 + '@rollup/rollup-linux-riscv64-gnu': 4.28.1 + '@rollup/rollup-linux-s390x-gnu': 4.28.1 + '@rollup/rollup-linux-x64-gnu': 4.28.1 + '@rollup/rollup-linux-x64-musl': 4.28.1 + '@rollup/rollup-win32-arm64-msvc': 4.28.1 + '@rollup/rollup-win32-ia32-msvc': 4.28.1 + '@rollup/rollup-win32-x64-msvc': 4.28.1 fsevents: 2.3.3 run-parallel@1.2.0: @@ -2536,10 +2531,10 @@ snapshots: parse-srcset: 1.0.2 postcss: 8.4.49 - sass@1.81.0: + sass@1.83.0: dependencies: chokidar: 4.0.1 - immutable: 5.0.2 + immutable: 5.0.3 source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.0 @@ -2572,19 +2567,19 @@ snapshots: svelte-bootstrap-icons@3.1.1: {} - svelte-check@4.0.9(picomatch@4.0.2)(svelte@5.2.2)(typescript@5.6.3): + svelte-check@4.1.1(picomatch@4.0.2)(svelte@5.12.0)(typescript@5.7.2): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 4.0.1 fdir: 6.4.2(picomatch@4.0.2) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.2.2 - typescript: 5.6.3 + svelte: 5.12.0 + typescript: 5.7.2 transitivePeerDependencies: - picomatch - svelte-eslint-parser@0.43.0(svelte@5.2.2): + svelte-eslint-parser@0.43.0(svelte@5.12.0): dependencies: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -2592,13 +2587,13 @@ snapshots: postcss: 8.4.49 postcss-scss: 4.0.9(postcss@8.4.49) optionalDependencies: - svelte: 5.2.2 + svelte: 5.12.0 svelte-tippy@1.3.2: dependencies: tippy.js: 6.3.7 - svelte@5.2.2: + svelte@5.12.0: dependencies: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.0 @@ -2607,18 +2602,18 @@ snapshots: acorn-typescript: 1.4.13(acorn@8.14.0) aria-query: 5.3.2 axobject-query: 4.1.0 - esm-env: 1.1.4 - esrap: 1.2.2 + esm-env: 1.2.1 + esrap: 1.2.3 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.12 + magic-string: 0.30.15 zimmerframe: 1.1.2 - sveltekit-i18n@2.4.2(svelte@5.2.2): + sveltekit-i18n@2.4.2(svelte@5.12.0): dependencies: - '@sveltekit-i18n/base': 1.3.7(svelte@5.2.2) + '@sveltekit-i18n/base': 1.3.7(svelte@5.12.0) '@sveltekit-i18n/parser-default': 1.1.1 - svelte: 5.2.2 + svelte: 5.12.0 tiny-glob@0.2.9: dependencies: @@ -2635,9 +2630,9 @@ snapshots: totalist@3.0.1: {} - ts-api-utils@1.4.0(typescript@5.6.3): + ts-api-utils@1.4.3(typescript@5.7.2): dependencies: - typescript: 5.6.3 + typescript: 5.7.2 tslog@4.9.3: {} @@ -2645,18 +2640,17 @@ snapshots: dependencies: prelude-ls: 1.2.1 - typescript-eslint@8.14.0(eslint@9.15.0)(typescript@5.6.3): + typescript-eslint@8.18.0(eslint@9.17.0)(typescript@5.7.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.14.0(@typescript-eslint/parser@8.14.0(eslint@9.15.0)(typescript@5.6.3))(eslint@9.15.0)(typescript@5.6.3) - '@typescript-eslint/parser': 8.14.0(eslint@9.15.0)(typescript@5.6.3) - '@typescript-eslint/utils': 8.14.0(eslint@9.15.0)(typescript@5.6.3) - optionalDependencies: - typescript: 5.6.3 + '@typescript-eslint/eslint-plugin': 8.18.0(@typescript-eslint/parser@8.18.0(eslint@9.17.0)(typescript@5.7.2))(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/parser': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + '@typescript-eslint/utils': 8.18.0(eslint@9.17.0)(typescript@5.7.2) + eslint: 9.17.0 + typescript: 5.7.2 transitivePeerDependencies: - - eslint - supports-color - typescript@5.6.3: {} + typescript@5.7.2: {} uc.micro@2.1.0: {} @@ -2666,18 +2660,18 @@ snapshots: util-deprecate@1.0.2: {} - vite@5.4.11(sass@1.81.0): + vite@5.4.11(sass@1.83.0): dependencies: esbuild: 0.21.5 postcss: 8.4.49 - rollup: 4.27.2 + rollup: 4.28.1 optionalDependencies: fsevents: 2.3.3 - sass: 1.81.0 + sass: 1.83.0 - vitefu@1.0.3(vite@5.4.11(sass@1.81.0)): + vitefu@1.0.4(vite@5.4.11(sass@1.83.0)): optionalDependencies: - vite: 5.4.11(sass@1.81.0) + vite: 5.4.11(sass@1.83.0) which@2.0.2: dependencies: diff --git a/Foxnouns.Frontend/src/lib/actions/callback.ts b/Foxnouns.Frontend/src/lib/actions/callback.ts index 865e106..3df1070 100644 --- a/Foxnouns.Frontend/src/lib/actions/callback.ts +++ b/Foxnouns.Frontend/src/lib/actions/callback.ts @@ -10,7 +10,7 @@ export default function createCallbackLoader( bodyFn?: (event: ServerLoadEvent) => Promise, ) { return async (event: ServerLoadEvent) => { - const { url, parent, fetch, cookies } = event; + const { parent, fetch, cookies } = event; bodyFn ??= async ({ url }) => { const code = url.searchParams.get("code") as string | null; diff --git a/Foxnouns.Frontend/src/lib/api/error.ts b/Foxnouns.Frontend/src/lib/api/error.ts index eb93884..1fd2041 100644 --- a/Foxnouns.Frontend/src/lib/api/error.ts +++ b/Foxnouns.Frontend/src/lib/api/error.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ export default class ApiError { raw?: RawApiError; code: ErrorCode; diff --git a/Foxnouns.Frontend/src/lib/api/index.ts b/Foxnouns.Frontend/src/lib/api/index.ts index f7a517d..0a4047d 100644 --- a/Foxnouns.Frontend/src/lib/api/index.ts +++ b/Foxnouns.Frontend/src/lib/api/index.ts @@ -23,7 +23,7 @@ export type RequestArgs = { /** * The body for this request, which will be serialized to JSON. Should be a plain JS object. */ - body?: any; + body?: unknown; /** * The fetch function to use. Should be passed in loader and action functions, but can be safely ignored for client-side requests. */ diff --git a/Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte index 8c7e744..e0091ff 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/BioEditor.svelte @@ -21,6 +21,8 @@ {#if value !== ""}
    {$t("edit-profile.preview")}
    + +
    {@html renderMarkdown(value)}
    {/if} diff --git a/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte b/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte index 0fd1960..aa413db 100644 --- a/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte +++ b/Foxnouns.Frontend/src/lib/components/profile/ProfileHeader.svelte @@ -47,6 +47,8 @@ {/if} {#if bio}
    + +

    {@html bio}

    {/if}
    diff --git a/Foxnouns.Frontend/src/lib/errorCodes.ts b/Foxnouns.Frontend/src/lib/errorCodes.ts index b9c3d9a..b97b71b 100644 --- a/Foxnouns.Frontend/src/lib/errorCodes.ts +++ b/Foxnouns.Frontend/src/lib/errorCodes.ts @@ -1,6 +1,7 @@ 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 { diff --git a/Foxnouns.Frontend/src/lib/i18n/index.ts b/Foxnouns.Frontend/src/lib/i18n/index.ts index 858f2bd..27a9603 100644 --- a/Foxnouns.Frontend/src/lib/i18n/index.ts +++ b/Foxnouns.Frontend/src/lib/i18n/index.ts @@ -1,6 +1,7 @@ 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", diff --git a/Foxnouns.Frontend/src/routes/auth/register/+page.svelte b/Foxnouns.Frontend/src/routes/auth/register/+page.svelte index b43b789..59c6181 100644 --- a/Foxnouns.Frontend/src/routes/auth/register/+page.svelte +++ b/Foxnouns.Frontend/src/routes/auth/register/+page.svelte @@ -1,12 +1,12 @@ diff --git a/Foxnouns.Frontend/src/routes/settings/+page.svelte b/Foxnouns.Frontend/src/routes/settings/+page.svelte index f43c8a5..74b4a49 100644 --- a/Foxnouns.Frontend/src/routes/settings/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/+page.svelte @@ -1,7 +1,7 @@ + + +{currentTime} (UTC{timezone}) diff --git a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte index 903312d..cefd8bc 100644 --- a/Foxnouns.Frontend/src/routes/@[username]/+page.svelte +++ b/Foxnouns.Frontend/src/routes/@[username]/+page.svelte @@ -25,7 +25,7 @@ {/if} - + {#if data.members.length > 0} From 9d3309333981fd80dced02f7629a57a12f75e009 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Dec 2024 16:32:08 +0100 Subject: [PATCH 176/261] feat: forgot password/reset password --- .../Authentication/EmailAuthController.cs | 57 +++++++++++++++++++ Foxnouns.Backend/Dto/Auth.cs | 4 ++ .../Extensions/KeyCacheExtensions.cs | 8 +-- .../Jobs/CreateDataExportInvocable.cs | 2 +- .../Mailables/PasswordChangedMailable.cs | 25 ++++++++ .../Mailables/ResetPasswordMailable.cs | 32 +++++++++++ Foxnouns.Backend/Services/MailService.cs | 35 ++++++++++++ Foxnouns.Backend/Utils/AuthUtils.cs | 7 ++- .../Views/Mail/PasswordChanged.cshtml | 8 +++ .../Views/Mail/ResetPassword.cshtml | 14 +++++ .../components/settings/EmailSettings.svelte | 2 +- .../src/lib/i18n/locales/en.json | 8 ++- .../auth/forgot-password/+page.server.ts | 34 +++++++++++ .../routes/auth/forgot-password/+page.svelte | 35 ++++++++++++ .../forgot-password/[code]/+page.server.ts | 48 ++++++++++++++++ .../auth/forgot-password/[code]/+page.svelte | 41 +++++++++++++ .../src/routes/auth/log-in/+page.svelte | 39 +++++++------ 17 files changed, 374 insertions(+), 25 deletions(-) create mode 100644 Foxnouns.Backend/Mailables/PasswordChangedMailable.cs create mode 100644 Foxnouns.Backend/Mailables/ResetPasswordMailable.cs create mode 100644 Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml create mode 100644 Foxnouns.Backend/Views/Mail/ResetPassword.cshtml create mode 100644 Foxnouns.Frontend/src/routes/auth/forgot-password/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/forgot-password/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.svelte diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index bbf41f5..bdf4b9a 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -183,6 +183,63 @@ public class EmailAuthController( 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) diff --git a/Foxnouns.Backend/Dto/Auth.cs b/Foxnouns.Backend/Dto/Auth.cs index ea9e67d..fbf5951 100644 --- a/Foxnouns.Backend/Dto/Auth.cs +++ b/Foxnouns.Backend/Dto/Auth.cs @@ -59,4 +59,8 @@ 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/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index d7e8784..615cc3d 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -28,7 +28,7 @@ public static class KeyCacheExtensions CancellationToken ct = default ) { - string state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); + string state = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct); return state; } @@ -51,8 +51,7 @@ public static class KeyCacheExtensions CancellationToken ct = default ) { - // This state is used in links, not just as JSON values, so make it URL-safe - string state = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); + string state = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync( $"email_state:{state}", new RegisterEmailState(email, userId), @@ -112,11 +111,12 @@ public static class KeyCacheExtensions public static async Task GetForgotPasswordStateAsync( this KeyCacheService keyCacheService, string state, + bool delete = true, CancellationToken ct = default ) => await keyCacheService.GetKeyAsync( $"forgot_password:{state}", - true, + delete, ct ); } diff --git a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs index cd5c97f..4d9e1b0 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs @@ -102,7 +102,7 @@ public class CreateDataExportInvocable( stream.Seek(0, SeekOrigin.Begin); // Upload the file! - string filename = AuthUtils.RandomToken().Replace('+', '-').Replace('/', '_'); + string filename = AuthUtils.RandomToken(); await objectStorageService.PutObjectAsync( ExportPath(user.Id, filename), stream, diff --git a/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs b/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs new file mode 100644 index 0000000..79d86e3 --- /dev/null +++ b/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs @@ -0,0 +1,25 @@ +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 new file mode 100644 index 0000000..0f89b90 --- /dev/null +++ b/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs @@ -0,0 +1,32 @@ +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/Services/MailService.cs b/Foxnouns.Backend/Services/MailService.cs index a1444d9..83458d6 100644 --- a/Foxnouns.Backend/Services/MailService.cs +++ b/Foxnouns.Backend/Services/MailService.cs @@ -63,6 +63,41 @@ public class MailService(ILogger logger, IMailer mailer, IQueue queue, Config co }); } + 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 diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 491694a..8a35cdc 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -131,7 +131,12 @@ public static class AuthUtils } public static string RandomToken(int bytes = 48) => - Convert.ToBase64String(RandomNumberGenerator.GetBytes(bytes)).Trim('='); + Convert + .ToBase64String(RandomNumberGenerator.GetBytes(bytes)) + .Trim('=') + // 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/Views/Mail/PasswordChanged.cshtml b/Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml new file mode 100644 index 0000000..458dcdf --- /dev/null +++ b/Foxnouns.Backend/Views/Mail/PasswordChanged.cshtml @@ -0,0 +1,8 @@ +@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 new file mode 100644 index 0000000..f141d8b --- /dev/null +++ b/Foxnouns.Backend/Views/Mail/ResetPassword.cshtml @@ -0,0 +1,14 @@ +@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.Frontend/src/lib/components/settings/EmailSettings.svelte b/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte index 29a1197..4bd3318 100644 --- a/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte +++ b/Foxnouns.Frontend/src/lib/components/settings/EmailSettings.svelte @@ -35,7 +35,7 @@
    - +

    Change password

    diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index cd45d49..f9de99f 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -61,7 +61,13 @@ "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:" + "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!" }, "error": { "bad-request-header": "Something was wrong with your input", diff --git a/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.server.ts b/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.server.ts new file mode 100644 index 0000000..f7195a0 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.server.ts @@ -0,0 +1,34 @@ +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 new file mode 100644 index 0000000..97d902a --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/forgot-password/+page.svelte @@ -0,0 +1,35 @@ + + + + {$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 new file mode 100644 index 0000000..896504e --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.server.ts @@ -0,0 +1,48 @@ +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 new file mode 100644 index 0000000..fa01587 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/auth/forgot-password/[code]/+page.svelte @@ -0,0 +1,41 @@ + + + + {$t("auth.reset-password-title")} • pronouns.cc + + +
    +
    +

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

    + + + +
    + +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    diff --git a/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte b/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte index 33d3e31..c6c47a9 100644 --- a/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte +++ b/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte @@ -2,7 +2,6 @@ import type { ActionData, PageData } from "./$types"; import { t } from "$lib/i18n"; import { enhance } from "$app/forms"; - import { Button, ButtonGroup, Input, InputGroup } from "@sveltestrap/sveltestrap"; import ErrorAlert from "$components/ErrorAlert.svelte"; type Props = { data: PageData; form: ActionData }; @@ -21,29 +20,34 @@
    {#if data.urls.email_enabled} -
    +

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

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

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

    {:else}
    {/if} -
    +

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

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

    @@ -71,19 +75,20 @@ {#if form?.showFediBox}

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

    - - + - - + +

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

    {/if} From 49b2902d6d2c32f6616ff869f023af5ae83f52c7 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Dec 2024 16:39:02 +0100 Subject: [PATCH 177/261] fix: use url-unsafe base 64 for auth tokens .net throws an error when decoding url-safe base 64 luckily we never decode it *except* for tokens, so those can keep using url-unsafe base 64. they're never used in URLs after all --- Foxnouns.Backend/Services/Auth/AuthService.cs | 2 +- Foxnouns.Backend/Utils/AuthUtils.cs | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Foxnouns.Backend/Services/Auth/AuthService.cs b/Foxnouns.Backend/Services/Auth/AuthService.cs index 89248cd..f8c2428 100644 --- a/Foxnouns.Backend/Services/Auth/AuthService.cs +++ b/Foxnouns.Backend/Services/Auth/AuthService.cs @@ -358,7 +358,7 @@ public class AuthService( private static (string, byte[]) GenerateToken() { - string token = AuthUtils.RandomToken(); + string token = AuthUtils.RandomUrlUnsafeToken(); byte[] hash = SHA512.HashData(Convert.FromBase64String(token)); return (token, hash); diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 8a35cdc..5ebd745 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -130,10 +130,11 @@ public static class AuthUtils 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) => - Convert - .ToBase64String(RandomNumberGenerator.GetBytes(bytes)) - .Trim('=') + RandomUrlUnsafeToken() // Make the token URL-safe .Replace('+', '-') .Replace('/', '_'); From 11257ae069b8dd2fcb94e77adfbe51c73be528b6 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Dec 2024 16:51:58 +0100 Subject: [PATCH 178/261] chore: clean up backend code, fix most inspections --- Foxnouns.Backend/Config.cs | 6 ++++-- .../Authentication/TumblrAuthController.cs | 14 ++++++++++++++ Foxnouns.Backend/Controllers/FlagsController.cs | 2 +- Foxnouns.Backend/Database/Models/User.cs | 4 +++- Foxnouns.Backend/Dto/Flag.cs | 3 +-- Foxnouns.Backend/Dto/Member.cs | 1 + Foxnouns.Backend/Dto/User.cs | 1 + .../Extensions/WebApplicationExtensions.cs | 2 +- Foxnouns.Backend/Jobs/Payloads.cs | 8 +------- .../Mailables/PasswordChangedMailable.cs | 14 ++++++++++++++ .../Mailables/ResetPasswordMailable.cs | 14 ++++++++++++++ .../Middleware/AuthorizationMiddleware.cs | 1 - .../Services/Auth/FediverseAuthService.Misskey.cs | 5 +---- .../Services/Auth/FediverseAuthService.cs | 3 +-- Foxnouns.Backend/Services/EmailRateLimiter.cs | 14 ++++++++++++++ Foxnouns.Backend/Services/MemberRendererService.cs | 1 - .../Services/MetricsCollectionService.cs | 2 +- Foxnouns.Backend/Services/ObjectStorageService.cs | 1 - Foxnouns.Backend/Services/UserRendererService.cs | 2 -- .../Utils/OpenApi/PropertyKeySchemaTransformer.cs | 14 ++++++++++++++ .../Utils/ValidationUtils.Preferences.cs | 1 - 21 files changed, 86 insertions(+), 27 deletions(-) diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 3874204..e0a579b 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -12,6 +12,8 @@ // // 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; @@ -20,8 +22,8 @@ public class Config { public string Host { get; init; } = "localhost"; public int Port { get; init; } = 3000; - public string BaseUrl { get; set; } = null!; - public string MediaBaseUrl { get; set; } = null!; + public string BaseUrl { get; init; } = null!; + public string MediaBaseUrl { get; init; } = null!; public string Address => $"http://{Host}:{Port}"; public string MetricsAddress => $"http://{Host}:{Logging.MetricsPort}"; diff --git a/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs index 919ca3f..9860957 100644 --- a/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/TumblrAuthController.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index c68fb96..2b145ac 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -73,7 +73,7 @@ public class FlagsController( await db.SaveChangesAsync(); queue.QueueInvocableWithPayload( - new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Name, req.Image, req.Description) + new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image) ); return Accepted(userRenderer.RenderPrideFlag(flag)); diff --git a/Foxnouns.Backend/Database/Models/User.cs b/Foxnouns.Backend/Database/Models/User.cs index c60430a..12df0ae 100644 --- a/Foxnouns.Backend/Database/Models/User.cs +++ b/Foxnouns.Backend/Database/Models/User.cs @@ -12,6 +12,8 @@ // // 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; @@ -89,5 +91,5 @@ public enum PreferenceSize public class UserSettings { - public bool? DarkMode { get; set; } = null; + public bool? DarkMode { get; set; } } diff --git a/Foxnouns.Backend/Dto/Flag.cs b/Foxnouns.Backend/Dto/Flag.cs index 955d8bd..203442b 100644 --- a/Foxnouns.Backend/Dto/Flag.cs +++ b/Foxnouns.Backend/Dto/Flag.cs @@ -14,6 +14,7 @@ // along with this program. If not, see . // ReSharper disable NotAccessedPositionalProperty.Global +// ReSharper disable UnusedAutoPropertyAccessor.Global using Foxnouns.Backend.Database; using Foxnouns.Backend.Utils; @@ -23,8 +24,6 @@ public record PrideFlagResponse(Snowflake Id, string? ImageUrl, string Name, str public record CreateFlagRequest(string Name, string Image, string? Description); -public record CreateFlagResponse(Snowflake Id, string Name, string? Description); - public class UpdateFlagRequest : PatchRequest { public string? Name { get; init; } diff --git a/Foxnouns.Backend/Dto/Member.cs b/Foxnouns.Backend/Dto/Member.cs index b0c4bfa..4fcc147 100644 --- a/Foxnouns.Backend/Dto/Member.cs +++ b/Foxnouns.Backend/Dto/Member.cs @@ -14,6 +14,7 @@ // 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; diff --git a/Foxnouns.Backend/Dto/User.cs b/Foxnouns.Backend/Dto/User.cs index 0ab9511..a78aeba 100644 --- a/Foxnouns.Backend/Dto/User.cs +++ b/Foxnouns.Backend/Dto/User.cs @@ -15,6 +15,7 @@ // 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; diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 41c9712..d1b1156 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -57,7 +57,7 @@ public static class WebApplicationExtensions if (config.Logging.SeqLogUrl != null) { - logCfg.WriteTo.Seq(config.Logging.SeqLogUrl, LogEventLevel.Verbose); + logCfg.WriteTo.Seq(config.Logging.SeqLogUrl); } // AddSerilog doesn't seem to add an ILogger to the service collection, so add that manually. diff --git a/Foxnouns.Backend/Jobs/Payloads.cs b/Foxnouns.Backend/Jobs/Payloads.cs index 192a6fa..374a5b7 100644 --- a/Foxnouns.Backend/Jobs/Payloads.cs +++ b/Foxnouns.Backend/Jobs/Payloads.cs @@ -18,12 +18,6 @@ namespace Foxnouns.Backend.Jobs; public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); -public record CreateFlagPayload( - Snowflake Id, - Snowflake UserId, - string Name, - string ImageData, - string? Description -); +public record CreateFlagPayload(Snowflake Id, Snowflake UserId, string ImageData); public record CreateDataExportPayload(Snowflake UserId); diff --git a/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs b/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs index 79d86e3..46a0309 100644 --- a/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs +++ b/Foxnouns.Backend/Mailables/PasswordChangedMailable.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs b/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs index 0f89b90..ee06732 100644 --- a/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs +++ b/Foxnouns.Backend/Mailables/ResetPasswordMailable.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 908598a..976dc5b 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -14,7 +14,6 @@ // along with this program. If not, see . using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Utils; -using Microsoft.AspNetCore.Mvc.ViewFeatures; namespace Foxnouns.Backend.Middleware; diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs index beff74a..10a61e4 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.Misskey.cs @@ -14,10 +14,8 @@ // along with this program. If not, see . using System.Net; using System.Text.Json.Serialization; -using System.Web; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Extensions; namespace Foxnouns.Backend.Services.Auth; @@ -120,8 +118,7 @@ public partial class FediverseAuthService private async Task GenerateMisskeyAuthUrlAsync( FediverseApplication app, - bool forceRefresh, - string? state = null + bool forceRefresh ) { if (forceRefresh) diff --git a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs index 250455a..9ca7290 100644 --- a/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs +++ b/Foxnouns.Backend/Services/Auth/FediverseAuthService.cs @@ -131,8 +131,7 @@ public partial class FediverseAuthService ), FediverseInstanceType.MisskeyApi => await GenerateMisskeyAuthUrlAsync( app, - forceRefresh, - state + forceRefresh ), _ => throw new ArgumentOutOfRangeException(nameof(app), app.InstanceType, null), }; diff --git a/Foxnouns.Backend/Services/EmailRateLimiter.cs b/Foxnouns.Backend/Services/EmailRateLimiter.cs index 9e73792..3a1a81a 100644 --- a/Foxnouns.Backend/Services/EmailRateLimiter.cs +++ b/Foxnouns.Backend/Services/EmailRateLimiter.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Services/MemberRendererService.cs b/Foxnouns.Backend/Services/MemberRendererService.cs index 39f73ba..b0efc8d 100644 --- a/Foxnouns.Backend/Services/MemberRendererService.cs +++ b/Foxnouns.Backend/Services/MemberRendererService.cs @@ -17,7 +17,6 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Dto; using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; namespace Foxnouns.Backend.Services; diff --git a/Foxnouns.Backend/Services/MetricsCollectionService.cs b/Foxnouns.Backend/Services/MetricsCollectionService.cs index 30568d5..8de0264 100644 --- a/Foxnouns.Backend/Services/MetricsCollectionService.cs +++ b/Foxnouns.Backend/Services/MetricsCollectionService.cs @@ -38,7 +38,7 @@ public class MetricsCollectionService(ILogger logger, IServiceProvider services, // ReSharper disable once SuggestVarOrType_SimpleTypes await using var db = scope.ServiceProvider.GetRequiredService(); - List? users = await db + List users = await db .Users.Where(u => !u.Deleted) .Select(u => u.LastActive) .ToListAsync(ct); diff --git a/Foxnouns.Backend/Services/ObjectStorageService.cs b/Foxnouns.Backend/Services/ObjectStorageService.cs index abfeafc..5ced7fb 100644 --- a/Foxnouns.Backend/Services/ObjectStorageService.cs +++ b/Foxnouns.Backend/Services/ObjectStorageService.cs @@ -13,7 +13,6 @@ // 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; using Minio.DataModel.Args; using Minio.Exceptions; diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index b5b1c50..028fc75 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -17,8 +17,6 @@ using Foxnouns.Backend.Database.Models; using Foxnouns.Backend.Dto; using Foxnouns.Backend.Utils; using Microsoft.EntityFrameworkCore; -using Newtonsoft.Json; -using NodaTime; namespace Foxnouns.Backend.Services; diff --git a/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs b/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs index 5be20cd..92c1f7c 100644 --- a/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs +++ b/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs @@ -1,3 +1,17 @@ +// 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; diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs index da6c61c..3eed6e2 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Preferences.cs @@ -12,7 +12,6 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Foxnouns.Backend.Controllers; using Foxnouns.Backend.Dto; namespace Foxnouns.Backend.Utils; From 41a008799ac90122de8a7b5ce72f9a508c41b42e Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 14 Dec 2024 16:54:47 +0100 Subject: [PATCH 179/261] update dependencies --- Foxnouns.Backend/Foxnouns.Backend.csproj | 2 +- Foxnouns.Backend/packages.lock.json | 6 +- Foxnouns.Frontend/package.json | 4 +- Foxnouns.Frontend/pnpm-lock.yaml | 80 ++++++++++++------------ package.json | 2 +- 5 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 4ed5c44..168cff6 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index 801e650..5f1b968 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -218,9 +218,9 @@ }, "Scalar.AspNetCore": { "type": "Direct", - "requested": "[1.2.51, )", - "resolved": "1.2.51", - "contentHash": "3eA0doeYYWwLKHUsStHslTmSLmvuEEatmRanb4CJyhBQQOF7u3dVF2Mx7ZAc7b3GBiMfaB/QxYvFdxxOfgRV9Q==" + "requested": "[1.2.55, )", + "resolved": "1.2.55", + "contentHash": "zArlr6nfPQMRwyia0WFirsyczQby51GhNgWITiEIRkot+CVGZSGQ4oWGqExO11/6x26G+mcQo9Oft1mGpN0/ZQ==" }, "Sentry.AspNetCore": { "type": "Direct", diff --git a/Foxnouns.Frontend/package.json b/Foxnouns.Frontend/package.json index a3783fe..b9e35fc 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -28,7 +28,7 @@ "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.2", "sass": "^1.83.0", - "svelte": "^5.12.0", + "svelte": "^5.13.0", "svelte-bootstrap-icons": "^3.1.1", "svelte-check": "^4.1.1", "sveltekit-i18n": "^2.4.2", @@ -36,7 +36,7 @@ "typescript-eslint": "^8.18.0", "vite": "^5.4.11" }, - "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee", + "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c", "dependencies": { "@fontsource/firago": "^5.1.0", "base64-arraybuffer": "^1.0.2", diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml index 45d0d8c..2ebd886 100644 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ b/Foxnouns.Frontend/pnpm-lock.yaml @@ -44,16 +44,16 @@ importers: devDependencies: '@sveltejs/adapter-node': specifier: ^5.2.10 - version: 5.2.10(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))) + version: 5.2.10(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.13.0)(vite@5.4.11(sass@1.83.0))) '@sveltejs/kit': specifier: ^2.11.1 - version: 2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + version: 2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)) '@sveltejs/vite-plugin-svelte': specifier: ^4.0.3 - version: 4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + version: 4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)) '@sveltestrap/sveltestrap': specifier: ^6.2.7 - version: 6.2.7(svelte@5.12.0) + version: 6.2.7(svelte@5.13.0) '@types/eslint': specifier: ^9.6.1 version: 9.6.1 @@ -77,7 +77,7 @@ importers: version: 9.1.0(eslint@9.17.0) eslint-plugin-svelte: specifier: ^2.46.1 - version: 2.46.1(eslint@9.17.0)(svelte@5.12.0) + version: 2.46.1(eslint@9.17.0)(svelte@5.13.0) globals: specifier: ^15.13.0 version: 15.13.0 @@ -86,22 +86,22 @@ importers: version: 3.4.2 prettier-plugin-svelte: specifier: ^3.3.2 - version: 3.3.2(prettier@3.4.2)(svelte@5.12.0) + version: 3.3.2(prettier@3.4.2)(svelte@5.13.0) sass: specifier: ^1.83.0 version: 1.83.0 svelte: - specifier: ^5.12.0 - version: 5.12.0 + specifier: ^5.13.0 + version: 5.13.0 svelte-bootstrap-icons: specifier: ^3.1.1 version: 3.1.1 svelte-check: specifier: ^4.1.1 - version: 4.1.1(picomatch@4.0.2)(svelte@5.12.0)(typescript@5.7.2) + version: 4.1.1(picomatch@4.0.2)(svelte@5.13.0)(typescript@5.7.2) sveltekit-i18n: specifier: ^2.4.2 - version: 2.4.2(svelte@5.12.0) + version: 2.4.2(svelte@5.13.0) typescript: specifier: ^5.7.2 version: 5.7.2 @@ -1342,8 +1342,8 @@ packages: svelte-tippy@1.3.2: resolution: {integrity: sha512-41f+85hwhKBRqX0UNYrgFsi34Kk/KDvUkIZXYANxkWoA2NTVTCZbUC2J8hRNZ4TRVxObTshoZRjK2co5+i6LMw==} - svelte@5.12.0: - resolution: {integrity: sha512-nOd7uj0D/4A3IrHnltaFYndVPGViYSs0s+Zi3N4uQg3owJt9RoiUdwxYx8qjorj5CtaGsx8dNYsFVbH6czrGNg==} + svelte@5.13.0: + resolution: {integrity: sha512-ZG4VmBNze/j2KxT2GEeUm8Jr3RLYQ3P5Y9/flUDCgaAxgzx4ZRTdiyh+PCr7qRlOr5M8uidIqr+3DwUFVrdL+A==} engines: {node: '>=18'} sveltekit-i18n@2.4.2: @@ -1778,17 +1778,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.28.1': optional: true - '@sveltejs/adapter-node@5.2.10(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))': + '@sveltejs/adapter-node@5.2.10(@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))': dependencies: '@rollup/plugin-commonjs': 28.0.1(rollup@4.28.1) '@rollup/plugin-json': 6.1.0(rollup@4.28.1) '@rollup/plugin-node-resolve': 15.3.0(rollup@4.28.1) - '@sveltejs/kit': 2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + '@sveltejs/kit': 2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)) rollup: 4.28.1 - '@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))': + '@sveltejs/kit@2.11.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.13.0)(vite@5.4.11(sass@1.83.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + '@sveltejs/vite-plugin-svelte': 4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 @@ -1800,42 +1800,42 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.1 sirv: 3.0.0 - svelte: 5.12.0 + svelte: 5.13.0 tiny-glob: 0.2.9 vite: 5.4.11(sass@1.83.0) - '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))': + '@sveltejs/vite-plugin-svelte-inspector@3.0.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.13.0)(vite@5.4.11(sass@1.83.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + '@sveltejs/vite-plugin-svelte': 4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)) debug: 4.4.0 - svelte: 5.12.0 + svelte: 5.13.0 vite: 5.4.11(sass@1.83.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0))': + '@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.12.0)(vite@5.4.11(sass@1.83.0)) + '@sveltejs/vite-plugin-svelte-inspector': 3.0.1(@sveltejs/vite-plugin-svelte@4.0.3(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)))(svelte@5.13.0)(vite@5.4.11(sass@1.83.0)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.15 - svelte: 5.12.0 + svelte: 5.13.0 vite: 5.4.11(sass@1.83.0) vitefu: 1.0.4(vite@5.4.11(sass@1.83.0)) transitivePeerDependencies: - supports-color - '@sveltekit-i18n/base@1.3.7(svelte@5.12.0)': + '@sveltekit-i18n/base@1.3.7(svelte@5.13.0)': dependencies: - svelte: 5.12.0 + svelte: 5.13.0 '@sveltekit-i18n/parser-default@1.1.1': {} - '@sveltestrap/sveltestrap@6.2.7(svelte@5.12.0)': + '@sveltestrap/sveltestrap@6.2.7(svelte@5.13.0)': dependencies: '@popperjs/core': 2.11.8 - svelte: 5.12.0 + svelte: 5.13.0 '@types/cookie@0.6.0': {} @@ -2093,7 +2093,7 @@ snapshots: dependencies: eslint: 9.17.0 - eslint-plugin-svelte@2.46.1(eslint@9.17.0)(svelte@5.12.0): + eslint-plugin-svelte@2.46.1(eslint@9.17.0)(svelte@5.13.0): dependencies: '@eslint-community/eslint-utils': 4.4.1(eslint@9.17.0) '@jridgewell/sourcemap-codec': 1.5.0 @@ -2106,9 +2106,9 @@ snapshots: postcss-safe-parser: 6.0.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 semver: 7.6.3 - svelte-eslint-parser: 0.43.0(svelte@5.12.0) + svelte-eslint-parser: 0.43.0(svelte@5.13.0) optionalDependencies: - svelte: 5.12.0 + svelte: 5.13.0 transitivePeerDependencies: - ts-node @@ -2462,10 +2462,10 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.12.0): + prettier-plugin-svelte@3.3.2(prettier@3.4.2)(svelte@5.13.0): dependencies: prettier: 3.4.2 - svelte: 5.12.0 + svelte: 5.13.0 prettier@3.4.2: {} @@ -2567,19 +2567,19 @@ snapshots: svelte-bootstrap-icons@3.1.1: {} - svelte-check@4.1.1(picomatch@4.0.2)(svelte@5.12.0)(typescript@5.7.2): + svelte-check@4.1.1(picomatch@4.0.2)(svelte@5.13.0)(typescript@5.7.2): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 4.0.1 fdir: 6.4.2(picomatch@4.0.2) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.12.0 + svelte: 5.13.0 typescript: 5.7.2 transitivePeerDependencies: - picomatch - svelte-eslint-parser@0.43.0(svelte@5.12.0): + svelte-eslint-parser@0.43.0(svelte@5.13.0): dependencies: eslint-scope: 7.2.2 eslint-visitor-keys: 3.4.3 @@ -2587,13 +2587,13 @@ snapshots: postcss: 8.4.49 postcss-scss: 4.0.9(postcss@8.4.49) optionalDependencies: - svelte: 5.12.0 + svelte: 5.13.0 svelte-tippy@1.3.2: dependencies: tippy.js: 6.3.7 - svelte@5.12.0: + svelte@5.13.0: dependencies: '@ampproject/remapping': 2.3.0 '@jridgewell/sourcemap-codec': 1.5.0 @@ -2609,11 +2609,11 @@ snapshots: magic-string: 0.30.15 zimmerframe: 1.1.2 - sveltekit-i18n@2.4.2(svelte@5.12.0): + sveltekit-i18n@2.4.2(svelte@5.13.0): dependencies: - '@sveltekit-i18n/base': 1.3.7(svelte@5.12.0) + '@sveltekit-i18n/base': 1.3.7(svelte@5.13.0) '@sveltekit-i18n/parser-default': 1.1.1 - svelte: 5.12.0 + svelte: 5.13.0 tiny-glob@0.2.9: dependencies: diff --git a/package.json b/package.json index 60f2ca5..2d79864 100644 --- a/package.json +++ b/package.json @@ -7,5 +7,5 @@ "dev": "concurrently -n .net,node,rate -c magenta,yellow,blue -i 'pnpm watch:be' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", "format": "dotnet csharpier . && cd Foxnouns.Frontend && pnpm format" }, - "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" + "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" } From 507b9c3f4cc27f8aec9cab8e8dece854c5cd9863 Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 15 Dec 2024 00:32:11 +0100 Subject: [PATCH 180/261] feat(frontend): custom preference editor --- .../src/lib/components/IconButton.svelte | 18 +++- .../editor/CustomPreferenceEditor.svelte | 41 ++++++++ .../editor/PreferenceIconSelector.svelte | 80 +++++++++++++++ .../editor/PreferenceSizeEditor.svelte | 43 ++++++++ .../src/lib/i18n/locales/en.json | 16 ++- .../src/routes/settings/+layout.svelte | 6 ++ .../src/routes/settings/flags/+page.svelte | 2 +- .../routes/settings/members/[id]/+page.svelte | 2 +- .../src/routes/settings/prefs/+page.svelte | 99 +++++++++++++++++++ .../routes/settings/profile/bio/+page.svelte | 3 +- 10 files changed, 302 insertions(+), 8 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/editor/CustomPreferenceEditor.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/PreferenceIconSelector.svelte create mode 100644 Foxnouns.Frontend/src/lib/components/editor/PreferenceSizeEditor.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/prefs/+page.svelte diff --git a/Foxnouns.Frontend/src/lib/components/IconButton.svelte b/Foxnouns.Frontend/src/lib/components/IconButton.svelte index 1feedd9..a370002 100644 --- a/Foxnouns.Frontend/src/lib/components/IconButton.svelte +++ b/Foxnouns.Frontend/src/lib/components/IconButton.svelte @@ -11,15 +11,29 @@ id?: string; onclick?: MouseEventHandler; outline?: boolean; + active?: boolean; + class?: string; }; - let { icon, tooltip, color = "primary", type, id, onclick, outline }: Props = $props(); + let { + icon, + tooltip, + color = "primary", + type, + id, + onclick, + outline, + active, + class: className, + }: Props = $props();

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

    -
    +
    diff --git a/Foxnouns.Frontend/src/routes/settings/prefs/+page.svelte b/Foxnouns.Frontend/src/routes/settings/prefs/+page.svelte new file mode 100644 index 0000000..2cc4f6a --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/prefs/+page.svelte @@ -0,0 +1,99 @@ + + +

    + {$t("settings.custom-preferences-title")} +
    + + +
    +

    + + + +
    + {#each customPreferences as _, idx} + remove(idx)} /> + {/each} +
    + +
    diff --git a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte index 4ec8717..19e04fb 100644 --- a/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte +++ b/Foxnouns.Frontend/src/routes/settings/profile/bio/+page.svelte @@ -3,7 +3,6 @@ import type { ActionData, PageData } from "./$types"; import BioEditor from "$components/editor/BioEditor.svelte"; import { t } from "$lib/i18n"; - import { enhance } from "$app/forms"; type Props = { data: PageData; form: ActionData }; let { data, form }: Props = $props(); @@ -13,6 +12,6 @@

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

    -
    + From b36b54f9e6f78501b7c0e20fabe5c29dff1c0cac Mon Sep 17 00:00:00 2001 From: sam Date: Sun, 15 Dec 2024 01:12:31 +0100 Subject: [PATCH 181/261] docker: expose metrics and internal API --- DOCKER.md | 3 +++ docker-compose.yml | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/DOCKER.md b/DOCKER.md index 5cbb6e6..b485eb7 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -8,3 +8,6 @@ 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/docker-compose.yml b/docker-compose.yml index 5606760..4fc94bb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,11 @@ services: - "Database:EnablePooling=true" - "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 From 79b8c4799ee528a73490f37f5cc1b40ff71f66a7 Mon Sep 17 00:00:00 2001 From: sam Date: Mon, 16 Dec 2024 21:38:38 +0100 Subject: [PATCH 182/261] feat: new migrator --- .../Utils/ValidationUtils.Fields.cs | 2 +- .../Foxnouns.DataMigrator.csproj | 20 ++ Foxnouns.DataMigrator/GoDatabase.cs | 71 +++++ Foxnouns.DataMigrator/Models/GoMember.cs | 26 ++ Foxnouns.DataMigrator/Models/GoUser.cs | 102 +++++++ Foxnouns.DataMigrator/Program.cs | 101 +++++++ Foxnouns.DataMigrator/Queries.cs | 34 +++ Foxnouns.DataMigrator/UserMigrator.cs | 261 ++++++++++++++++ Foxnouns.NET.sln | 10 +- migrators/NetImporter/ImportUser.cs | 192 ------------ migrators/NetImporter/NetImporter.cs | 89 ------ migrators/NetImporter/NetImporter.csproj | 26 -- migrators/go-exporter/.gitignore | 1 - migrators/go-exporter/go.mod | 82 ----- migrators/go-exporter/go.sum | 286 ------------------ migrators/go-exporter/main.go | 101 ------- migrators/go-exporter/user.go | 134 -------- 17 files changed, 621 insertions(+), 917 deletions(-) create mode 100644 Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj create mode 100644 Foxnouns.DataMigrator/GoDatabase.cs create mode 100644 Foxnouns.DataMigrator/Models/GoMember.cs create mode 100644 Foxnouns.DataMigrator/Models/GoUser.cs create mode 100644 Foxnouns.DataMigrator/Program.cs create mode 100644 Foxnouns.DataMigrator/Queries.cs create mode 100644 Foxnouns.DataMigrator/UserMigrator.cs delete mode 100644 migrators/NetImporter/ImportUser.cs delete mode 100644 migrators/NetImporter/NetImporter.cs delete mode 100644 migrators/NetImporter/NetImporter.csproj delete mode 100644 migrators/go-exporter/.gitignore delete mode 100644 migrators/go-exporter/go.mod delete mode 100644 migrators/go-exporter/go.sum delete mode 100644 migrators/go-exporter/main.go delete mode 100644 migrators/go-exporter/user.go diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs index 8a7cad5..0235eb6 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs @@ -19,7 +19,7 @@ namespace Foxnouns.Backend.Utils; public static partial class ValidationUtils { - private static readonly string[] DefaultStatusOptions = + public static readonly string[] DefaultStatusOptions = [ "favourite", "okay", diff --git a/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj b/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj new file mode 100644 index 0000000..5fde110 --- /dev/null +++ b/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj @@ -0,0 +1,20 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + diff --git a/Foxnouns.DataMigrator/GoDatabase.cs b/Foxnouns.DataMigrator/GoDatabase.cs new file mode 100644 index 0000000..a2f7720 --- /dev/null +++ b/Foxnouns.DataMigrator/GoDatabase.cs @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..d1c52fe --- /dev/null +++ b/Foxnouns.DataMigrator/Models/GoMember.cs @@ -0,0 +1,26 @@ +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 new file mode 100644 index 0000000..20f5d51 --- /dev/null +++ b/Foxnouns.DataMigrator/Models/GoUser.cs @@ -0,0 +1,102 @@ +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 new file mode 100644 index 0000000..307cda5 --- /dev/null +++ b/Foxnouns.DataMigrator/Program.cs @@ -0,0 +1,101 @@ +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 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(); + + 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(); + + Log.Information("Migrating applications"); + Dictionary appIds = await MigrateAppsAsync(conn, context); + + Log.Information("Migrating users"); + List users = await Queries.GetUsersAsync(conn); + 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!"); + } + + 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(), + } + ); + } + + return appIds; + } +} diff --git a/Foxnouns.DataMigrator/Queries.cs b/Foxnouns.DataMigrator/Queries.cs new file mode 100644 index 0000000..0d6e71f --- /dev/null +++ b/Foxnouns.DataMigrator/Queries.cs @@ -0,0 +1,34 @@ +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) => + (await conn.QueryAsync("select * from users order by id")).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 new file mode 100644 index 0000000..0263c47 --- /dev/null +++ b/Foxnouns.DataMigrator/UserMigrator.cs @@ -0,0 +1,261 @@ +using System.Diagnostics.CodeAnalysis; +using Dapper; +using Foxnouns.Backend.Database; +using Foxnouns.Backend.Database.Models; +using Foxnouns.Backend.Utils; +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, + 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, + 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, + 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, + }, + }; + } + + 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 ValidationUtils.DefaultStatusOptions.Contains(id) ? id : "okay"; + } +} diff --git a/Foxnouns.NET.sln b/Foxnouns.NET.sln index aec8ae7..de091c2 100644 --- a/Foxnouns.NET.sln +++ b/Foxnouns.NET.sln @@ -5,7 +5,7 @@ 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}") = "NetImporter", "migrators\NetImporter\NetImporter.csproj", "{FBCF80EE-624F-43AF-8122-230B5447940C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Foxnouns.DataMigrator", "Foxnouns.DataMigrator\Foxnouns.DataMigrator.csproj", "{1909CCB6-F9B0-4C36-9618-82CC3A41C5B0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -20,9 +20,9 @@ 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 - {FBCF80EE-624F-43AF-8122-230B5447940C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FBCF80EE-624F-43AF-8122-230B5447940C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FBCF80EE-624F-43AF-8122-230B5447940C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FBCF80EE-624F-43AF-8122-230B5447940C}.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/migrators/NetImporter/ImportUser.cs b/migrators/NetImporter/ImportUser.cs deleted file mode 100644 index d1c264a..0000000 --- a/migrators/NetImporter/ImportUser.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using Foxnouns.Backend.Database; -using Foxnouns.Backend.Database.Models; -using Microsoft.EntityFrameworkCore; -using NodaTime; -using NodaTime.Extensions; -using Serilog; - -namespace NetImporter; - -public static class Users -{ - public static async Task ImportUsers(string filename) - { - await using var db = await NetImporter.GetContextAsync(); - await db.Database.ExecuteSqlRawAsync("SELECT 1"); - - var stopwatch = new Stopwatch(); - stopwatch.Start(); - - var users = NetImporter - .ReadFromFile(filename) - .Output.Select(ConvertUser) - .ToList(); - db.AddRange(users); - await db.SaveChangesAsync(); - - stopwatch.Stop(); - Log.Information( - "Imported {Count} users in {Duration}", - users.Count, - stopwatch.ElapsedDuration() - ); - } - - private static User ConvertUser(ImportUser oldUser) - { - var user = new User - { - Id = oldUser.Id, - Username = oldUser.Username, - DisplayName = oldUser.DisplayName, - Bio = oldUser.Bio, - MemberTitle = oldUser.MemberTitle, - LastActive = oldUser.LastActive.ToInstant(), - Avatar = oldUser.AvatarHash, - Links = oldUser.Links ?? [], - - Role = oldUser.ParseRole(), - Deleted = oldUser.Deleted, - DeletedAt = oldUser.DeletedAt?.ToInstant(), - DeletedBy = null, - }; - - if (oldUser is { DiscordId: not null, DiscordUsername: not null }) - { - user.AuthMethods.Add( - new AuthMethod - { - Id = SnowflakeGenerator.Instance.GenerateSnowflake(), - AuthType = AuthType.Discord, - RemoteId = oldUser.DiscordId, - RemoteUsername = oldUser.DiscordUsername, - } - ); - } - - if (oldUser is { TumblrId: not null, TumblrUsername: not null }) - { - user.AuthMethods.Add( - new AuthMethod - { - Id = SnowflakeGenerator.Instance.GenerateSnowflake(), - AuthType = AuthType.Tumblr, - RemoteId = oldUser.TumblrId, - RemoteUsername = oldUser.TumblrUsername, - } - ); - } - - if (oldUser is { GoogleId: not null, GoogleUsername: not null }) - { - user.AuthMethods.Add( - new AuthMethod - { - Id = SnowflakeGenerator.Instance.GenerateSnowflake(), - AuthType = AuthType.Google, - RemoteId = oldUser.GoogleId, - RemoteUsername = oldUser.GoogleUsername, - } - ); - } - - // Convert all custom preference UUIDs to snowflakes - var prefMapping = new Dictionary(); - foreach (var (key, value) in oldUser.CustomPreferences) - { - var newKey = SnowflakeGenerator.Instance.GenerateSnowflake(); - prefMapping[key] = newKey; - user.CustomPreferences[newKey] = value; - } - - foreach (var name in oldUser.Names ?? []) - { - user.Names.Add( - new FieldEntry - { - Value = name.Value, - Status = prefMapping.TryGetValue(name.Status, out var newStatus) - ? newStatus.ToString() - : name.Status, - } - ); - } - - foreach (var pronoun in oldUser.Pronouns ?? []) - { - user.Pronouns.Add( - new Pronoun - { - Value = pronoun.Value, - DisplayText = pronoun.DisplayText, - Status = prefMapping.TryGetValue(pronoun.Status, out var newStatus) - ? newStatus.ToString() - : pronoun.Status, - } - ); - } - - foreach (var field in oldUser.Fields ?? []) - { - var entries = field - .Entries.Select(entry => new FieldEntry - { - Value = entry.Value, - Status = prefMapping.TryGetValue(entry.Status, out var newStatus) - ? newStatus.ToString() - : entry.Status, - }) - .ToList(); - - user.Fields.Add(new Field { Name = field.Name, Entries = entries.ToArray() }); - } - - Log.Debug("Converted user {UserId}", oldUser.Id); - - return user; - } - - [SuppressMessage("ReSharper", "ClassNeverInstantiated.Local")] - private record ImportUser( - Snowflake Id, - string Sid, - string Username, - string? DisplayName, - string? Bio, - string? MemberTitle, - OffsetDateTime LastActive, - string? AvatarHash, - string[]? Links, - FieldEntry[]? Names, - Pronoun[]? Pronouns, - Field[]? Fields, - string? DiscordId, - string? DiscordUsername, - string? FediverseId, - string? FediverseUsername, - long? FediverseAppId, - string? TumblrId, - string? TumblrUsername, - string? GoogleId, - string? GoogleUsername, - bool MemberListHidden, - string? Timezone, - string Role, - bool Deleted, - OffsetDateTime? DeletedAt, - string? DeleteReason, - Dictionary CustomPreferences - ) - { - public UserRole ParseRole() => - Role switch - { - "USER" => UserRole.User, - "MODERATOR" => UserRole.Moderator, - "ADMIN" => UserRole.Admin, - _ => UserRole.User, - }; - } -} diff --git a/migrators/NetImporter/NetImporter.cs b/migrators/NetImporter/NetImporter.cs deleted file mode 100644 index 7a0ddf6..0000000 --- a/migrators/NetImporter/NetImporter.cs +++ /dev/null @@ -1,89 +0,0 @@ -using Foxnouns.Backend; -using Foxnouns.Backend.Database; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using NodaTime; -using NodaTime.Serialization.JsonNet; -using Serilog; -using Serilog.Events; - -namespace NetImporter; - -internal static class NetImporter -{ - public static async Task Main(string[] args) - { - Log.Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override( - "Microsoft.EntityFrameworkCore.Database.Command", - LogEventLevel.Information - ) - .WriteTo.Console() - .CreateLogger(); - - switch (args.Length) - { - case < 2: - Console.WriteLine("Not enough arguments. Usage: "); - return; - case > 2: - Console.WriteLine("Too many arguments. Usage: "); - return; - } - - switch (args[0].ToLowerInvariant()) - { - case "users": - await Users.ImportUsers(args[1]); - break; - default: - Console.WriteLine("Invalid type. Valid types are: users"); - break; - } - } - - internal static async Task GetContextAsync() - { - var connString = Environment.GetEnvironmentVariable("DATABASE"); - if (connString == null) - throw new Exception("$DATABASE not set, must be an ADO.NET connection string"); - - var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger); - var config = new Config { Database = new Config.DatabaseConfig { Url = connString } }; - - var dataSource = DatabaseContext.BuildDataSource(config); - var options = DatabaseContext - .BuildOptions(new DbContextOptionsBuilder(), dataSource, loggerFactory) - .Options; - var db = new DatabaseContext(options); - - if ((await db.Database.GetPendingMigrationsAsync()).Any()) - { - Log.Fatal("Database needs to be migrated first"); - } - - return db; - } - - private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings - { - ContractResolver = new DefaultContractResolver - { - NamingStrategy = new SnakeCaseNamingStrategy(), - }, - }.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb); - - internal static Input ReadFromFile(string path) - { - var data = File.ReadAllText(path); - return JsonConvert.DeserializeObject>(data, Settings) - ?? throw new Exception("Invalid input file"); - } -} - -internal record Input(List Output, List Skipped); diff --git a/migrators/NetImporter/NetImporter.csproj b/migrators/NetImporter/NetImporter.csproj deleted file mode 100644 index d27f214..0000000 --- a/migrators/NetImporter/NetImporter.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - Exe - net9.0 - enable - enable - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - diff --git a/migrators/go-exporter/.gitignore b/migrators/go-exporter/.gitignore deleted file mode 100644 index 94a2dd1..0000000 --- a/migrators/go-exporter/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.json \ No newline at end of file diff --git a/migrators/go-exporter/go.mod b/migrators/go-exporter/go.mod deleted file mode 100644 index aaadec0..0000000 --- a/migrators/go-exporter/go.mod +++ /dev/null @@ -1,82 +0,0 @@ -module code.vulpine.solutions/sam/Foxnouns.NET/go-exporter - -go 1.22.6 - -require ( - cloud.google.com/go/compute v1.23.2 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - codeberg.org/pronounscc/pronouns.cc v0.6.5-0.20240213162950-5fcd87a94a07 // indirect - emperror.dev/errors v0.8.1 // indirect - github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf // indirect - github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec // indirect - github.com/ajg/form v1.5.1 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bwmarrin/discordgo v0.27.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect - github.com/davidbyttow/govips/v2 v2.13.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/georgysavva/scany/v2 v2.0.0 // indirect - github.com/getsentry/sentry-go v0.25.0 // indirect - github.com/go-chi/chi/v5 v5.0.10 // indirect - github.com/go-chi/cors v1.2.1 // indirect - github.com/go-chi/httprate v0.7.4 // indirect - github.com/go-chi/render v1.0.3 // indirect - github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/gobwas/glob v0.2.3 // indirect - github.com/golang-jwt/jwt/v4 v4.5.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.4.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.4.3 // indirect - github.com/jackc/puddle/v2 v2.2.1 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.17.2 // indirect - github.com/klauspost/cpuid/v2 v2.2.5 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect - github.com/mediocregopher/radix/v4 v4.1.4 // indirect - github.com/minio/md5-simd v1.1.2 // indirect - github.com/minio/minio-go/v7 v7.0.63 // indirect - github.com/minio/sha256-simd v1.0.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/prometheus/client_golang v1.17.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/rs/xid v1.5.0 // indirect - github.com/rubenv/sql-migrate v1.5.2 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/tilinna/clock v1.1.0 // indirect - github.com/toshi0607/chi-prometheus v0.1.4 // indirect - github.com/urfave/cli/v2 v2.25.7 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect - go.opencensus.io v0.24.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.26.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/image v0.13.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect - google.golang.org/api v0.148.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a // indirect - google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/migrators/go-exporter/go.sum b/migrators/go-exporter/go.sum deleted file mode 100644 index 933780c..0000000 --- a/migrators/go-exporter/go.sum +++ /dev/null @@ -1,286 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/compute v1.23.2 h1:nWEMDhgbBkBJjfpVySqU4jgWdc22PLR0o4vEexZHers= -cloud.google.com/go/compute v1.23.2/go.mod h1:JJ0atRC0J/oWYiiVBmsSsrRnh92DhZPG4hFDcR04Rns= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -codeberg.org/pronounscc/pronouns.cc v0.6.5-0.20240213162950-5fcd87a94a07 h1:Vsf1xjT8msCuNj3KbDsqs7QR614WJgYFYc6+cMsx8zM= -codeberg.org/pronounscc/pronouns.cc v0.6.5-0.20240213162950-5fcd87a94a07/go.mod h1:Y4HFkHFeIGoK+CAkig0bUxRTUm5Eprr7oP3mpVizUi0= -emperror.dev/errors v0.8.1 h1:UavXZ5cSX/4u9iyvH6aDcuGkVjeexUGJ7Ij7G4VfQT0= -emperror.dev/errors v0.8.1/go.mod h1:YcRvLPh626Ubn2xqtoprejnA5nFha+TJ+2vew48kWuE= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf h1:+edM69bH/X6JpYPmJYBRLanAMe1V5yRXYU3hHUovGcE= -github.com/aarondl/json v0.0.0-20221020222930-8b0db17ef1bf/go.mod h1:FZqLhJSj2tg0ZN48GB1zvj00+ZYcHPqgsC7yzcgCq6k= -github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec h1:2NFk5fe52cHyRcUnXSs4CSEAqm+rL/hr3AdflBE3VPU= -github.com/aarondl/opt v0.0.0-20230313190023-85d93d668fec/go.mod h1:l4/5NZtYd/SIohsFhaJQQe+sPOTG22furpZ5FvcYOzk= -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= -github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davidbyttow/govips/v2 v2.13.0 h1:5MK9ZcXZC5GzUR9Ca8fJwOYqMgll/H096ec0PJP59QM= -github.com/davidbyttow/govips/v2 v2.13.0/go.mod h1:LPTrwWtNa5n4yl9UC52YBOEGdZcY5hDTP4Ms2QWasTw= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/georgysavva/scany/v2 v2.0.0 h1:RGXqxDv4row7/FYoK8MRXAZXqoWF/NM+NP0q50k3DKU= -github.com/georgysavva/scany/v2 v2.0.0/go.mod h1:sigOdh+0qb/+aOs3TVhehVT10p8qJL7K/Zhyz8vWo38= -github.com/getsentry/sentry-go v0.25.0 h1:q6Eo+hS+yoJlTO3uu/azhQadsD8V+jQn2D8VvX1eOyI= -github.com/getsentry/sentry-go v0.25.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= -github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= -github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= -github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= -github.com/go-chi/httprate v0.7.4 h1:a2GIjv8he9LRf3712zxxnRdckQCm7I8y8yQhkJ84V6M= -github.com/go-chi/httprate v0.7.4/go.mod h1:6GOYBSwnpra4CQfAKXu8sQZg+nZ0M1g9QnyFvxrAB8A= -github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= -github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= -github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= -github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= -github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -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-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= -github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= -github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= -github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4= -github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= -github.com/mediocregopher/radix/v4 v4.1.4 h1:Uze6DEbEAvL+VHXUEu/EDBTkUk5CLct5h3nVSGpc6Ts= -github.com/mediocregopher/radix/v4 v4.1.4/go.mod h1:ajchozX/6ELmydxWeWM6xCFHVpZ4+67LXHOTOVR0nCE= -github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= -github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.63 h1:GbZ2oCvaUdgT5640WJOpyDhhDxvknAJU2/T3yurwcbQ= -github.com/minio/minio-go/v7 v7.0.63/go.mod h1:Q6X7Qjb7WMhvG65qKf4gUgA5XaiSox74kR1uAEjxRS4= -github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= -github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= -github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= -github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/tilinna/clock v1.0.2/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao= -github.com/tilinna/clock v1.1.0 h1:6IQQQCo6KoBxVudv6gwtY8o4eDfhHo8ojA5dP0MfhSs= -github.com/tilinna/clock v1.1.0/go.mod h1:ZsP7BcY7sEEz7ktc0IVy8Us6boDrK8VradlKRUGfOao= -github.com/toshi0607/chi-prometheus v0.1.4 h1:5KpqJrmdvMvbfU0JiL9ghOTbe8S9sgHDCCQvXgnyoJo= -github.com/toshi0607/chi-prometheus v0.1.4/go.mod h1:E++tBjqpDsvGWjLYdcFd5rvqJ7HG8wwBux+M6gyIL/Q= -github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= -github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= -github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= -go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4= -golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= -golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.148.0 h1:HBq4TZlN4/1pNcu0geJZ/Q50vIwIXT532UIMYoo0vOs= -google.golang.org/api v0.148.0/go.mod h1:8/TBgwaKjfqTdacOJrOv2+2Q6fBDU1uHKK06oGSkxzU= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a h1:a2MQQVoTo96JC9PMGtGBymLp7+/RzpFc2yX/9WfFg1c= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231012201019-e917dd12ba7a/go.mod h1:4cYg8o5yUbm77w8ZX00LhMVNl/YVBFJRYWDc0uYWMs0= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -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= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/migrators/go-exporter/main.go b/migrators/go-exporter/main.go deleted file mode 100644 index ac749ac..0000000 --- a/migrators/go-exporter/main.go +++ /dev/null @@ -1,101 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "log" - "os" - "time" - - "codeberg.org/pronounscc/pronouns.cc/backend/db" - "github.com/Masterminds/squirrel" - "github.com/jackc/pgx/v5/pgxpool" -) - -var exportWhat = flag.String("export", "", "What to export") -var exportLimit = flag.Int64("limit", 0, "Number of items to export for testing") - -var sq = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) -var oldDB *db.DB - -type Output struct { - Output any `json:"output"` // The output array - Skipped []string `json:"skipped"` // IDs of skipped items -} - -func main() { - flag.Parse() - - dbUrl := os.Getenv("DATABASE") - - pool, err := pgxpool.New(context.Background(), dbUrl) - if err != nil { - log.Fatalf("creating database pool: %v\n", err) - } - - oldDB = &db.DB{ - Pool: pool, - } - - switch *exportWhat { - case "users": - exportUsers() - default: - fmt.Printf("invalid export type %q\nvalid export types are: users\n", *exportWhat) - } -} - -func exportUsers() { - filename := fmt.Sprintf("users-output-%v.json", time.Now().Unix()) - f, err := os.Create(filename) - if err != nil { - log.Fatalf("error opening output file %q: %v\n", filename, err) - } - defer f.Close() - - users, err := getUsers() - if err != nil { - log.Fatalf("getting users from database: %v\n", err) - } - - log.Println("converting users") - - start := time.Now() - - var ( - newUsers []NewUser - skipped []string - ) - for i, u := range users { - newUser, err := userToNewUser(u) - if err != nil { - log.Printf("error converting user %v: %v\n", u.SnowflakeID, err) - skipped = append(skipped, u.SnowflakeID.String()) - continue - } - - newUsers = append(newUsers, newUser) - log.Printf("converted user %7d (%v)\n", i+1, u.SnowflakeID) - } - - log.Printf("converted users in %v (skipped: %v)\n", time.Since(start).Round(time.Millisecond), len(skipped)) - - b, err := json.MarshalIndent(Output{Output: newUsers, Skipped: skipped}, "", " ") - if err != nil { - log.Fatalf("error marshaling json: %v\n", err) - } - - _, err = f.Write(b) - if err != nil { - log.Fatalf("writing file: %v\n", err) - } - - err = f.Sync() - if err != nil { - log.Fatalf("syncing file: %v\n", err) - } - - fmt.Printf("\n\nexported %v users! filename: %q\n", len(newUsers), filename) -} diff --git a/migrators/go-exporter/user.go b/migrators/go-exporter/user.go deleted file mode 100644 index 1af246e..0000000 --- a/migrators/go-exporter/user.go +++ /dev/null @@ -1,134 +0,0 @@ -package main - -import ( - "context" - "time" - - "codeberg.org/pronounscc/pronouns.cc/backend/db" - "emperror.dev/errors" - "github.com/georgysavva/scany/v2/pgxscan" -) - -func getUsers() (u []db.User, err error) { - q := sq.Select("*", "(SELECT instance FROM fediverse_apps WHERE id = users.fediverse_app_id) AS fediverse_instance"). - From("users").OrderBy("snowflake_id ASC") - - if *exportLimit != 0 { - q = q.Limit(uint64(*exportLimit)) - } - - sql, args, err := q.ToSql() - if err != nil { - return u, errors.Wrap(err, "building sql") - } - - err = pgxscan.Select(context.Background(), oldDB, &u, sql, args...) - if err != nil { - return u, errors.Wrap(err, "getting users from db") - } - - return u, nil -} - -func userToNewUser(u db.User) (NewUser, error) { - fields, err := oldDB.UserFields(context.Background(), u.ID) - if err != nil { - return NewUser{}, errors.Wrap(err, "getting fields") - } - - new := NewUser{ - ID: u.SnowflakeID.String(), - SID: u.SID, - Username: u.Username, - DisplayName: u.DisplayName, - Bio: u.Bio, - MemberTitle: u.MemberTitle, - LastActive: u.LastActive, - Avatar: u.Avatar, - Links: u.Links, - Names: u.Names, - - Discord: u.Discord, - DiscordUsername: u.DiscordUsername, - Fediverse: u.Fediverse, - FediverseUsername: u.FediverseUsername, - FediverseAppID: u.FediverseAppID, - Tumblr: u.Tumblr, - TumblrUsername: u.TumblrUsername, - Google: u.Google, - GoogleUsername: u.GoogleUsername, - - MemberListHidden: u.ListPrivate, - Timezone: u.Timezone, - Role: "USER", - - Deleted: u.DeletedAt != nil, - DeletedAt: u.DeletedAt, - DeleteReason: u.DeleteReason, - - CustomPreferences: u.CustomPreferences, - } - - if u.IsAdmin { - new.Role = "ADMIN" - } - - for _, p := range u.Pronouns { - new.Pronouns = append(new.Pronouns, NewPronoun{Value: p.Pronouns, Status: string(p.Status), DisplayText: p.DisplayText}) - } - - for _, f := range fields { - new.Fields = append(new.Fields, NewField{Name: f.Name, Entries: f.Entries}) - } - - return new, nil -} - -type NewUser struct { - ID string `json:"id"` - SID string `json:"sid"` - Username string `json:"username"` - DisplayName *string `json:"display_name"` - Bio *string `json:"bio"` - MemberTitle *string `json:"member_title"` - LastActive time.Time `json:"last_active"` - Avatar *string `json:"avatar_hash"` - Links []string `json:"links"` - Names []db.FieldEntry `json:"names"` - Pronouns []NewPronoun `json:"pronouns"` - Fields []NewField `json:"fields"` - - Discord *string `json:"discord_id"` - DiscordUsername *string `json:"discord_username"` - - Fediverse *string `json:"fediverse_id"` - FediverseUsername *string `json:"fediverse_username"` - FediverseAppID *int64 `json:"fediverse_app_id"` - - Tumblr *string `json:"tumblr_id"` - TumblrUsername *string `json:"tumblr_username"` - - Google *string `json:"google_id"` - GoogleUsername *string `json:"google_username"` - - MemberListHidden bool `json:"member_list_hidden"` - Timezone *string `json:"timezone"` - Role string `json:"role"` // one of USER or ADMIN - - Deleted bool `json:"deleted"` - DeletedAt *time.Time `json:"deleted_at"` - DeleteReason *string `json:"delete_reason"` // TODO: this should be imported as a warning - - CustomPreferences db.CustomPreferences `json:"custom_preferences"` -} - -type NewPronoun struct { - Value string `json:"value"` - Status string `json:"status"` - DisplayText *string `json:"display_text"` -} - -type NewField struct { - Name string `json:"name"` - Entries []db.FieldEntry `json:"entries"` -} From 36cb1d20432f754036b4bfd765e807a7983c99ae Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 17 Dec 2024 17:52:32 +0100 Subject: [PATCH 183/261] feat: moderation API --- .../Moderation/AuditLogController.cs | 55 ++++ .../Moderation/ModActionsController.cs | 138 +++++++++ .../Moderation/ReportsController.cs | 224 ++++++++++++++ .../Controllers/NotificationsController.cs | 52 ++++ Foxnouns.Backend/Database/DatabaseContext.cs | 4 + .../Migrations/20241217010207_AddReports.cs | 161 ++++++++++ .../DatabaseContextModelSnapshot.cs | 195 ++++++++++++ .../Database/Models/AuditLogEntry.cs | 43 +++ .../Database/Models/Notification.cs | 41 +++ Foxnouns.Backend/Database/Models/Report.cs | 73 +++++ Foxnouns.Backend/Dto/Moderation.cs | 84 +++++ Foxnouns.Backend/Dto/User.cs | 3 +- Foxnouns.Backend/ExpectedError.cs | 2 + .../Extensions/WebApplicationExtensions.cs | 4 + Foxnouns.Backend/Foxnouns.Backend.csproj | 2 +- .../Middleware/AuthorizationMiddleware.cs | 44 +-- .../Middleware/LimitMiddleware.cs | 64 ++++ .../Services/ModerationRendererService.cs | 73 +++++ .../Services/ModerationService.cs | 292 ++++++++++++++++++ .../Services/UserRendererService.cs | 3 +- Foxnouns.Backend/Utils/AuthUtils.cs | 1 + Foxnouns.Frontend/src/lib/api/models/user.ts | 1 + .../src/lib/components/Navbar.svelte | 17 +- .../src/lib/i18n/locales/en.json | 4 +- 24 files changed, 1535 insertions(+), 45 deletions(-) create mode 100644 Foxnouns.Backend/Controllers/Moderation/AuditLogController.cs create mode 100644 Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs create mode 100644 Foxnouns.Backend/Controllers/Moderation/ReportsController.cs create mode 100644 Foxnouns.Backend/Controllers/NotificationsController.cs create mode 100644 Foxnouns.Backend/Database/Migrations/20241217010207_AddReports.cs create mode 100644 Foxnouns.Backend/Database/Models/AuditLogEntry.cs create mode 100644 Foxnouns.Backend/Database/Models/Notification.cs create mode 100644 Foxnouns.Backend/Database/Models/Report.cs create mode 100644 Foxnouns.Backend/Dto/Moderation.cs create mode 100644 Foxnouns.Backend/Middleware/LimitMiddleware.cs create mode 100644 Foxnouns.Backend/Services/ModerationRendererService.cs create mode 100644 Foxnouns.Backend/Services/ModerationService.cs diff --git a/Foxnouns.Backend/Controllers/Moderation/AuditLogController.cs b/Foxnouns.Backend/Controllers/Moderation/AuditLogController.cs new file mode 100644 index 0000000..8b556de --- /dev/null +++ b/Foxnouns.Backend/Controllers/Moderation/AuditLogController.cs @@ -0,0 +1,55 @@ +// 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 + ) + { + limit = limit switch + { + > 100 => 100, + < 0 => 100, + null => 100, + _ => limit, + }; + + IQueryable query = db.AuditLog.OrderByDescending(e => e.Id); + + if (before != null) + query = query.Where(e => e.Id < before.Value); + if (type != null) + query = query.Where(e => e.Type == type); + + List entries = await query.Take(limit!.Value).ToListAsync(); + + return Ok(entries.Select(moderationRenderer.RenderAuditLogEntry)); + } +} diff --git a/Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs b/Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs new file mode 100644 index 0000000..2fb4473 --- /dev/null +++ b/Foxnouns.Backend/Controllers/Moderation/ModActionsController.cs @@ -0,0 +1,138 @@ +// 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/ReportsController.cs b/Foxnouns.Backend/Controllers/Moderation/ReportsController.cs new file mode 100644 index 0000000..b8acc56 --- /dev/null +++ b/Foxnouns.Backend/Controllers/Moderation/ReportsController.cs @@ -0,0 +1,224 @@ +// 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; +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 + ) + { + 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, + 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 + ) + { + 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, + 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(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) + .OrderByDescending(r => r.Id); + + if (before != null) + query = query.Where(r => r.Id < before.Value); + if (!includeClosed) + query = query.Where(r => r.Status == ReportStatus.Open); + + List reports = await query.Take(limit!.Value).ToListAsync(); + + return Ok(reports.Select(moderationRenderer.RenderReport)); + } + + [HttpPost("reports/{id}/ignore")] + [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 new file mode 100644 index 0000000..8bea907 --- /dev/null +++ b/Foxnouns.Backend/Controllers/NotificationsController.cs @@ -0,0 +1,52 @@ +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(UsableBySuspendedUsers = 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(UsableBySuspendedUsers = 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/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index ddf7853..9baa143 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -71,6 +71,10 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) 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!; + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) { // Snowflakes are stored as longs diff --git a/Foxnouns.Backend/Database/Migrations/20241217010207_AddReports.cs b/Foxnouns.Backend/Database/Migrations/20241217010207_AddReports.cs new file mode 100644 index 0000000..22a1cf8 --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20241217010207_AddReports.cs @@ -0,0 +1,161 @@ +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index cfe2513..83a90fd 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -22,6 +22,7 @@ namespace Foxnouns.Backend.Database.Migrations .HasAnnotation("ProductVersion", "9.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 63); + NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); modelBuilder.Entity("Foxnouns.Backend.Database.Models.Application", b => @@ -61,6 +62,62 @@ 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") + .HasDatabaseName("ix_audit_log_report_id"); + + b.ToTable("audit_log", (string)null); + }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuthMethod", b => { b.Property("Id") @@ -270,6 +327,45 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("member_flags", (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") + .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") @@ -302,6 +398,55 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("pride_flags", (string)null); }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.Report", b => + { + b.Property("Id") + .HasColumnType("bigint") + .HasColumnName("id"); + + 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.TemporaryKey", b => { b.Property("Id") @@ -522,6 +667,16 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("user_flags", (string)null); }); + modelBuilder.Entity("Foxnouns.Backend.Database.Models.AuditLogEntry", b => + { + b.HasOne("Foxnouns.Backend.Database.Models.Report", "Report") + .WithMany() + .HasForeignKey("ReportId") + .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") @@ -584,6 +739,18 @@ namespace Foxnouns.Backend.Database.Migrations b.Navigation("PrideFlag"); }); + 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) @@ -594,6 +761,34 @@ namespace Foxnouns.Backend.Database.Migrations .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") diff --git a/Foxnouns.Backend/Database/Models/AuditLogEntry.cs b/Foxnouns.Backend/Database/Models/AuditLogEntry.cs new file mode 100644 index 0000000..a4983ae --- /dev/null +++ b/Foxnouns.Backend/Database/Models/AuditLogEntry.cs @@ -0,0 +1,43 @@ +// 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 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, +} diff --git a/Foxnouns.Backend/Database/Models/Notification.cs b/Foxnouns.Backend/Database/Models/Notification.cs new file mode 100644 index 0000000..59bf1c3 --- /dev/null +++ b/Foxnouns.Backend/Database/Models/Notification.cs @@ -0,0 +1,41 @@ +// 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/Report.cs b/Foxnouns.Backend/Database/Models/Report.cs new file mode 100644 index 0000000..e668f44 --- /dev/null +++ b/Foxnouns.Backend/Database/Models/Report.cs @@ -0,0 +1,73 @@ +// 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 ReportTargetType TargetType { get; init; } + public string? TargetSnapshot { get; init; } +} + +[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/Dto/Moderation.cs b/Foxnouns.Backend/Dto/Moderation.cs new file mode 100644 index 0000000..0de65c7 --- /dev/null +++ b/Foxnouns.Backend/Dto/Moderation.cs @@ -0,0 +1,84 @@ +// 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 Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Foxnouns.Backend.Dto; + +public record ReportResponse( + Snowflake Id, + PartialUser Reporter, + PartialUser TargetUser, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + PartialMember? TargetMember, + ReportStatus Status, + ReportReason Reason, + ReportTargetType TargetType, + JObject? Snapshot +); + +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)] Snowflake? ReportId, + AuditLogEntryType Type, + string? Reason, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] string[]? ClearedFields +); + +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); + +public record IgnoreReportRequest(string? Reason = null); + +public record WarnUserRequest( + string Reason, + FieldsToClear[]? ClearFields = null, + Snowflake? MemberId = null, + Snowflake? ReportId = null +); + +public record SuspendUserRequest(string Reason, bool ClearProfile, Snowflake? ReportId = null); + +public enum FieldsToClear +{ + DisplayName, + Avatar, + Bio, + Links, + Names, + Pronouns, + Fields, + Flags, + CustomPreferences, +} diff --git a/Foxnouns.Backend/Dto/User.cs b/Foxnouns.Backend/Dto/User.cs index a78aeba..c681001 100644 --- a/Foxnouns.Backend/Dto/User.cs +++ b/Foxnouns.Backend/Dto/User.cs @@ -47,7 +47,8 @@ public record UserResponse( [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)] string? Timezone, + [property: JsonProperty(NullValueHandling = NullValueHandling.Ignore)] bool? Deleted ); public record AuthMethodResponse( diff --git a/Foxnouns.Backend/ExpectedError.cs b/Foxnouns.Backend/ExpectedError.cs index 6c5c7e9..6a704e2 100644 --- a/Foxnouns.Backend/ExpectedError.cs +++ b/Foxnouns.Backend/ExpectedError.cs @@ -166,6 +166,8 @@ public enum ErrorCode MemberNotFound, AccountAlreadyLinked, LastAuthMethod, + InvalidReportTarget, + InvalidWarningTarget, } public class ValidationError diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index d1b1156..64564b2 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -113,6 +113,8 @@ public static class WebApplicationExtensions .AddSingleton() .AddScoped() .AddScoped() + .AddScoped() + .AddScoped() .AddScoped() .AddScoped() .AddScoped() @@ -139,11 +141,13 @@ public static class WebApplicationExtensions services .AddScoped() .AddScoped() + .AddScoped() .AddScoped(); public static IApplicationBuilder UseCustomMiddleware(this IApplicationBuilder app) => app.UseMiddleware() .UseMiddleware() + .UseMiddleware() .UseMiddleware(); public static async Task Initialize(this WebApplication app, string[] args) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 168cff6..8238fc8 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -35,7 +35,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs index 976dc5b..fc216f5 100644 --- a/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs +++ b/Foxnouns.Backend/Middleware/AuthorizationMiddleware.cs @@ -22,17 +22,16 @@ public class AuthorizationMiddleware : IMiddleware public async Task InvokeAsync(HttpContext ctx, RequestDelegate next) { Endpoint? endpoint = ctx.GetEndpoint(); - AuthorizeAttribute? authorizeAttribute = - endpoint?.Metadata.GetMetadata(); - LimitAttribute? limitAttribute = endpoint?.Metadata.GetMetadata(); + AuthorizeAttribute? attribute = endpoint?.Metadata.GetMetadata(); - if (authorizeAttribute == null || authorizeAttribute.Scopes.Length == 0) + if (attribute == null || attribute.Scopes.Length == 0) { await next(ctx); return; } Token? token = ctx.GetToken(); + if (token == null) { throw new ApiError.Unauthorized( @@ -41,40 +40,15 @@ public class AuthorizationMiddleware : IMiddleware ); } - // Users who got suspended by a moderator can still access *some* endpoints. - if ( - token.User.Deleted - && (limitAttribute?.UsableBySuspendedUsers != true || token.User.DeletedBy == null) - ) - { - throw new ApiError.Forbidden("Deleted users cannot access this endpoint."); - } - - if ( - authorizeAttribute.Scopes.Length > 0 - && authorizeAttribute.Scopes.Except(token.Scopes.ExpandScopes()).Any() - ) + if (attribute.Scopes.Except(token.Scopes.ExpandScopes()).Any()) { throw new ApiError.Forbidden( "This endpoint requires ungranted scopes.", - authorizeAttribute.Scopes.Except(token.Scopes.ExpandScopes()), + attribute.Scopes.Except(token.Scopes.ExpandScopes()), ErrorCode.MissingScopes ); } - if (limitAttribute?.RequireAdmin == true && token.User.Role != UserRole.Admin) - { - throw new ApiError.Forbidden("This endpoint can only be used by admins."); - } - - if ( - limitAttribute?.RequireModerator == true - && 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); } } @@ -84,11 +58,3 @@ public class AuthorizeAttribute(params string[] scopes) : Attribute { public readonly string[] Scopes = scopes.Except([":admin", ":moderator", ":deleted"]).ToArray(); } - -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] -public class LimitAttribute : Attribute -{ - public bool UsableBySuspendedUsers { get; init; } - public bool RequireAdmin { get; init; } - public bool RequireModerator { get; init; } -} diff --git a/Foxnouns.Backend/Middleware/LimitMiddleware.cs b/Foxnouns.Backend/Middleware/LimitMiddleware.cs new file mode 100644 index 0000000..82613c5 --- /dev/null +++ b/Foxnouns.Backend/Middleware/LimitMiddleware.cs @@ -0,0 +1,64 @@ +// 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(); + + if (attribute == null) + { + await next(ctx); + return; + } + + Token? token = ctx.GetToken(); + if ( + token?.User.Deleted == true + && (!attribute.UsableBySuspendedUsers || token.User.DeletedBy == null) + ) + { + 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 UsableBySuspendedUsers { get; init; } + public bool RequireAdmin { get; init; } + public bool RequireModerator { get; init; } +} diff --git a/Foxnouns.Backend/Services/ModerationRendererService.cs b/Foxnouns.Backend/Services/ModerationRendererService.cs new file mode 100644 index 0000000..e5d8165 --- /dev/null +++ b/Foxnouns.Backend/Services/ModerationRendererService.cs @@ -0,0 +1,73 @@ +// 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( + DatabaseContext db, + 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.TargetType, + report.TargetSnapshot != null + ? JsonConvert.DeserializeObject(report.TargetSnapshot) + : null + ); + } + + public AuditLogResponse RenderAuditLogEntry(AuditLogEntry entry) + { + return new AuditLogResponse( + Id: entry.Id, + Moderator: ToEntity(entry.ModeratorId, entry.ModeratorUsername)!, + TargetUser: ToEntity(entry.TargetUserId, entry.TargetUsername), + TargetMember: ToEntity(entry.TargetMemberId, entry.TargetMemberName), + ReportId: entry.ReportId, + 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 new file mode 100644 index 0000000..5444657 --- /dev/null +++ b/Foxnouns.Backend/Services/ModerationService.cs @@ -0,0 +1,292 @@ +// 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 NodaTime; + +namespace Foxnouns.Backend.Services; + +public class ModerationService( + ILogger logger, + DatabaseContext db, + ISnowflakeGenerator snowflakeGenerator, + IQueue queue, + 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 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 (!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 = []; + + queue.QueueInvocableWithPayload( + 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: + queue.QueueInvocableWithPayload< + MemberAvatarUpdateInvocable, + AvatarUpdatePayload + >(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: + queue.QueueInvocableWithPayload< + UserAvatarUpdateInvocable, + AvatarUpdatePayload + >(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); + } + + await db.SaveChangesAsync(); + + return entry; + } +} diff --git a/Foxnouns.Backend/Services/UserRendererService.cs b/Foxnouns.Backend/Services/UserRendererService.cs index 028fc75..7a00328 100644 --- a/Foxnouns.Backend/Services/UserRendererService.cs +++ b/Foxnouns.Backend/Services/UserRendererService.cs @@ -114,7 +114,8 @@ public class UserRendererService( tokenHidden ? user.ListHidden : null, tokenHidden ? user.LastActive : null, tokenHidden ? user.LastSidReroll : null, - tokenHidden ? user.Timezone ?? "" : null + tokenHidden ? user.Timezone ?? "" : null, + tokenHidden ? user.Deleted : null ); } diff --git a/Foxnouns.Backend/Utils/AuthUtils.cs b/Foxnouns.Backend/Utils/AuthUtils.cs index 5ebd745..2ce46e2 100644 --- a/Foxnouns.Backend/Utils/AuthUtils.cs +++ b/Foxnouns.Backend/Utils/AuthUtils.cs @@ -38,6 +38,7 @@ public static class AuthUtils "user.read_flags", "user.create_flags", "user.update_flags", + "user.moderation", ]; public static readonly string[] MemberScopes = diff --git a/Foxnouns.Frontend/src/lib/api/models/user.ts b/Foxnouns.Frontend/src/lib/api/models/user.ts index f830983..29740e6 100644 --- a/Foxnouns.Frontend/src/lib/api/models/user.ts +++ b/Foxnouns.Frontend/src/lib/api/models/user.ts @@ -26,6 +26,7 @@ export type MeUser = UserWithMembers & { last_active: string; last_sid_reroll: string; timezone: string; + deleted: boolean; }; export type UserWithMembers = User & { members: PartialMember[] | null }; diff --git a/Foxnouns.Frontend/src/lib/components/Navbar.svelte b/Foxnouns.Frontend/src/lib/components/Navbar.svelte index 2661fc9..365ede2 100644 --- a/Foxnouns.Frontend/src/lib/components/Navbar.svelte +++ b/Foxnouns.Frontend/src/lib/components/Navbar.svelte @@ -9,17 +9,25 @@ NavItem, } from "@sveltestrap/sveltestrap"; import { page } from "$app/stores"; - import type { User, Meta } from "$api/models/index"; + import type { Meta, MeUser } from "$api/models/index"; import Logo from "$components/Logo.svelte"; import { t } from "$lib/i18n"; - type Props = { user: User | null; meta: Meta }; + type Props = { user: MeUser | null; meta: Meta }; let { user, meta }: Props = $props(); let isOpen = $state(true); const toggleMenu = () => (isOpen = !isOpen); +{#if user && user.deleted} +
    + {$t("nav.suspended-account-hint")} +
    + {$t("nav.appeal-suspension-link")} +
    +{/if} + @@ -58,6 +66,11 @@ diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 7f2b2ab..7145181 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -291,7 +291,9 @@ "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" + "custom-preference-notice-link": "Go to settings", + "crop-avatar-header": "Crop avatar", + "crop-avatar-button": "Crop" }, "cancel": "Cancel", "report": { From 7ea6c62d67bd01820530703a324500e4bf981137 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 28 Feb 2025 16:36:45 +0100 Subject: [PATCH 237/261] chore(backend): update dependencies --- Foxnouns.Backend/Foxnouns.Backend.csproj | 36 +- Foxnouns.Backend/Program.cs | 24 +- .../OpenApi/PropertyKeySchemaTransformer.cs | 6 +- Foxnouns.Backend/packages.lock.json | 371 +++++++++--------- .../Foxnouns.DataMigrator.csproj | 6 +- 5 files changed, 224 insertions(+), 219 deletions(-) diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index 6f6d69f..c30f2b9 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -8,41 +8,41 @@ - - + + - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + + - - - - + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + - + diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 66e57a6..0f1d9f1 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -23,7 +23,6 @@ using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using Prometheus; -using Scalar.AspNetCore; using Sentry.Extensibility; using Serilog; @@ -46,7 +45,8 @@ builder // 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() @@ -109,16 +109,18 @@ if (config.Logging.SentryTracing) app.UseCors(); app.UseCustomMiddleware(); app.MapControllers(); -app.MapOpenApi("/api-docs/openapi/{documentName}.json"); -app.MapScalarApiReference(options => -{ - options.Title = "pronouns.cc API"; - options.OpenApiRoutePattern = "/api-docs/openapi/{documentName}.json"; - options.EndpointPathPrefix = "/api-docs/{documentName}"; -}); -app.Urls.Clear(); -app.Urls.Add(config.Address); +// 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 => diff --git a/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs b/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs index 92c1f7c..9835b50 100644 --- a/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs +++ b/Foxnouns.Backend/Utils/OpenApi/PropertyKeySchemaTransformer.cs @@ -22,8 +22,10 @@ namespace Foxnouns.Backend.Utils.OpenApi; public class PropertyKeySchemaTransformer : IOpenApiSchemaTransformer { - private static readonly DefaultContractResolver SnakeCaseConverter = - new() { NamingStrategy = new SnakeCaseNamingStrategy() }; + private static readonly DefaultContractResolver SnakeCaseConverter = new() + { + NamingStrategy = new SnakeCaseNamingStrategy(), + }; public Task TransformAsync( OpenApiSchema schema, diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index dc238f7..e3799c6 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -4,9 +4,9 @@ "net9.0": { "Coravel": { "type": "Direct", - "requested": "[6.0.0, )", - "resolved": "6.0.0", - "contentHash": "U16V/IxGL2TcpU9sT1gUA3pqoVIlz+WthC4idn8OTPiEtLElTcmNF6sHt+gOx8DRU8TBgN5vjfL4AHetjacOWQ==", + "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", @@ -17,12 +17,12 @@ }, "Coravel.Mailer": { "type": "Direct", - "requested": "[7.0.0, )", - "resolved": "7.0.0", - "contentHash": "mxSlOOBxPjCAZruOpgXtubnZA9lD0DRgutApQmAsts7DoRfe0wTzqWrYjeZTiIzgVJZKZxJglN8duTvbPrw3jQ==", + "requested": "[7.1.0, )", + "resolved": "7.1.0", + "contentHash": "yMbUrwKl5/HbJeX8JkHa8Q3CPTJ3OmPyDSG7sULbXGEhzc2GiYIh7pmVhI1FFeL3VUtFavMDkS8PTwEeCpiwlg==", "dependencies": { - "MailKit": "4.3.0", - "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.27" + "MailKit": "4.8.0", + "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": "6.0.36" } }, "EFCore.NamingConventions": { @@ -60,41 +60,41 @@ }, "Microsoft.AspNetCore.Mvc.NewtonsoftJson": { "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "pTFDEmZi3GheCSPrBxzyE63+d5unln2vYldo/nOm1xet/4rpEk2oJYcwpclPQ13E+LZBF9XixkgwYTUwqznlWg==", + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "cCnaxji6nqIHHLAEhZ6QirXCvwJNi0Q/qCPLkRW5SqMYNuOwoQdGk1KAhW65phBq1VHGt7wLbadpuGPGqfiZuA==", "dependencies": { - "Microsoft.AspNetCore.JsonPatch": "9.0.0", + "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.0, )", - "resolved": "9.0.0", - "contentHash": "FqUK5j1EOPNuFT7IafltZQ3cakqhSwVzH5ZW1MhZDe4pPXs9sJ2M5jom1Omsu+mwF2tNKKlRAzLRHQTZzbd+6Q==", + "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.0, )", - "resolved": "9.0.0", - "contentHash": "wpG+nfnfDAw87R3ovAsUmjr3MZ4tYXf6bFqEPVAIKE6IfPml3DS//iX0DBnf8kWn5ZHSO5oi1m4d/Jf+1LifJQ==", + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "P90ZuybgcpW32y985eOYxSoZ9IiL0UTYQlY0y1Pt1iHAnpZj/dQHREpSpry1RNvk8YjAeoAkWFdem5conqB9zQ==", "dependencies": { - "Microsoft.EntityFrameworkCore.Abstractions": "9.0.0", - "Microsoft.EntityFrameworkCore.Analyzers": "9.0.0", - "Microsoft.Extensions.Caching.Memory": "9.0.0", - "Microsoft.Extensions.Logging": "9.0.0" + "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.0, )", - "resolved": "9.0.0", - "contentHash": "Pqo8I+yHJ3VQrAoY0hiSncf+5P7gN/RkNilK5e+/K/yKh+yAWxdUAI6t0TG26a9VPlCa9FhyklzyFvRyj3YG9A==", + "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", @@ -102,33 +102,33 @@ "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.0", - "Microsoft.Extensions.Caching.Memory": "9.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", - "Microsoft.Extensions.DependencyModel": "9.0.0", - "Microsoft.Extensions.Logging": "9.0.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.0" + "System.Text.Json": "9.0.2" } }, "Microsoft.Extensions.Caching.Memory": { "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "zbnPX/JQ0pETRSUG9fNPBvpIq42Aufvs15gGYyNIMhCun9yhmWihz0WgsI7bSDPjxWTKBf8oX/zv6v2uZ3W9OQ==", + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "AlEfp0DMz8E1h1Exi8LBrUCNmCYcGDfSM4F/uK1D1cYx/R3w0LVvlmjICqxqXTsy7BEZaCf5leRZY2FuPEiFaw==", "dependencies": { - "Microsoft.Extensions.Caching.Abstractions": "9.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging.Abstractions": "9.0.0", - "Microsoft.Extensions.Options": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" + "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" } }, "MimeKit": { "type": "Direct", - "requested": "[4.9.0, )", - "resolved": "4.9.0", - "contentHash": "DZXXMZzmAABDxFhOSMb6SE8KKxcRd/sk1E6aJTUE5ys2FWOQhznYV2Gl3klaaSfqKn27hQ32haqquH1J8Z6kJw==", + "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", @@ -137,11 +137,11 @@ }, "Minio": { "type": "Direct", - "requested": "[6.0.3, )", - "resolved": "6.0.3", - "contentHash": "WHlkouclHtiK/pIXPHcjVmbeELHPtElj2qRSopFVpSmsFhZXeM10sPvczrkSPePsmwuvZdFryJ/hJzKu3XeLVg==", + "requested": "[6.0.4, )", + "resolved": "6.0.4", + "contentHash": "JckRL95hQ/eDHTQZ/BB7jeR0JyF+bOctMW6uriXHY5YPjCX61hiJGsswGjuDSEViKJEPxtPi3e4IwD/1TJ7PIw==", "dependencies": { - "CommunityToolkit.HighPerformance": "8.2.2", + "CommunityToolkit.HighPerformance": "8.3.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.1", "Microsoft.Extensions.Logging": "8.0.0", "System.IO.Hashing": "8.0.0", @@ -156,39 +156,39 @@ }, "NodaTime": { "type": "Direct", - "requested": "[3.2.0, )", - "resolved": "3.2.0", - "contentHash": "yoRA3jEJn8NM0/rQm78zuDNPA3DonNSZdsorMUj+dltc1D+/Lc5h9YXGqbEEZozMGr37lAoYkcSM/KjTVqD0ow==" + "requested": "[3.2.1, )", + "resolved": "3.2.1", + "contentHash": "D1aHhUfPQUxU2nfDCVuSLahpp0xCYZTmj/KNH3mSK/tStJYcx9HO9aJ0qbOP3hzjGPV/DXOqY2AHe27Nt4xs4g==" }, "Npgsql.EntityFrameworkCore.PostgreSQL": { "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "cYdOGplIvr9KgsG8nJ8xnzBTImeircbgetlzS1OmepS5dAQW6PuGpVrLOKBNEwEvGYZPsV8037X5vZ/Dmpwz7Q==", + "requested": "[9.0.3, )", + "resolved": "9.0.3", + "contentHash": "1A6HpMPbzK+quxdtug1aDHI4BSRTgpi7OaDt8WQh7SFJd2sSQ0nNTZ7sYrwyxVf4AdKdN7XJL9tpiiJjRUaa4g==", "dependencies": { - "Microsoft.EntityFrameworkCore": "[9.0.0, 10.0.0)", - "Microsoft.EntityFrameworkCore.Relational": "[9.0.0, 10.0.0)", + "Microsoft.EntityFrameworkCore": "[9.0.1, 10.0.0)", + "Microsoft.EntityFrameworkCore.Relational": "[9.0.1, 10.0.0)", "Npgsql": "9.0.2" } }, "Npgsql.EntityFrameworkCore.PostgreSQL.NodaTime": { "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "+mfwiRCK+CAKTkeBZCuQuMaOwM/yMX8B65515PS1le9TUjlG8DobuAmb48MSR/Pr/YMvU1tV8FFEFlyQviQzrg==", + "requested": "[9.0.3, )", + "resolved": "9.0.3", + "contentHash": "Eks1o3NfIbS3EHhrXC0QABrQab7CJ64C2+kF0YJWLwlH/tu3ExrgrSLpLI6INdeMYcLr2PXu71LjJsrQNVciYg==", "dependencies": { - "Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.2", + "Npgsql.EntityFrameworkCore.PostgreSQL": "9.0.3", "Npgsql.NodaTime": "9.0.2" } }, "Npgsql.Json.NET": { "type": "Direct", - "requested": "[9.0.2, )", - "resolved": "9.0.2", - "contentHash": "E81dvvpNtS4WigxZu16OAFxVvPvbEkXI7vJXZzEp7GQ03MArF5V4HBb7KXDzTaE5ZQ0bhCUFoMTODC6Z8mu27g==", + "requested": "[9.0.3, )", + "resolved": "9.0.3", + "contentHash": "lN8p9UKkoXaGUhX3DHg/1W6YeEfbjQiQ7XrJSGREUoDHXOLxDQHJnZ49P/9P2s/pH6HTVgTgT5dijpKoRLN0vQ==", "dependencies": { "Newtonsoft.Json": "13.0.3", - "Npgsql": "9.0.2" + "Npgsql": "9.0.3" } }, "prometheus-net": { @@ -212,24 +212,24 @@ }, "Roslynator.Analyzers": { "type": "Direct", - "requested": "[4.12.9, )", - "resolved": "4.12.9", - "contentHash": "X6lDpN/D5wuinq37KIx+l3GSUe9No+8bCjGBTI5sEEtxapLztkHg6gzNVhMXpXw8P+/5gFYxTXJ5Pf8O4iNz/w==" + "requested": "[4.13.1, )", + "resolved": "4.13.1", + "contentHash": "KZpLy6ZlCebMk+d/3I5KU2R7AOb4LNJ6tPJqPtvFXmO8bEBHQvCIAvJOnY2tu4C9/aVOROTDYUFADxFqw1gh/g==" }, "Scalar.AspNetCore": { "type": "Direct", - "requested": "[1.2.55, )", - "resolved": "1.2.55", - "contentHash": "zArlr6nfPQMRwyia0WFirsyczQby51GhNgWITiEIRkot+CVGZSGQ4oWGqExO11/6x26G+mcQo9Oft1mGpN0/ZQ==" + "requested": "[2.0.18, )", + "resolved": "2.0.18", + "contentHash": "nS8Sw6wRO1A/dARn3q9R6znIBfddJcmAczI5uMROBGWkO2KG/ad/Ld+UeUePTxGr1+6humJSOxI7An+q4q3oGA==" }, "Sentry.AspNetCore": { "type": "Direct", - "requested": "[4.13.0, )", - "resolved": "4.13.0", - "contentHash": "1cH9hSvjRbTkcpjUejFTrTC3jMIiOrcZ0DIvt16+AYqXhuxPEnI56npR1nhv+7WUGyhyp5cHFIZqrKnyrrGP0w==", + "requested": "[5.2.0, )", + "resolved": "5.2.0", + "contentHash": "vEKanBDOxCnEQrcMq3j47z8HOblRfiyJotdm9Fyc24cmLrLsTYZnWWprCYstt++M9bGSXYf4jrM2aaWxgJ8aww==", "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Sentry.Extensions.Logging": "4.13.0" + "Microsoft.Extensions.Configuration.Binder": "9.0.0", + "Sentry.Extensions.Logging": "5.2.0" } }, "Serilog": { @@ -264,12 +264,12 @@ }, "Serilog.Sinks.Seq": { "type": "Direct", - "requested": "[8.0.0, )", - "resolved": "8.0.0", - "contentHash": "z5ig56/qzjkX6Fj4U/9m1g8HQaQiYPMZS4Uevtjg1I+WWzoGSf5t/E+6JbMP/jbZYhU63bA5NJN5y0x+qqx2Bw==", + "requested": "[9.0.0, )", + "resolved": "9.0.0", + "contentHash": "aNU8A0K322q7+voPNmp1/qNPH+9QK8xvM1p72sMmCG0wGlshFzmtDW9QnVSoSYCj0MgQKcMOlgooovtBhRlNHw==", "dependencies": { - "Serilog": "4.0.0", - "Serilog.Sinks.File": "5.0.0" + "Serilog": "4.2.0", + "Serilog.Sinks.File": "6.0.0" } }, "SixLabors.ImageSharp": { @@ -280,9 +280,9 @@ }, "System.Text.Json": { "type": "Direct", - "requested": "[9.0.0, )", - "resolved": "9.0.0", - "contentHash": "js7+qAu/9mQvnhA4EfGMZNEzXtJCDxgkgj8ohuxq/Qxv+R56G+ljefhiJHOxTNiw54q8vmABCWUwkMulNdlZ4A==" + "requested": "[9.0.2, )", + "resolved": "9.0.2", + "contentHash": "4TY2Yokh5Xp8XHFhsY9y84yokS7B0rhkaZCXuRiKppIiKwPVH4lVSFD9EEFzRpXdBM5ZeZXD43tc2vB6njEwwQ==" }, "System.Text.RegularExpressions": { "type": "Direct", @@ -306,8 +306,8 @@ }, "CommunityToolkit.HighPerformance": { "type": "Transitive", - "resolved": "8.2.2", - "contentHash": "+zIp8d3sbtYaRbM6hqDs4Ui/z34j7DcUmleruZlYLE4CVxXq+MO8XJyIs42vzeTYFX+k0Iq1dEbBUnQ4z/Gnrw==" + "resolved": "8.3.0", + "contentHash": "2zc0Wfr9OtEbLqm6J1Jycim/nKmYv+v12CytJ3tZGNzw7n3yjh1vNCMX0kIBaFBk3sw8g0pMR86QJGXGlArC+A==" }, "EntityFrameworkCore.Exceptions.Common": { "type": "Transitive", @@ -319,16 +319,17 @@ }, "MailKit": { "type": "Transitive", - "resolved": "4.3.0", - "contentHash": "jVmB3Nr0JpqhyMiXOGWMin+QvRKpucGpSFBCav9dG6jEJPdBV+yp1RHVpKzxZPfT+0adaBuZlMFdbIciZo1EWA==", + "resolved": "4.8.0", + "contentHash": "zZ1UoM4FUnSFUJ9fTl5CEEaejR0DNP6+FDt1OfXnjg4igZntcir1tg/8Ufd6WY5vrpmvToAjluYqjVM24A+5lA==", "dependencies": { - "MimeKit": "4.3.0" + "MimeKit": "4.8.0", + "System.Formats.Asn1": "8.0.1" } }, "Microsoft.AspNetCore.JsonPatch": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "/4UONYoAIeexPoAmbzBPkVGA6KAY7t0BM+1sr0fKss2V1ERCdcM+Llub4X5Ma+LJ60oPp6KzM0e3j+Pp/JHCNw==", + "resolved": "9.0.2", + "contentHash": "bZMRhazEBgw9aZ5EBGYt0017CSd+aecsUCnppVjSa1SzWH6C1ieTSQZRAe+H0DzAVzWAoK7HLwKnQUPioopPrA==", "dependencies": { "Microsoft.CSharp": "4.7.0", "Newtonsoft.Json": "13.0.3" @@ -336,27 +337,27 @@ }, "Microsoft.AspNetCore.Mvc.Razor.Extensions": { "type": "Transitive", - "resolved": "6.0.27", - "contentHash": "trwJhFrTQuJTImmixMsDnDgRE8zuTzAUAot7WqiUlmjNzlJWLOaXXBpeA/xfNJvZuOsyGjC7RIzEyNyDGhDTLg==", + "resolved": "6.0.36", + "contentHash": "KFHRhrGAnd80310lpuWzI7Cf+GidS/h3JaPDFFnSmSGjCxB5vkBv5E+TXclJCJhqPtgNxg+keTC5SF1T9ieG5w==", "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.27", - "Microsoft.CodeAnalysis.Razor": "6.0.27" + "Microsoft.AspNetCore.Razor.Language": "6.0.36", + "Microsoft.CodeAnalysis.Razor": "6.0.36" } }, "Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation": { "type": "Transitive", - "resolved": "6.0.27", - "contentHash": "C6Gh/sAuUACxNtllcH4ZniWtPcGbixJuB1L5RXwoUe1a1wM6rpQ2TVMWpX2+cgeBj8U/izJyWY+nJ4Lz8mmMKA==", + "resolved": "6.0.36", + "contentHash": "0OG/wNedsQ9kTMrFuvrUDoJvp6Fxj6BzWgi7AUCluOENxu/0PzbjY9AC5w6mZJ22/AFxn2gFc2m0yOBTfQbiPg==", "dependencies": { - "Microsoft.AspNetCore.Mvc.Razor.Extensions": "6.0.27", - "Microsoft.CodeAnalysis.Razor": "6.0.27", - "Microsoft.Extensions.DependencyModel": "6.0.0" + "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.27", - "contentHash": "bI1kIZBgx7oJIB7utPrw4xIgcj7Pdx1jnHMTdsG54U602OcGpBzbfAuKaWs+LVdj+zZVuZsCSoRIZNJKTDP7Hw==" + "resolved": "6.0.36", + "contentHash": "n5Mg5D0aRrhHJJ6bJcwKqQydIFcgUq0jTlvuynoJjwA2IvAzh8Aqf9cpYagofQbIlIXILkCP6q6FgbngyVtpYA==" }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", @@ -410,10 +411,10 @@ }, "Microsoft.CodeAnalysis.Razor": { "type": "Transitive", - "resolved": "6.0.27", - "contentHash": "NAUvSjH8QY8gPp/fXjHhi3MnQEGtSJA0iRT/dT3RKO3AdGACPJyGmKEKxLag9+Kf2On51yGHT9DEPPnK3hyezg==", + "resolved": "6.0.36", + "contentHash": "RTLNJglWezr/1IkiWdtDpPYW7X7lwa4ow8E35cHt+sWdWxOnl+ayQqMy1RfbaLp7CLmRmgXSzMMZZU3D4vZi9Q==", "dependencies": { - "Microsoft.AspNetCore.Razor.Language": "6.0.27", + "Microsoft.AspNetCore.Razor.Language": "6.0.36", "Microsoft.CodeAnalysis.CSharp": "4.0.0", "Microsoft.CodeAnalysis.Common": "4.0.0" } @@ -449,48 +450,48 @@ }, "Microsoft.EntityFrameworkCore.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "fnmifFL8KaA4ZNLCVgfjCWhZUFxkrDInx5hR4qG7Q8IEaSiy/6VOSRFyx55oH7MV4y7wM3J3EE90nSpcVBI44Q==" + "resolved": "9.0.2", + "contentHash": "oVSjNSIYHsk0N66eqAWgDcyo9etEFbUswbz7SmlYR6nGp05byHrJAYM5N8U2aGWJWJI6WvIC2e4TXJgH6GZ6HQ==" }, "Microsoft.EntityFrameworkCore.Analyzers": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "Qje+DzXJOKiXF72SL0XxNlDtTkvWWvmwknuZtFahY5hIQpRKO59qnGuERIQ3qlzuq5x4bAJ8WMbgU5DLhBgeOQ==" + "resolved": "9.0.2", + "contentHash": "w4jzX7XI+L3erVGzbHXpx64A3QaLXxqG3f1vPpGYYZGpxOIHkh7e4iLLD7cq4Ng1vjkwzWl5ZJp0Kj/nHsgFYg==" }, "Microsoft.EntityFrameworkCore.Relational": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "j+msw6fWgAE9M3Q/5B9Uhv7pdAdAQUvFPJAiBJmoy+OXvehVbfbCE8ftMAa51Uo2ZeiqVnHShhnv4Y4UJJmUzA==", + "resolved": "9.0.2", + "contentHash": "r7O4N5uaM95InVSGUj7SMOQWN0f1PBF2Y30ow7Jg+pGX5GJCRVd/1fq83lQ50YMyq+EzyHac5o4CDQA2RsjKJQ==", "dependencies": { - "Microsoft.EntityFrameworkCore": "9.0.0", - "Microsoft.Extensions.Caching.Memory": "9.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", - "Microsoft.Extensions.Logging": "9.0.0" + "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.Caching.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "FPWZAa9c0H4dvOj351iR1jkUIs4u9ykL4Bm592yhjDyO5lCoWd+TMAHx2EMbarzUvCvgjWjJIoC6//Q9kH6YhA==", + "resolved": "9.0.2", + "contentHash": "a7QhA25n+BzSM5r5d7JznfyluMBGI7z3qyLlFviZ1Eiqv6DdiK27sLZdP/rpYirBM6UYAKxu5TbmfhIy13GN9A==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.Primitives": "9.0.2" } }, "Microsoft.Extensions.Configuration": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "0J/9YNXTMWSZP2p2+nvl8p71zpSwokZXZuJW+VjdErkegAnFdO1XlqtA62SJtgVYHdKu3uPxJHcMR/r35HwFBA==", + "resolved": "9.0.0", + "contentHash": "YIMO9T3JL8MeEXgVozKt2v79hquo/EFtnY0vgxmLnUvk1Rei/halI7kOWZL2RBeV9FMGzgM9LZA8CVaNwFMaNA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Configuration.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "lqvd7W3FGKUO1+ZoUEMaZ5XDJeWvjpy2/M/ptCGz3tXLD4HWVaSzjufsAsjemasBEg+2SxXVtYVvGt5r2nKDlg==", + "resolved": "9.0.2", + "contentHash": "I0O/270E/lUNqbBxlRVjxKOMZyYjP88dpEgQTveml+h2lTzAP4vbawLVwjS9SC7lKaU893bwyyNz0IVJYsm9EA==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.Primitives": "9.0.2" } }, "Microsoft.Extensions.Configuration.Binder": { @@ -503,30 +504,30 @@ }, "Microsoft.Extensions.DependencyInjection": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "MCPrg7v3QgNMr0vX4vzRXvkNGgLg8vKWX0nKCWUxu2uPyMsaRgiRc1tHBnbTcfJMhMKj2slE/j2M9oGkd25DNw==", + "resolved": "9.0.2", + "contentHash": "ZffbJrskOZ40JTzcTyKwFHS5eACSWp2bUQBBApIgGV+es8RaTD4OxUG7XxFr3RIPLXtYQ1jQzF2DjKB5fZn7Qg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" } }, "Microsoft.Extensions.DependencyInjection.Abstractions": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "+6f2qv2a3dLwd5w6JanPIPs47CxRbnk+ZocMJUhv9NxP88VlOcJYZs9jY+MYSjxvady08bUZn6qgiNh7DadGgg==" + "resolved": "9.0.2", + "contentHash": "MNe7GSTBf3jQx5vYrXF0NZvn6l7hUKF6J54ENfAgCO8y6xjN1XUmKKWG464LP2ye6QqDiA1dkaWEZBYnhoZzjg==" }, "Microsoft.Extensions.DependencyModel": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "saxr2XzwgDU77LaQfYFXmddEDRUKHF4DaGMZkNB3qjdVSZlax3//dGJagJkKrGMIPNZs2jVFXITyCCR6UHJNdA==" + "resolved": "9.0.2", + "contentHash": "3ImbcbS68jy9sKr9Z9ToRbEEX0bvIRdb8zyf5ebtL9Av2CUCGHvaO5wsSXfRfAjr60Vrq0tlmNji9IzAxW6EOw==" }, "Microsoft.Extensions.Diagnostics": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "3PZp/YSkIXrF7QK7PfC1bkyRYwqOHpWFad8Qx+4wkuumAeXo1NHaxpS9LboNA9OvNSAu+QOVlXbMyoY+pHSqcw==", + "resolved": "9.0.0", + "contentHash": "0CF9ZrNw5RAlRfbZuVIvzzhP8QeWqHiUmMBU/2H7Nmit8/vwP3/SbHeEctth7D4Gz2fBnEbokPc1NU8/j/1ZLw==", "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Diagnostics.Abstractions": "9.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0" } }, "Microsoft.Extensions.Diagnostics.Abstractions": { @@ -559,49 +560,49 @@ } }, "Microsoft.Extensions.Http": { - "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "cWz4caHwvx0emoYe7NkHPxII/KkTI8R/LC9qdqJqnKv2poTJ4e2qqPGQqvRoQ5kaSA4FU5IV3qFAuLuOhoqULQ==", - "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Diagnostics": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" - } - }, - "Microsoft.Extensions.Logging": { "type": "Transitive", "resolved": "9.0.0", - "contentHash": "crjWyORoug0kK7RSNJBTeSE6VX8IQgLf3nUpTB9m62bPXp/tzbnOsnbe8TXEG0AASNaKZddnpHKw7fET8E++Pg==", + "contentHash": "DqI4q54U4hH7bIAq9M5a/hl5Odr/KBAoaZ0dcT4OgutD8dook34CbkvAfAIzkMVjYXiL+E5ul9etwwqiX4PHGw==", "dependencies": { - "Microsoft.Extensions.DependencyInjection": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Diagnostics": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", "Microsoft.Extensions.Logging.Abstractions": "9.0.0", "Microsoft.Extensions.Options": "9.0.0" } }, + "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.0", - "contentHash": "g0UfujELzlLbHoVG8kPKVBaW470Ewi+jnptGS9KUi6jcb+k2StujtK3m26DFSGGwQ/+bVgZfsWqNzlP6YOejvw==", + "resolved": "9.0.2", + "contentHash": "dV9s2Lamc8jSaqhl2BQSPn/AryDIH2sSbQUyLitLXV0ROmsb+SROnn2cH939JFbsNrnf3mIM3GNRKT7P0ldwLg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2" } }, "Microsoft.Extensions.Logging.Configuration": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "ixXXV0G/12g6MXK65TLngYN9V5hQQRuV+fZi882WIoVJT7h5JvoYoxTEwCgdqwLjSneqh1O+66gM8sMr9z/rsQ==", + "resolved": "9.0.0", + "contentHash": "H05HiqaNmg6GjH34ocYE9Wm1twm3Oz2aXZko8GTwGBzM7op2brpAA8pJ5yyD1OpS1mXUtModBYOlcZ/wXeWsSg==", "dependencies": { - "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" + "Microsoft.Extensions.Configuration": "9.0.0", + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Configuration.Binder": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Logging": "9.0.0", + "Microsoft.Extensions.Logging.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "9.0.0" } }, "Microsoft.Extensions.ObjectPool": { @@ -611,29 +612,29 @@ }, "Microsoft.Extensions.Options": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "y2146b3jrPI3Q0lokKXdKLpmXqakYbDIPDV6r3M8SqvSf45WwOTzkyfDpxnZXJsJQEpAsAqjUq5Pu8RCJMjubg==", + "resolved": "9.0.2", + "contentHash": "zr98z+AN8+isdmDmQRuEJ/DAKZGUTHmdv3t0ZzjHvNqvA44nAgkXE9kYtfoN6581iALChhVaSw2Owt+Z2lVbkQ==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", - "Microsoft.Extensions.Primitives": "9.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.2", + "Microsoft.Extensions.Primitives": "9.0.2" } }, "Microsoft.Extensions.Options.ConfigurationExtensions": { "type": "Transitive", - "resolved": "8.0.0", - "contentHash": "0f4DMRqEd50zQh+UyJc+/HiBsZ3vhAQALgdkcQEalSH1L2isdC7Yj54M3cyo5e+BeO5fcBQ7Dxly8XiBBcvRgw==", + "resolved": "9.0.0", + "contentHash": "Ob3FXsXkcSMQmGZi7qP07EQ39kZpSBlTcAZLbJLdI4FIf0Jug8biv2HTavWmnTirchctPlq9bl/26CXtQRguzA==", "dependencies": { - "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", - "Microsoft.Extensions.Primitives": "8.0.0" + "Microsoft.Extensions.Configuration.Abstractions": "9.0.0", + "Microsoft.Extensions.Configuration.Binder": "9.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.0", + "Microsoft.Extensions.Options": "9.0.0", + "Microsoft.Extensions.Primitives": "9.0.0" } }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.0", - "contentHash": "N3qEBzmLMYiASUlKxxFIISP4AiwuPTHF5uCh+2CWSwwzAJiIYx0kBJsS30cp1nvhSySFAVi30jecD307jV+8Kg==" + "resolved": "9.0.2", + "contentHash": "puBMtKe/wLuYa7H6docBkLlfec+h8L35DXqsDKKJgW0WY5oCwJ3cBJKcDaZchv6knAyqOMfsl6VUbaR++E5LXA==" }, "Microsoft.NETCore.Platforms": { "type": "Transitive", @@ -668,8 +669,8 @@ }, "Npgsql": { "type": "Transitive", - "resolved": "9.0.2", - "contentHash": "hCbO8box7i/XXiTFqCJ3GoowyLqx3JXxyrbOJ6om7dr+eAknvBNhhUHeJVGAQo44sySZTfdVffp4BrtPeLZOAA==", + "resolved": "9.0.3", + "contentHash": "tPvY61CxOAWxNsKLEBg+oR646X4Bc8UmyQ/tJszL/7mEmIXQnnBhVJZrZEEUv0Bstu0mEsHZD5At3EO8zQRAYw==", "dependencies": { "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } @@ -685,18 +686,18 @@ }, "Sentry": { "type": "Transitive", - "resolved": "4.13.0", - "contentHash": "Wfw3M1WpFcrYaGzPm7QyUTfIOYkVXQ1ry6p4WYjhbLz9fPwV23SGQZTFDpdox67NHM0V0g1aoQ4YKLm4ANtEEg==" + "resolved": "5.2.0", + "contentHash": "b3aZSOU2CjlIIFRtPRbXParKQ+9PF+JOqkSD7Gxq6PiR07t1rnK+crPtdrWMXfW6PVo/s67trCJ+fuLsgTeADw==" }, "Sentry.Extensions.Logging": { "type": "Transitive", - "resolved": "4.13.0", - "contentHash": "yZ5+TtJKWcss6cG17YjnovImx4X56T8O6Qy6bsMC8tMDttYy8J7HJ2F+WdaZNyjOCo0Rfi6N2gc+Clv/5pf+TQ==", + "resolved": "5.2.0", + "contentHash": "546bHsERKY7/pG5T4mVIp6WbHnQPMst6VDuxSaeU5DhQHLfh7KhgMmkdZ4Xvdlr95fvWk5/bX2xbipy6qoh/1A==", "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "8.0.0", - "Microsoft.Extensions.Http": "8.0.0", - "Microsoft.Extensions.Logging.Configuration": "8.0.0", - "Sentry": "4.13.0" + "Microsoft.Extensions.Configuration.Binder": "9.0.0", + "Microsoft.Extensions.Http": "9.0.0", + "Microsoft.Extensions.Logging.Configuration": "9.0.0", + "Sentry": "5.2.0" } }, "Serilog.Extensions.Hosting": { diff --git a/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj b/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj index 5fde110..ead15ce 100644 --- a/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj +++ b/Foxnouns.DataMigrator/Foxnouns.DataMigrator.csproj @@ -12,9 +12,9 @@ - - - + + + From 218c756a7075902bc3d6a4a762639ce9013b2ddd Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 28 Feb 2025 16:37:15 +0100 Subject: [PATCH 238/261] feat(backend): make field limits configurable --- Foxnouns.Backend/Config.cs | 5 + .../Controllers/MembersController.cs | 15 +- .../Controllers/UsersController.cs | 9 +- .../ValidationService.Fields.cs} | 224 +++++++++--------- Foxnouns.Backend/Utils/Limits.cs | 23 -- .../Utils/ValidationUtils.Strings.cs | 2 +- Foxnouns.DataMigrator/UserMigrator.cs | 4 +- 7 files changed, 133 insertions(+), 149 deletions(-) rename Foxnouns.Backend/{Utils/ValidationUtils.Fields.cs => Services/ValidationService.Fields.cs} (56%) delete mode 100644 Foxnouns.Backend/Utils/Limits.cs diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index 0ed8b7a..d1e6df5 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -99,6 +99,11 @@ public class Config { 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; diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index 8f832c1..dbea99c 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -81,13 +81,13 @@ public class MembersController( ("display_name", validationService.ValidateDisplayName(req.DisplayName)), ("bio", validationService.ValidateBio(req.Bio)), ("avatar", validationService.ValidateAvatar(req.Avatar)), - .. ValidationUtils.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), - .. ValidationUtils.ValidateFieldEntries( + .. validationService.ValidateFields(req.Fields, CurrentUser!.CustomPreferences), + .. validationService.ValidateFieldEntries( req.Names?.ToArray(), CurrentUser!.CustomPreferences, "names" ), - .. ValidationUtils.ValidatePronouns( + .. validationService.ValidatePronouns( req.Pronouns?.ToArray(), CurrentUser!.CustomPreferences ), @@ -191,7 +191,7 @@ public class MembersController( if (req.Names != null) { errors.AddRange( - ValidationUtils.ValidateFieldEntries( + validationService.ValidateFieldEntries( req.Names, CurrentUser!.CustomPreferences, "names" @@ -203,7 +203,7 @@ public class MembersController( if (req.Pronouns != null) { errors.AddRange( - ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences) + validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences) ); member.Pronouns = req.Pronouns.ToList(); } @@ -211,7 +211,10 @@ public class MembersController( if (req.Fields != null) { errors.AddRange( - ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences) + validationService.ValidateFields( + req.Fields.ToList(), + CurrentUser!.CustomPreferences + ) ); member.Fields = req.Fields.ToList(); } diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index 6ccbff0..f7e3115 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -91,7 +91,7 @@ public class UsersController( if (req.Names != null) { errors.AddRange( - ValidationUtils.ValidateFieldEntries( + validationService.ValidateFieldEntries( req.Names, CurrentUser!.CustomPreferences, "names" @@ -103,7 +103,7 @@ public class UsersController( if (req.Pronouns != null) { errors.AddRange( - ValidationUtils.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences) + validationService.ValidatePronouns(req.Pronouns, CurrentUser!.CustomPreferences) ); user.Pronouns = req.Pronouns.ToList(); } @@ -111,7 +111,10 @@ public class UsersController( if (req.Fields != null) { errors.AddRange( - ValidationUtils.ValidateFields(req.Fields.ToList(), CurrentUser!.CustomPreferences) + validationService.ValidateFields( + req.Fields.ToList(), + CurrentUser!.CustomPreferences + ) ); user.Fields = req.Fields.ToList(); } diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs b/Foxnouns.Backend/Services/ValidationService.Fields.cs similarity index 56% rename from Foxnouns.Backend/Utils/ValidationUtils.Fields.cs rename to Foxnouns.Backend/Services/ValidationService.Fields.cs index 0235eb6..e2cbff3 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Fields.cs +++ b/Foxnouns.Backend/Services/ValidationService.Fields.cs @@ -15,9 +15,9 @@ using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -namespace Foxnouns.Backend.Utils; +namespace Foxnouns.Backend.Services; -public static partial class ValidationUtils +public partial class ValidationService { public static readonly string[] DefaultStatusOptions = [ @@ -28,7 +28,7 @@ public static partial class ValidationUtils "avoid", ]; - public static IEnumerable<(string, ValidationError?)> ValidateFields( + public IEnumerable<(string, ValidationError?)> ValidateFields( List? fields, IReadOnlyDictionary customPreferences ) @@ -37,7 +37,7 @@ public static partial class ValidationUtils return []; var errors = new List<(string, ValidationError?)>(); - if (fields.Count > 25) + if (fields.Count > _limits.MaxFields) { errors.Add( ( @@ -45,7 +45,7 @@ public static partial class ValidationUtils ValidationError.LengthError( "Too many fields", 0, - Limits.FieldLimit, + _limits.MaxFields, fields.Count ) ) @@ -53,39 +53,38 @@ public static partial class ValidationUtils } // No overwhelming this function, thank you - if (fields.Count > 100) + if (fields.Count > _limits.MaxFields + 50) return errors; foreach ((Field? field, int index) in fields.Select((field, index) => (field, index))) { - switch (field.Name.Length) + if (field.Name.Length > _limits.MaxFieldNameLength) { - case > Limits.FieldNameLimit: - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too long", - 1, - Limits.FieldNameLimit, - field.Name.Length - ) + errors.Add( + ( + $"fields.{index}.name", + ValidationError.LengthError( + "Field name is too long", + 1, + _limits.MaxFieldNameLength, + field.Name.Length ) - ); - break; - case < 1: - errors.Add( - ( - $"fields.{index}.name", - ValidationError.LengthError( - "Field name is too short", - 1, - Limits.FieldNameLimit, - 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 ) - ); - break; + ) + ); } errors = errors @@ -102,7 +101,7 @@ public static partial class ValidationUtils return errors; } - public static IEnumerable<(string, ValidationError?)> ValidateFieldEntries( + public IEnumerable<(string, ValidationError?)> ValidateFieldEntries( FieldEntry[]? entries, IReadOnlyDictionary customPreferences, string errorPrefix = "fields" @@ -112,7 +111,7 @@ public static partial class ValidationUtils return []; var errors = new List<(string, ValidationError?)>(); - if (entries.Length > Limits.FieldEntriesLimit) + if (entries.Length > _limits.MaxFieldEntries) { errors.Add( ( @@ -120,7 +119,7 @@ public static partial class ValidationUtils ValidationError.LengthError( "Field has too many entries", 0, - Limits.FieldEntriesLimit, + _limits.MaxFieldEntries, entries.Length ) ) @@ -128,7 +127,7 @@ public static partial class ValidationUtils } // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > Limits.FieldEntriesLimit + 50) + if (entries.Length > _limits.MaxFieldEntries + 50) return errors; string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray(); @@ -139,34 +138,33 @@ public static partial class ValidationUtils ) ) { - switch (entry.Value.Length) + if (entry.Value.Length > _limits.MaxFieldEntryTextLength) { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Field value is too long", + 1, + _limits.MaxFieldEntryTextLength, + entry.Value.Length ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Field value is too short", - 1, - Limits.FieldEntryTextLimit, - 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 ) - ); - break; + ) + ); } if ( @@ -186,7 +184,7 @@ public static partial class ValidationUtils return errors; } - public static IEnumerable<(string, ValidationError?)> ValidatePronouns( + public IEnumerable<(string, ValidationError?)> ValidatePronouns( Pronoun[]? entries, IReadOnlyDictionary customPreferences, string errorPrefix = "pronouns" @@ -196,7 +194,7 @@ public static partial class ValidationUtils return []; var errors = new List<(string, ValidationError?)>(); - if (entries.Length > Limits.FieldEntriesLimit) + if (entries.Length > _limits.MaxFieldEntries) { errors.Add( ( @@ -204,7 +202,7 @@ public static partial class ValidationUtils ValidationError.LengthError( "Too many pronouns", 0, - Limits.FieldEntriesLimit, + _limits.MaxFieldEntries, entries.Length ) ) @@ -212,7 +210,7 @@ public static partial class ValidationUtils } // Same as above, no overwhelming this function with a ridiculous amount of entries - if (entries.Length > Limits.FieldEntriesLimit + 50) + if (entries.Length > _limits.MaxFieldEntries + 50) return errors; string[] customPreferenceIds = customPreferences.Keys.Select(id => id.ToString()).ToArray(); @@ -221,66 +219,64 @@ public static partial class ValidationUtils (Pronoun? entry, int entryIdx) in entries.Select((entry, entryIdx) => (entry, entryIdx)) ) { - switch (entry.Value.Length) + if (entry.Value.Length > _limits.MaxFieldEntryTextLength) { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.value", + ValidationError.LengthError( + "Pronoun value is too long", + 1, + _limits.MaxFieldEntryTextLength, + entry.Value.Length ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.value", - ValidationError.LengthError( - "Pronoun value is too short", - 1, - Limits.FieldEntryTextLimit, - 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 ) - ); - break; + ) + ); } if (entry.DisplayText != null) { - switch (entry.DisplayText.Length) + if (entry.DisplayText.Length > _limits.MaxFieldEntryTextLength) { - case > Limits.FieldEntryTextLimit: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too long", - 1, - Limits.FieldEntryTextLimit, - entry.Value.Length - ) + errors.Add( + ( + $"{errorPrefix}.{entryIdx}.display_text", + ValidationError.LengthError( + "Pronoun display text is too long", + 1, + _limits.MaxFieldEntryTextLength, + entry.Value.Length ) - ); - break; - case < 1: - errors.Add( - ( - $"{errorPrefix}.{entryIdx}.display_text", - ValidationError.LengthError( - "Pronoun display text is too short", - 1, - Limits.FieldEntryTextLimit, - 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 ) - ); - break; + ) + ); } } diff --git a/Foxnouns.Backend/Utils/Limits.cs b/Foxnouns.Backend/Utils/Limits.cs deleted file mode 100644 index 3010d46..0000000 --- a/Foxnouns.Backend/Utils/Limits.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 . -namespace Foxnouns.Backend.Utils; - -public static class Limits -{ - public const int FieldLimit = 25; - public const int FieldNameLimit = 100; - public const int FieldEntryTextLimit = 100; - public const int FieldEntriesLimit = 100; -} diff --git a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs index 1a99993..82ee485 100644 --- a/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs +++ b/Foxnouns.Backend/Utils/ValidationUtils.Strings.cs @@ -20,7 +20,7 @@ public static partial class ValidationUtils public static ValidationError? ValidateReportContext(string? context) => context?.Length > MaximumReportContextLength - ? ValidationError.GenericValidationError("Avatar is too large", null) + ? ValidationError.GenericValidationError("Report context is too long", null) : null; public const int MinimumPasswordLength = 12; diff --git a/Foxnouns.DataMigrator/UserMigrator.cs b/Foxnouns.DataMigrator/UserMigrator.cs index df895b9..ee46878 100644 --- a/Foxnouns.DataMigrator/UserMigrator.cs +++ b/Foxnouns.DataMigrator/UserMigrator.cs @@ -2,7 +2,7 @@ using System.Diagnostics.CodeAnalysis; using Dapper; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; -using Foxnouns.Backend.Utils; +using Foxnouns.Backend.Services; using Foxnouns.DataMigrator.Models; using NodaTime.Extensions; using Npgsql; @@ -260,6 +260,6 @@ public class UserMigrator( { if (_preferenceIds.TryGetValue(id, out Snowflake preferenceId)) return preferenceId.ToString(); - return ValidationUtils.DefaultStatusOptions.Contains(id) ? id : "okay"; + return ValidationService.DefaultStatusOptions.Contains(id) ? id : "okay"; } } From a2485367894f52793227b0470d8e2e966b47b406 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 28 Feb 2025 16:47:21 +0100 Subject: [PATCH 239/261] fix typo in DOCKER.md --- DOCKER.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DOCKER.md b/DOCKER.md index b485eb7..a007aab 100644 --- a/DOCKER.md +++ b/DOCKER.md @@ -2,7 +2,7 @@ 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. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do th esame. +3. Copy `docker/frontend.example.env` to `docker/frontend.env`, and do the same. 4. Build with `docker compose build` 5. Run with `docker compose up` From 7d6d4631b81168bf67df25c34d4d8cf2750a9942 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 28 Feb 2025 16:50:57 +0100 Subject: [PATCH 240/261] fix(frontend): don't reference email auth in text if it's disabled --- Foxnouns.Frontend/src/lib/i18n/locales/en.json | 3 ++- Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 7145181..fe10b04 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -86,7 +86,8 @@ "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" + "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", diff --git a/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte b/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte index ee4d040..3efbfa0 100644 --- a/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte +++ b/Foxnouns.Frontend/src/routes/auth/log-in/+page.svelte @@ -50,8 +50,13 @@
    {/if}
    -

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

    -

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

    + {#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} From cd24196cd126b2b7de3ca0d0b117b63aa1eecb8a Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 28 Feb 2025 16:53:53 +0100 Subject: [PATCH 241/261] chore(backend): format --- Foxnouns.Backend/Services/EmailRateLimiter.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Foxnouns.Backend/Services/EmailRateLimiter.cs b/Foxnouns.Backend/Services/EmailRateLimiter.cs index 3a1a81a..cc2dbb4 100644 --- a/Foxnouns.Backend/Services/EmailRateLimiter.cs +++ b/Foxnouns.Backend/Services/EmailRateLimiter.cs @@ -23,8 +23,11 @@ public class EmailRateLimiter { private readonly ConcurrentDictionary _limiters = new(); - private readonly FixedWindowRateLimiterOptions _limiterOptions = - new() { Window = TimeSpan.FromHours(2), PermitLimit = 3 }; + private readonly FixedWindowRateLimiterOptions _limiterOptions = new() + { + Window = TimeSpan.FromHours(2), + PermitLimit = 3, + }; private RateLimiter GetLimiter(string bucket) => _limiters.GetOrAdd(bucket, _ => new FixedWindowRateLimiter(_limiterOptions)); From 7759225428cc2540a5d292f5d67bd3606adc957a Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 4 Mar 2025 17:03:39 +0100 Subject: [PATCH 242/261] refactor(backend): replace coravel with hangfire for background jobs for *some reason*, coravel locks a persistent job queue behind a paywall. this means that if the server ever crashes, all pending jobs are lost. this is... not good, so we're switching to hangfire for that instead. coravel is still used for emails, though. BREAKING CHANGE: Foxnouns.NET now requires Redis to work. the EFCore storage for hangfire doesn't work well enough, unfortunately. --- Foxnouns.Backend/Config.cs | 1 + .../Authentication/AuthController.cs | 2 +- .../Authentication/EmailAuthController.cs | 2 +- .../Controllers/ExportsController.cs | 15 +-- .../Controllers/FlagsController.cs | 9 +- .../Controllers/MembersController.cs | 10 +- .../Controllers/UsersController.cs | 6 +- Foxnouns.Backend/Database/DatabaseContext.cs | 2 - .../20250304155708_RemoveTemporaryKeys.cs | 55 +++++++++++ .../DatabaseContextModelSnapshot.cs | 35 +------ .../Database/Models/TemporaryKey.cs | 25 ----- .../Extensions/ImageObjectExtensions.cs | 10 +- .../Extensions/KeyCacheExtensions.cs | 50 ++++------ .../Extensions/WebApplicationExtensions.cs | 13 ++- Foxnouns.Backend/Foxnouns.Backend.csproj | 4 + ...ortInvocable.cs => CreateDataExportJob.cs} | 26 +++--- Foxnouns.Backend/Jobs/CreateFlagInvocable.cs | 30 +++--- ...eInvocable.cs => MemberAvatarUpdateJob.cs} | 22 +++-- Foxnouns.Backend/Jobs/Payloads.cs | 2 - ...ateInvocable.cs => UserAvatarUpdateJob.cs} | 22 +++-- Foxnouns.Backend/Program.cs | 15 +++ Foxnouns.Backend/Services/KeyCacheService.cs | 92 ++++--------------- .../Services/ModerationService.cs | 17 +--- Foxnouns.Backend/packages.lock.json | 76 +++++++++++++++ 24 files changed, 272 insertions(+), 269 deletions(-) create mode 100644 Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs delete mode 100644 Foxnouns.Backend/Database/Models/TemporaryKey.cs rename Foxnouns.Backend/Jobs/{CreateDataExportInvocable.cs => CreateDataExportJob.cs} (93%) rename Foxnouns.Backend/Jobs/{MemberAvatarUpdateInvocable.cs => MemberAvatarUpdateJob.cs} (86%) rename Foxnouns.Backend/Jobs/{UserAvatarUpdateInvocable.cs => UserAvatarUpdateJob.cs} (88%) diff --git a/Foxnouns.Backend/Config.cs b/Foxnouns.Backend/Config.cs index d1e6df5..b48a2c4 100644 --- a/Foxnouns.Backend/Config.cs +++ b/Foxnouns.Backend/Config.cs @@ -55,6 +55,7 @@ public class Config public bool? EnablePooling { get; init; } public int? Timeout { get; init; } public int? MaxPoolSize { get; init; } + public string Redis { get; init; } = string.Empty; } public class StorageConfig diff --git a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs index 0d95eb2..39d3b11 100644 --- a/Foxnouns.Backend/Controllers/Authentication/AuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/AuthController.cs @@ -46,7 +46,7 @@ public class AuthController( config.GoogleAuth.Enabled, config.TumblrAuth.Enabled ); - string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync(ct)); + string state = HttpUtility.UrlEncode(await keyCacheService.GenerateAuthStateAsync()); string? discord = null; string? google = null; string? tumblr = null; diff --git a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs index bdf4b9a..8024ee6 100644 --- a/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs +++ b/Foxnouns.Backend/Controllers/Authentication/EmailAuthController.cs @@ -56,7 +56,7 @@ public class EmailAuthController( if (!req.Email.Contains('@')) throw new ApiError.BadRequest("Email is invalid", "email", req.Email); - string state = await keyCacheService.GenerateRegisterEmailStateAsync(req.Email, null, ct); + 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 ( diff --git a/Foxnouns.Backend/Controllers/ExportsController.cs b/Foxnouns.Backend/Controllers/ExportsController.cs index 7f40625..0442386 100644 --- a/Foxnouns.Backend/Controllers/ExportsController.cs +++ b/Foxnouns.Backend/Controllers/ExportsController.cs @@ -12,7 +12,6 @@ // // 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; @@ -28,13 +27,8 @@ namespace Foxnouns.Backend.Controllers; [Authorize("identify")] [Limit(UsableByDeletedUsers = true)] [ApiExplorerSettings(IgnoreApi = true)] -public class ExportsController( - ILogger logger, - Config config, - IClock clock, - DatabaseContext db, - IQueue queue -) : ApiControllerBase +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(); @@ -80,10 +74,7 @@ public class ExportsController( throw new ApiError.BadRequest("You can't request a new data export so soon."); } - queue.QueueInvocableWithPayload( - new CreateDataExportPayload(CurrentUser.Id) - ); - + CreateDataExportJob.Enqueue(CurrentUser.Id); return NoContent(); } } diff --git a/Foxnouns.Backend/Controllers/FlagsController.cs b/Foxnouns.Backend/Controllers/FlagsController.cs index e976072..bed022a 100644 --- a/Foxnouns.Backend/Controllers/FlagsController.cs +++ b/Foxnouns.Backend/Controllers/FlagsController.cs @@ -12,7 +12,6 @@ // // 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; @@ -30,8 +29,7 @@ namespace Foxnouns.Backend.Controllers; public class FlagsController( DatabaseContext db, UserRendererService userRenderer, - ISnowflakeGenerator snowflakeGenerator, - IQueue queue + ISnowflakeGenerator snowflakeGenerator ) : ApiControllerBase { [HttpGet] @@ -74,10 +72,7 @@ public class FlagsController( db.Add(flag); await db.SaveChangesAsync(); - queue.QueueInvocableWithPayload( - new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image) - ); - + CreateFlagJob.Enqueue(new CreateFlagPayload(flag.Id, CurrentUser!.Id, req.Image)); return Accepted(userRenderer.RenderPrideFlag(flag)); } diff --git a/Foxnouns.Backend/Controllers/MembersController.cs b/Foxnouns.Backend/Controllers/MembersController.cs index dbea99c..bc35f62 100644 --- a/Foxnouns.Backend/Controllers/MembersController.cs +++ b/Foxnouns.Backend/Controllers/MembersController.cs @@ -12,7 +12,6 @@ // // 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 EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -37,7 +36,6 @@ public class MembersController( MemberRendererService memberRenderer, ISnowflakeGenerator snowflakeGenerator, ObjectStorageService objectStorageService, - IQueue queue, IClock clock, ValidationService validationService, Config config @@ -139,9 +137,7 @@ public class MembersController( if (req.Avatar != null) { - queue.QueueInvocableWithPayload( - new AvatarUpdatePayload(member.Id, req.Avatar) - ); + MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar)); } return Ok(memberRenderer.RenderMember(member, CurrentToken)); @@ -239,9 +235,7 @@ public class MembersController( // so it's in a separate block to the validation above. if (req.HasProperty(nameof(req.Avatar))) { - queue.QueueInvocableWithPayload( - new AvatarUpdatePayload(member.Id, req.Avatar) - ); + MemberAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(member.Id, req.Avatar)); } try diff --git a/Foxnouns.Backend/Controllers/UsersController.cs b/Foxnouns.Backend/Controllers/UsersController.cs index f7e3115..787ff66 100644 --- a/Foxnouns.Backend/Controllers/UsersController.cs +++ b/Foxnouns.Backend/Controllers/UsersController.cs @@ -12,7 +12,6 @@ // // 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 EntityFramework.Exceptions.Common; using Foxnouns.Backend.Database; using Foxnouns.Backend.Database.Models; @@ -34,7 +33,6 @@ public class UsersController( ILogger logger, UserRendererService userRenderer, ISnowflakeGenerator snowflakeGenerator, - IQueue queue, IClock clock, ValidationService validationService ) : ApiControllerBase @@ -177,9 +175,7 @@ public class UsersController( // so it's in a separate block to the validation above. if (req.HasProperty(nameof(req.Avatar))) { - queue.QueueInvocableWithPayload( - new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar) - ); + UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(CurrentUser!.Id, req.Avatar)); } try diff --git a/Foxnouns.Backend/Database/DatabaseContext.cs b/Foxnouns.Backend/Database/DatabaseContext.cs index ae620f2..c9120f3 100644 --- a/Foxnouns.Backend/Database/DatabaseContext.cs +++ b/Foxnouns.Backend/Database/DatabaseContext.cs @@ -64,7 +64,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) public DbSet FediverseApplications { get; init; } = null!; public DbSet Tokens { get; init; } = null!; public DbSet Applications { get; init; } = null!; - public DbSet TemporaryKeys { get; init; } = null!; public DbSet DataExports { get; init; } = null!; public DbSet PrideFlags { get; init; } = null!; @@ -87,7 +86,6 @@ public class DatabaseContext(DbContextOptions options) : DbContext(options) 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(k => k.Key).IsUnique(); modelBuilder.Entity().HasIndex(d => d.Filename).IsUnique(); // Two indexes on auth_methods, one for fediverse auth and one for all other types. diff --git a/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs b/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs new file mode 100644 index 0000000..27a8ada --- /dev/null +++ b/Foxnouns.Backend/Database/Migrations/20250304155708_RemoveTemporaryKeys.cs @@ -0,0 +1,55 @@ +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/DatabaseContextModelSnapshot.cs b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs index 6b4f4d4..922a599 100644 --- a/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs +++ b/Foxnouns.Backend/Database/Migrations/DatabaseContextModelSnapshot.cs @@ -19,7 +19,7 @@ namespace Foxnouns.Backend.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("ProductVersion", "9.0.2") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "hstore"); @@ -479,39 +479,6 @@ namespace Foxnouns.Backend.Database.Migrations b.ToTable("reports", (string)null); }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.TemporaryKey", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint") - .HasColumnName("id"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("Expires") - .HasColumnType("timestamp with time zone") - .HasColumnName("expires"); - - b.Property("Key") - .IsRequired() - .HasColumnType("text") - .HasColumnName("key"); - - b.Property("Value") - .IsRequired() - .HasColumnType("text") - .HasColumnName("value"); - - b.HasKey("Id") - .HasName("pk_temporary_keys"); - - b.HasIndex("Key") - .IsUnique() - .HasDatabaseName("ix_temporary_keys_key"); - - b.ToTable("temporary_keys", (string)null); - }); - modelBuilder.Entity("Foxnouns.Backend.Database.Models.Token", b => { b.Property("Id") diff --git a/Foxnouns.Backend/Database/Models/TemporaryKey.cs b/Foxnouns.Backend/Database/Models/TemporaryKey.cs deleted file mode 100644 index f83e515..0000000 --- a/Foxnouns.Backend/Database/Models/TemporaryKey.cs +++ /dev/null @@ -1,25 +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 TemporaryKey -{ - public long Id { get; init; } - public required string Key { get; init; } - public required string Value { get; set; } - public Instant Expires { get; init; } -} diff --git a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs index db0797c..2d3108b 100644 --- a/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs +++ b/Foxnouns.Backend/Extensions/ImageObjectExtensions.cs @@ -33,24 +33,20 @@ public static class ImageObjectExtensions Snowflake id, string hash, CancellationToken ct = default - ) => - await objectStorageService.RemoveObjectAsync( - MemberAvatarUpdateInvocable.Path(id, hash), - ct - ); + ) => 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(UserAvatarUpdateInvocable.Path(id, hash), ct); + ) => await objectStorageService.RemoveObjectAsync(UserAvatarUpdateJob.Path(id, hash), ct); public static async Task DeleteFlagAsync( this ObjectStorageService objectStorageService, string hash, CancellationToken ct = default - ) => await objectStorageService.RemoveObjectAsync(CreateFlagInvocable.Path(hash), ct); + ) => await objectStorageService.RemoveObjectAsync(CreateFlagJob.Path(hash), ct); public static async Task<(string Hash, Stream Image)> ConvertBase64UriToImage( string uri, diff --git a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs index 615cc3d..a4fb444 100644 --- a/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs +++ b/Foxnouns.Backend/Extensions/KeyCacheExtensions.cs @@ -23,23 +23,19 @@ namespace Foxnouns.Backend.Extensions; public static class KeyCacheExtensions { - public static async Task GenerateAuthStateAsync( - this KeyCacheService keyCacheService, - CancellationToken ct = default - ) + public static async Task GenerateAuthStateAsync(this KeyCacheService keyCacheService) { string state = AuthUtils.RandomToken(); - await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10), ct); + await keyCacheService.SetKeyAsync($"oauth_state:{state}", "", Duration.FromMinutes(10)); return state; } public static async Task ValidateAuthStateAsync( this KeyCacheService keyCacheService, - string state, - CancellationToken ct = default + string state ) { - string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}", ct: ct); + string? val = await keyCacheService.GetKeyAsync($"oauth_state:{state}"); if (val == null) throw new ApiError.BadRequest("Invalid OAuth state"); } @@ -47,63 +43,55 @@ public static class KeyCacheExtensions public static async Task GenerateRegisterEmailStateAsync( this KeyCacheService keyCacheService, string email, - Snowflake? userId = null, - CancellationToken ct = default + Snowflake? userId = null ) { string state = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync( $"email_state:{state}", new RegisterEmailState(email, userId), - Duration.FromDays(1), - ct + Duration.FromDays(1) ); return state; } public static async Task GetRegisterEmailStateAsync( this KeyCacheService keyCacheService, - string state, - CancellationToken ct = default - ) => await keyCacheService.GetKeyAsync($"email_state:{state}", ct: ct); + string state + ) => await keyCacheService.GetKeyAsync($"email_state:{state}"); public static async Task GenerateAddExtraAccountStateAsync( this KeyCacheService keyCacheService, AuthType authType, Snowflake userId, - string? instance = null, - CancellationToken ct = default + string? instance = null ) { string state = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync( $"add_account:{state}", new AddExtraAccountState(authType, userId, instance), - Duration.FromDays(1), - ct + Duration.FromDays(1) ); return state; } public static async Task GetAddExtraAccountStateAsync( this KeyCacheService keyCacheService, - string state, - CancellationToken ct = default - ) => await keyCacheService.GetKeyAsync($"add_account:{state}", true, ct); + string state + ) => await keyCacheService.GetKeyAsync($"add_account:{state}", true); public static async Task GenerateForgotPasswordStateAsync( this KeyCacheService keyCacheService, string email, - Snowflake userId, - CancellationToken ct = default + Snowflake userId ) { string state = AuthUtils.RandomToken(); await keyCacheService.SetKeyAsync( $"forgot_password:{state}", new ForgotPasswordState(email, userId), - Duration.FromHours(1), - ct + Duration.FromHours(1) ); return state; } @@ -111,14 +99,8 @@ public static class KeyCacheExtensions public static async Task GetForgotPasswordStateAsync( this KeyCacheService keyCacheService, string state, - bool delete = true, - CancellationToken ct = default - ) => - await keyCacheService.GetKeyAsync( - $"forgot_password:{state}", - delete, - ct - ); + bool delete = true + ) => await keyCacheService.GetKeyAsync($"forgot_password:{state}", delete); } public record RegisterEmailState( diff --git a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs index 07394f2..8db7a1b 100644 --- a/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs +++ b/Foxnouns.Backend/Extensions/WebApplicationExtensions.cs @@ -51,9 +51,12 @@ public static class WebApplicationExtensions "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); if (config.Logging.SeqLogUrl != null) @@ -112,12 +115,12 @@ public static class WebApplicationExtensions .AddSnowflakeGenerator() .AddSingleton() .AddSingleton() + .AddSingleton() .AddScoped() .AddScoped() .AddScoped() .AddScoped() .AddScoped() - .AddScoped() .AddScoped() .AddScoped() .AddScoped() @@ -126,10 +129,10 @@ public static class WebApplicationExtensions // Background services .AddHostedService() // Transient jobs - .AddTransient() - .AddTransient() - .AddTransient() - .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() + .AddTransient() // Legacy services .AddScoped() .AddScoped(); diff --git a/Foxnouns.Backend/Foxnouns.Backend.csproj b/Foxnouns.Backend/Foxnouns.Backend.csproj index c30f2b9..a8c21fb 100644 --- a/Foxnouns.Backend/Foxnouns.Backend.csproj +++ b/Foxnouns.Backend/Foxnouns.Backend.csproj @@ -12,6 +12,9 @@ + + + @@ -42,6 +45,7 @@ + diff --git a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs b/Foxnouns.Backend/Jobs/CreateDataExportJob.cs similarity index 93% rename from Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs rename to Foxnouns.Backend/Jobs/CreateDataExportJob.cs index becd858..3662e33 100644 --- a/Foxnouns.Backend/Jobs/CreateDataExportInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateDataExportJob.cs @@ -14,11 +14,11 @@ // along with this program. If not, see . using System.IO.Compression; using System.Net; -using Coravel.Invocable; 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; @@ -26,7 +26,7 @@ using NodaTime.Text; namespace Foxnouns.Backend.Jobs; -public class CreateDataExportInvocable( +public class CreateDataExportJob( DatabaseContext db, IClock clock, UserRendererService userRenderer, @@ -34,37 +34,41 @@ public class CreateDataExportInvocable( ObjectStorageService objectStorageService, ISnowflakeGenerator snowflakeGenerator, ILogger logger -) : IInvocable, IInvocableWithPayload +) { private static readonly HttpClient Client = new(); - private readonly ILogger _logger = logger.ForContext(); - public required CreateDataExportPayload Payload { get; set; } + private readonly ILogger _logger = logger.ForContext(); - public async Task Invoke() + public static void Enqueue(Snowflake userId) + { + BackgroundJob.Enqueue(j => j.InvokeAsync(userId)); + } + + public async Task InvokeAsync(Snowflake userId) { try { - await InvokeAsync(); + await InvokeAsyncInner(userId); } catch (Exception e) { - _logger.Error(e, "Error generating data export for user {UserId}", Payload.UserId); + _logger.Error(e, "Error generating data export for user {UserId}", userId); } } - private async Task InvokeAsync() + 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 == Payload.UserId); + .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", - Payload.UserId + userId ); return; } diff --git a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs index 1b8905b..e40bfa4 100644 --- a/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs +++ b/Foxnouns.Backend/Jobs/CreateFlagInvocable.cs @@ -12,49 +12,53 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Coravel.Invocable; 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 CreateFlagInvocable( +public class CreateFlagJob( DatabaseContext db, ObjectStorageService objectStorageService, ILogger logger -) : IInvocable, IInvocableWithPayload +) { - private readonly ILogger _logger = logger.ForContext(); - public required CreateFlagPayload Payload { get; set; } + private readonly ILogger _logger = logger.ForContext(); - public async Task Invoke() + 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 + payload.Id, + payload.UserId, + payload.ImageData.Length ); try { PrideFlag? flag = await db.PrideFlags.FirstOrDefaultAsync(f => - f.Id == Payload.Id && f.UserId == Payload.UserId + 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 + payload.Id ); return; } (string? hash, Stream? image) = await ImageObjectExtensions.ConvertBase64UriToImage( - Payload.ImageData, + payload.ImageData, 256, false ); @@ -68,7 +72,7 @@ public class CreateFlagInvocable( } catch (ArgumentException ae) { - _logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", Payload.Id, ae.Message); + _logger.Warning("Invalid data URI for flag {FlagId}: {Reason}", payload.Id, ae.Message); } throw new NotImplementedException(); diff --git a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs similarity index 86% rename from Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs rename to Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs index 01ec9e3..907dfc4 100644 --- a/Foxnouns.Backend/Jobs/MemberAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/MemberAvatarUpdateJob.cs @@ -12,29 +12,33 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Coravel.Invocable; 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 MemberAvatarUpdateInvocable( +public class MemberAvatarUpdateJob( DatabaseContext db, ObjectStorageService objectStorageService, ILogger logger -) : IInvocable, IInvocableWithPayload +) { - private readonly ILogger _logger = logger.ForContext(); - public required AvatarUpdatePayload Payload { get; set; } + private readonly ILogger _logger = logger.ForContext(); - public async Task Invoke() + public static void Enqueue(AvatarUpdatePayload payload) { - if (Payload.NewAvatar != null) - await UpdateMemberAvatarAsync(Payload.Id, Payload.NewAvatar); + 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); + await ClearMemberAvatarAsync(payload.Id); } private async Task UpdateMemberAvatarAsync(Snowflake id, string newAvatar) diff --git a/Foxnouns.Backend/Jobs/Payloads.cs b/Foxnouns.Backend/Jobs/Payloads.cs index 374a5b7..1f76ea2 100644 --- a/Foxnouns.Backend/Jobs/Payloads.cs +++ b/Foxnouns.Backend/Jobs/Payloads.cs @@ -19,5 +19,3 @@ namespace Foxnouns.Backend.Jobs; public record AvatarUpdatePayload(Snowflake Id, string? NewAvatar); public record CreateFlagPayload(Snowflake Id, Snowflake UserId, string ImageData); - -public record CreateDataExportPayload(Snowflake UserId); diff --git a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs b/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs similarity index 88% rename from Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs rename to Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs index 862d0da..1ab446c 100644 --- a/Foxnouns.Backend/Jobs/UserAvatarUpdateInvocable.cs +++ b/Foxnouns.Backend/Jobs/UserAvatarUpdateJob.cs @@ -12,29 +12,33 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -using Coravel.Invocable; 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 UserAvatarUpdateInvocable( +public class UserAvatarUpdateJob( DatabaseContext db, ObjectStorageService objectStorageService, ILogger logger -) : IInvocable, IInvocableWithPayload +) { - private readonly ILogger _logger = logger.ForContext(); - public required AvatarUpdatePayload Payload { get; set; } + private readonly ILogger _logger = logger.ForContext(); - public async Task Invoke() + public static void Enqueue(AvatarUpdatePayload payload) { - if (Payload.NewAvatar != null) - await UpdateUserAvatarAsync(Payload.Id, Payload.NewAvatar); + 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); + await ClearUserAvatarAsync(payload.Id); } private async Task UpdateUserAvatarAsync(Snowflake id, string newAvatar) diff --git a/Foxnouns.Backend/Program.cs b/Foxnouns.Backend/Program.cs index 0f1d9f1..b5bc338 100644 --- a/Foxnouns.Backend/Program.cs +++ b/Foxnouns.Backend/Program.cs @@ -19,6 +19,8 @@ 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; @@ -73,6 +75,18 @@ builder ); }); +builder + .Services.AddHangfire( + (services, c) => + { + c.UseRedisStorage( + services.GetRequiredService().Multiplexer, + new RedisStorageOptions { Prefix = "foxnouns_net:" } + ); + } + ) + .AddHangfireServer(); + builder.Services.AddOpenApi( "v2", options => @@ -109,6 +123,7 @@ if (config.Logging.SentryTracing) 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 diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 0163516..1ad825f 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -17,94 +17,42 @@ using Foxnouns.Backend.Database.Models; using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using NodaTime; +using StackExchange.Redis; namespace Foxnouns.Backend.Services; -public class KeyCacheService(DatabaseContext db, IClock clock, ILogger logger) +public class KeyCacheService(Config config) { - private readonly ILogger _logger = logger.ForContext(); + public ConnectionMultiplexer Multiplexer { get; } = + // ConnectionMultiplexer.Connect(config.Database.Redis); + ConnectionMultiplexer.Connect("127.0.0.1:6379"); - public Task SetKeyAsync( - string key, - string value, - Duration expireAfter, - CancellationToken ct = default - ) => SetKeyAsync(key, value, clock.GetCurrentInstant() + expireAfter, ct); + public async Task SetKeyAsync(string key, string value, Duration expireAfter) => + await Multiplexer + .GetDatabase() + .StringSetAsync(key, value, expiry: expireAfter.ToTimeSpan()); - public async Task SetKeyAsync( - string key, - string value, - Instant expires, - CancellationToken ct = default - ) - { - db.TemporaryKeys.Add( - new TemporaryKey - { - Expires = expires, - Key = key, - Value = value, - } - ); - await db.SaveChangesAsync(ct); - } + public async Task GetKeyAsync(string key, bool delete = false) => + delete + ? await Multiplexer.GetDatabase().StringGetDeleteAsync(key) + : await Multiplexer.GetDatabase().StringGetAsync(key); - public async Task GetKeyAsync( - string key, - bool delete = false, - CancellationToken ct = default - ) - { - TemporaryKey? value = await db.TemporaryKeys.FirstOrDefaultAsync(k => k.Key == key, ct); - if (value == null) - return null; + public async Task DeleteKeyAsync(string key) => + await Multiplexer.GetDatabase().KeyDeleteAsync(key); - if (delete) - await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct); + public Task DeleteExpiredKeysAsync(CancellationToken ct) => Task.CompletedTask; - return value.Value; - } - - public async Task DeleteKeyAsync(string key, CancellationToken ct = default) => - await db.TemporaryKeys.Where(k => k.Key == key).ExecuteDeleteAsync(ct); - - public async Task DeleteExpiredKeysAsync(CancellationToken ct) - { - int count = await db - .TemporaryKeys.Where(k => k.Expires < clock.GetCurrentInstant()) - .ExecuteDeleteAsync(ct); - if (count != 0) - _logger.Information("Removed {Count} expired keys from the database", count); - } - - public Task SetKeyAsync( - string key, - T obj, - Duration expiresAt, - CancellationToken ct = default - ) - where T : class => SetKeyAsync(key, obj, clock.GetCurrentInstant() + expiresAt, ct); - - public async Task SetKeyAsync( - string key, - T obj, - Instant expires, - CancellationToken ct = default - ) + public async Task SetKeyAsync(string key, T obj, Duration expiresAt) where T : class { string value = JsonConvert.SerializeObject(obj); - await SetKeyAsync(key, value, expires, ct); + await SetKeyAsync(key, value, expiresAt); } - public async Task GetKeyAsync( - string key, - bool delete = false, - CancellationToken ct = default - ) + public async Task GetKeyAsync(string key, bool delete = false) where T : class { - string? value = await GetKeyAsync(key, delete, ct); + string? value = await GetKeyAsync(key, delete); return value == null ? default : JsonConvert.DeserializeObject(value); } } diff --git a/Foxnouns.Backend/Services/ModerationService.cs b/Foxnouns.Backend/Services/ModerationService.cs index 4e2afe6..30d99ed 100644 --- a/Foxnouns.Backend/Services/ModerationService.cs +++ b/Foxnouns.Backend/Services/ModerationService.cs @@ -27,7 +27,6 @@ public class ModerationService( ILogger logger, DatabaseContext db, ISnowflakeGenerator snowflakeGenerator, - IQueue queue, IClock clock ) { @@ -181,9 +180,7 @@ public class ModerationService( target.CustomPreferences = []; target.ProfileFlags = []; - queue.QueueInvocableWithPayload( - new AvatarUpdatePayload(target.Id, null) - ); + UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(target.Id, null)); // TODO: also clear member profiles? @@ -264,10 +261,9 @@ public class ModerationService( targetMember.DisplayName = null; break; case FieldsToClear.Avatar: - queue.QueueInvocableWithPayload< - MemberAvatarUpdateInvocable, - AvatarUpdatePayload - >(new AvatarUpdatePayload(targetMember.Id, null)); + MemberAvatarUpdateJob.Enqueue( + new AvatarUpdatePayload(targetMember.Id, null) + ); break; case FieldsToClear.Bio: targetMember.Bio = null; @@ -306,10 +302,7 @@ public class ModerationService( targetUser.DisplayName = null; break; case FieldsToClear.Avatar: - queue.QueueInvocableWithPayload< - UserAvatarUpdateInvocable, - AvatarUpdatePayload - >(new AvatarUpdatePayload(targetUser.Id, null)); + UserAvatarUpdateJob.Enqueue(new AvatarUpdatePayload(targetUser.Id, null)); break; case FieldsToClear.Bio: targetUser.Bio = null; diff --git a/Foxnouns.Backend/packages.lock.json b/Foxnouns.Backend/packages.lock.json index e3799c6..3a7aec6 100644 --- a/Foxnouns.Backend/packages.lock.json +++ b/Foxnouns.Backend/packages.lock.json @@ -46,6 +46,37 @@ "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, )", @@ -278,6 +309,16 @@ "resolved": "3.1.6", "contentHash": "dHQ5jugF9v+5/LCVHCWVzaaIL6WOehqJy6eju/0VFYFPEj2WtqkGPoEV9EVQP83dHsdoqYaTuWpZdwAd37UwfA==" }, + "StackExchange.Redis": { + "type": "Direct", + "requested": "[2.8.24, )", + "resolved": "2.8.24", + "contentHash": "GWllmsFAtLyhm4C47cOCipGxyEi1NQWTFUHXnJ8hiHOsK/bH3T5eLkWPVW+LRL6jDiB3g3izW3YEHgLuPoJSyA==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Pipelines.Sockets.Unofficial": "2.2.8" + } + }, "System.Text.Json": { "type": "Direct", "requested": "[9.0.2, )", @@ -317,6 +358,33 @@ "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", @@ -684,6 +752,14 @@ "Npgsql": "9.0.2" } }, + "Pipelines.Sockets.Unofficial": { + "type": "Transitive", + "resolved": "2.2.8", + "contentHash": "zG2FApP5zxSx6OcdJQLbZDk2AVlN2BNQD6MorwIfV6gVj0RRxWPEp2LXAxqDGZqeNV1Zp0BNPcNaey/GXmTdvQ==", + "dependencies": { + "System.IO.Pipelines": "5.0.1" + } + }, "Sentry": { "type": "Transitive", "resolved": "5.2.0", From f99d10ecf01881154bf95a8eb14c0fdfb9e21c6c Mon Sep 17 00:00:00 2001 From: sam Date: Tue, 4 Mar 2025 17:25:07 +0100 Subject: [PATCH 243/261] fix(backend): don't hardcode redis URL, add redis to docker compose --- Foxnouns.Backend/Services/KeyCacheService.cs | 3 +-- docker-compose.yml | 10 +++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Foxnouns.Backend/Services/KeyCacheService.cs b/Foxnouns.Backend/Services/KeyCacheService.cs index 1ad825f..228a8fc 100644 --- a/Foxnouns.Backend/Services/KeyCacheService.cs +++ b/Foxnouns.Backend/Services/KeyCacheService.cs @@ -24,8 +24,7 @@ namespace Foxnouns.Backend.Services; public class KeyCacheService(Config config) { public ConnectionMultiplexer Multiplexer { get; } = - // ConnectionMultiplexer.Connect(config.Database.Redis); - ConnectionMultiplexer.Connect("127.0.0.1:6379"); + ConnectionMultiplexer.Connect(config.Database.Redis); public async Task SetKeyAsync(string key, string value, Duration expireAfter) => await Multiplexer diff --git a/docker-compose.yml b/docker-compose.yml index 751d919..084bcd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: 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" @@ -31,7 +32,7 @@ services: rate: image: rate - build: ./rate + build: ./Foxnouns.RateLimiter environment: - "PORT=5003" restart: unless-stopped @@ -52,6 +53,12 @@ services: 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 @@ -67,3 +74,4 @@ volumes: caddy_data: caddy_config: postgres_data: + redict_data: From dd9d35249c6c2ce1c17cce181b058f6f720f12b1 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 5 Mar 2025 01:18:21 +0100 Subject: [PATCH 244/261] feat(frontend): notifications --- .../src/lib/api/models/moderation.ts | 9 ++++ .../src/lib/components/Navbar.svelte | 17 +++++++- .../components/settings/Notification.svelte | 43 +++++++++++++++++++ .../src/lib/i18n/locales/en.json | 13 +++++- .../src/routes/+layout.server.ts | 10 ++++- Foxnouns.Frontend/src/routes/+layout.svelte | 2 +- .../settings/notifications/+page.server.ts | 11 +++++ .../settings/notifications/+page.svelte | 16 +++++++ .../notifications/ack/[id]/+server.ts | 14 ++++++ 9 files changed, 129 insertions(+), 6 deletions(-) create mode 100644 Foxnouns.Frontend/src/lib/components/settings/Notification.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/notifications/+page.server.ts create mode 100644 Foxnouns.Frontend/src/routes/settings/notifications/+page.svelte create mode 100644 Foxnouns.Frontend/src/routes/settings/notifications/ack/[id]/+server.ts diff --git a/Foxnouns.Frontend/src/lib/api/models/moderation.ts b/Foxnouns.Frontend/src/lib/api/models/moderation.ts index eee9382..689e9b8 100644 --- a/Foxnouns.Frontend/src/lib/api/models/moderation.ts +++ b/Foxnouns.Frontend/src/lib/api/models/moderation.ts @@ -112,3 +112,12 @@ export enum ClearableField { 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/components/Navbar.svelte b/Foxnouns.Frontend/src/lib/components/Navbar.svelte index edfbd1a..2074347 100644 --- a/Foxnouns.Frontend/src/lib/components/Navbar.svelte +++ b/Foxnouns.Frontend/src/lib/components/Navbar.svelte @@ -13,13 +13,21 @@ import Logo from "$components/Logo.svelte"; import { t } from "$lib/i18n"; - type Props = { user: MeUser | null; meta: Meta }; - let { user, meta }: Props = $props(); + type Props = { user: MeUser | null; meta: Meta; unreadNotifications?: boolean }; + let { user, meta, unreadNotifications }: Props = $props(); let isOpen = $state(true); const toggleMenu = () => (isOpen = !isOpen); +{#if user && unreadNotifications} +
    + {$t("nav.unread-notification-text")} +
    + {$t("nav.unread-notification-link")} +
    +{/if} + {#if user && user.deleted}
    {#if user.suspended} @@ -87,6 +95,11 @@ background-color: var(--bs-danger-bg-subtle); } + .notification-alert { + color: var(--bs-warning-text-emphasis); + background-color: var(--bs-warning-bg-subtle); + } + /* These exact values make it look almost identical to the SVG version, which is what we want */ #beta-text { font-size: 0.7em; diff --git a/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte b/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte new file mode 100644 index 0000000..c452f4c --- /dev/null +++ b/Foxnouns.Frontend/src/lib/components/settings/Notification.svelte @@ -0,0 +1,43 @@ + + +
    +
    +
    + +
    +

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

    +
    +
    +
    + +
    diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index fe10b04..9f54943 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -9,7 +9,9 @@ "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" + "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": { @@ -329,7 +331,9 @@ }, "alert": { "auth-method-remove-success": "Successfully unlinked account!", - "auth-required": "You must log in to access this page." + "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", @@ -342,5 +346,10 @@ "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}}" } } diff --git a/Foxnouns.Frontend/src/routes/+layout.server.ts b/Foxnouns.Frontend/src/routes/+layout.server.ts index 82f3cb2..2debd7c 100644 --- a/Foxnouns.Frontend/src/routes/+layout.server.ts +++ b/Foxnouns.Frontend/src/routes/+layout.server.ts @@ -2,16 +2,24 @@ 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); @@ -19,5 +27,5 @@ export const load = (async ({ fetch, cookies }) => { } const meta = await apiRequest("GET", "/meta", { fetch, cookies }); - return { meta, meUser, token }; + return { meta, meUser, token, unreadNotifications }; }) satisfies LayoutServerLoad; diff --git a/Foxnouns.Frontend/src/routes/+layout.svelte b/Foxnouns.Frontend/src/routes/+layout.svelte index e5be130..b991f8a 100644 --- a/Foxnouns.Frontend/src/routes/+layout.svelte +++ b/Foxnouns.Frontend/src/routes/+layout.svelte @@ -11,7 +11,7 @@
    - + {@render children?.()}
    diff --git a/Foxnouns.Frontend/src/routes/settings/notifications/+page.server.ts b/Foxnouns.Frontend/src/routes/settings/notifications/+page.server.ts new file mode 100644 index 0000000..40470bd --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/notifications/+page.server.ts @@ -0,0 +1,11 @@ +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 new file mode 100644 index 0000000..a33d734 --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/notifications/+page.svelte @@ -0,0 +1,16 @@ + + + + +{#each data.notifications as notification (notification.id)} + +{:else} + You have 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 new file mode 100644 index 0000000..a9a22fc --- /dev/null +++ b/Foxnouns.Frontend/src/routes/settings/notifications/ack/[id]/+server.ts @@ -0,0 +1,14 @@ +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"); +}; From 7d0df67c067eeb09f5f4c17af83b86b074f5ef71 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 5 Mar 2025 15:13:26 +0100 Subject: [PATCH 245/261] fix(frontend): fix moving pronouns --- .../src/lib/components/editor/PronounEntryEditor.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte index bcf5c15..64b354e 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/PronounEntryEditor.svelte @@ -48,7 +48,7 @@ icon="chevron-down" color="secondary" tooltip={$t("editor.move-entry-down")} - onclick={() => moveValue(index, true)} + onclick={() => moveValue(index, false)} /> From 790b39f730a6df3e42d33225f01f7e5839cc8729 Mon Sep 17 00:00:00 2001 From: sam Date: Wed, 5 Mar 2025 15:13:44 +0100 Subject: [PATCH 246/261] fix(frontend): consistency in the editor --- .../src/lib/components/editor/AvatarEditor.svelte | 2 +- .../src/lib/components/editor/CustomPreferencesNotice.svelte | 2 +- .../src/lib/components/editor/FieldsEditor.svelte | 2 ++ .../src/routes/settings/members/[id]/fields/+page.svelte | 5 ----- .../src/routes/settings/profile/fields/+page.svelte | 5 ----- 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte b/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte index a945bbf..288bcc5 100644 --- a/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte +++ b/Foxnouns.Frontend/src/lib/components/editor/AvatarEditor.svelte @@ -130,7 +130,7 @@ accept="image/png, image/jpeg, image/gif, image/webp" />
    diff --git a/Foxnouns.Frontend/src/lib/i18n/locales/en.json b/Foxnouns.Frontend/src/lib/i18n/locales/en.json index 9f54943..dc74d2f 100644 --- a/Foxnouns.Frontend/src/lib/i18n/locales/en.json +++ b/Foxnouns.Frontend/src/lib/i18n/locales/en.json @@ -350,6 +350,8 @@ "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}}" + "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/routes/+page.svelte b/Foxnouns.Frontend/src/routes/+page.svelte index 47ab0e5..4fd36dc 100644 --- a/Foxnouns.Frontend/src/routes/+page.svelte +++ b/Foxnouns.Frontend/src/routes/+page.svelte @@ -1,4 +1,5 @@ {$t("title.settings")} • pronouns.cc +{#if data.meta.notice} +
    + +
    +{/if} +

    - - - - {#each arr as flag (flag.id)} - - - - {flag.name} - - - {#if lastEditedFlag === flag.id}{/if} - - - {/each} - +
    + +
    + + + +{#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} From a89a5b3494a9c09070ea2e370a8f8569e319cc49 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 17 Apr 2025 15:03:38 +0200 Subject: [PATCH 259/261] chore(frontend): switch from pnpm to npm --- .husky/task-runner.json | 17 +- Foxnouns.Frontend/Dockerfile | 10 +- Foxnouns.Frontend/icons.js | 2 +- Foxnouns.Frontend/package-lock.json | 6354 +++++++++++++++++++++++++++ Foxnouns.Frontend/package.json | 1 - Foxnouns.Frontend/pnpm-lock.yaml | 4219 ------------------ build.sh | 8 +- package.json | 7 +- 8 files changed, 6369 insertions(+), 4249 deletions(-) create mode 100644 Foxnouns.Frontend/package-lock.json delete mode 100644 Foxnouns.Frontend/pnpm-lock.yaml diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 576b8bc..72e6fea 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -3,12 +3,8 @@ "tasks": [ { "name": "run-prettier", - "command": "pnpm", - "args": [ - "prettier", - "-w", - "${staged}" - ], + "command": "npx", + "args": ["prettier", "-w", "${staged}"], "include": [ "Foxnouns.Frontend/**/*.ts", "Foxnouns.Frontend/**/*.json", @@ -22,13 +18,8 @@ { "name": "run-csharpier", "command": "dotnet", - "args": [ - "csharpier", - "${staged}" - ], - "include": [ - "**/*.cs" - ] + "args": ["csharpier", "${staged}"], + "include": ["**/*.cs"] } ] } diff --git a/Foxnouns.Frontend/Dockerfile b/Foxnouns.Frontend/Dockerfile index 6470b27..2a86593 100644 --- a/Foxnouns.Frontend/Dockerfile +++ b/Foxnouns.Frontend/Dockerfile @@ -1,9 +1,5 @@ FROM docker.io/node:23-slim -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - COPY ./Foxnouns.Frontend /app COPY ./docker/frontend.env /app/.env.local WORKDIR /app @@ -11,7 +7,7 @@ WORKDIR /app ENV PRIVATE_API_HOST=http://rate:5003/api ENV PRIVATE_INTERNAL_API_HOST=http://backend:5000/api -RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile -RUN pnpm run build +RUN npm ci +RUN npm run build -CMD ["pnpm", "node", "build/index.js"] +CMD ["node", "build/index.js"] diff --git a/Foxnouns.Frontend/icons.js b/Foxnouns.Frontend/icons.js index 3efc0a4..0c9ffc4 100644 --- a/Foxnouns.Frontend/icons.js +++ b/Foxnouns.Frontend/icons.js @@ -1,6 +1,6 @@ // 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 `pnpm node icons.js` in the frontend directory. +// 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" }; diff --git a/Foxnouns.Frontend/package-lock.json b/Foxnouns.Frontend/package-lock.json new file mode 100644 index 0000000..ac5f6f1 --- /dev/null +++ b/Foxnouns.Frontend/package-lock.json @@ -0,0 +1,6354 @@ +{ + "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 index ce82f74..507e11f 100644 --- a/Foxnouns.Frontend/package.json +++ b/Foxnouns.Frontend/package.json @@ -38,7 +38,6 @@ "typescript-eslint": "^8.29.0", "vite": "^6.2.5" }, - "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c", "dependencies": { "@fontsource/firago": "^5.2.5", "@sentry/sveltekit": "^9.11.0", diff --git a/Foxnouns.Frontend/pnpm-lock.yaml b/Foxnouns.Frontend/pnpm-lock.yaml deleted file mode 100644 index d6d5639..0000000 --- a/Foxnouns.Frontend/pnpm-lock.yaml +++ /dev/null @@ -1,4219 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@fontsource/firago': - specifier: ^5.2.5 - version: 5.2.5 - '@sentry/sveltekit': - specifier: ^9.11.0 - version: 9.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - base64-arraybuffer: - specifier: ^1.0.2 - version: 1.0.2 - bootstrap-icons: - specifier: ^1.11.3 - version: 1.11.3 - luxon: - specifier: ^3.6.1 - version: 3.6.1 - markdown-it: - specifier: ^14.1.0 - version: 14.1.0 - minidenticons: - specifier: ^4.2.1 - version: 4.2.1 - pretty-bytes: - specifier: ^6.1.1 - version: 6.1.1 - sanitize-html: - specifier: ^2.15.0 - version: 2.15.0 - svelte-tippy: - specifier: ^1.3.2 - version: 1.3.2 - tippy.js: - specifier: ^6.3.7 - version: 6.3.7 - tslog: - specifier: ^4.9.3 - version: 4.9.3 - devDependencies: - '@sveltejs/adapter-node': - specifier: ^5.2.12 - version: 5.2.12(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3))) - '@sveltejs/kit': - specifier: ^2.20.4 - version: 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - '@sveltejs/vite-plugin-svelte': - specifier: ^5.0.3 - version: 5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - '@sveltestrap/sveltestrap': - specifier: ^7.1.0 - version: 7.1.0(svelte@5.25.7) - '@types/eslint': - specifier: ^9.6.1 - version: 9.6.1 - '@types/luxon': - specifier: ^3.6.2 - version: 3.6.2 - '@types/markdown-it': - specifier: ^14.1.2 - version: 14.1.2 - '@types/sanitize-html': - specifier: ^2.15.0 - version: 2.15.0 - bootstrap: - specifier: ^5.3.5 - version: 5.3.5(@popperjs/core@2.11.8) - dotenv: - specifier: ^16.4.7 - version: 16.4.7 - eslint: - specifier: ^9.24.0 - version: 9.24.0 - eslint-config-prettier: - specifier: ^9.1.0 - version: 9.1.0(eslint@9.24.0) - eslint-plugin-svelte: - specifier: ^2.46.1 - version: 2.46.1(eslint@9.24.0)(svelte@5.25.7) - globals: - specifier: ^16.0.0 - version: 16.0.0 - prettier: - specifier: ^3.5.3 - version: 3.5.3 - prettier-plugin-svelte: - specifier: ^3.3.3 - version: 3.3.3(prettier@3.5.3)(svelte@5.25.7) - sass: - specifier: ^1.86.3 - version: 1.86.3 - svelte: - specifier: ^5.25.7 - version: 5.25.7 - svelte-bootstrap-icons: - specifier: ^3.1.2 - version: 3.1.2 - svelte-check: - specifier: ^4.1.5 - version: 4.1.5(picomatch@4.0.2)(svelte@5.25.7)(typescript@5.8.3) - svelte-easy-crop: - specifier: ^4.0.1 - version: 4.0.1(svelte@5.25.7) - sveltekit-i18n: - specifier: ^2.4.2 - version: 2.4.2(svelte@5.25.7) - typescript: - specifier: ^5.8.3 - version: 5.8.3 - typescript-eslint: - specifier: ^8.29.0 - version: 8.29.0(eslint@9.24.0)(typescript@5.8.3) - vite: - specifier: ^6.2.5 - version: 6.2.5(@types/node@22.14.0)(sass@1.86.3) - -packages: - - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} - - '@babel/code-frame@7.26.2': - resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} - engines: {node: '>=6.9.0'} - - '@babel/compat-data@7.26.8': - resolution: {integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==} - engines: {node: '>=6.9.0'} - - '@babel/core@7.26.10': - resolution: {integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==} - engines: {node: '>=6.9.0'} - - '@babel/generator@7.27.0': - resolution: {integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-compilation-targets@7.27.0': - resolution: {integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-imports@7.25.9': - resolution: {integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==} - engines: {node: '>=6.9.0'} - - '@babel/helper-module-transforms@7.26.0': - resolution: {integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - - '@babel/helper-string-parser@7.25.9': - resolution: {integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-identifier@7.25.9': - resolution: {integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==} - engines: {node: '>=6.9.0'} - - '@babel/helper-validator-option@7.25.9': - resolution: {integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==} - engines: {node: '>=6.9.0'} - - '@babel/helpers@7.27.0': - resolution: {integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.26.9': - resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/parser@7.27.0': - resolution: {integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==} - engines: {node: '>=6.0.0'} - hasBin: true - - '@babel/template@7.27.0': - resolution: {integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==} - engines: {node: '>=6.9.0'} - - '@babel/traverse@7.27.0': - resolution: {integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==} - engines: {node: '>=6.9.0'} - - '@babel/types@7.27.0': - resolution: {integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==} - engines: {node: '>=6.9.0'} - - '@esbuild/aix-ppc64@0.25.2': - resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.25.2': - resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.25.2': - resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.25.2': - resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.25.2': - resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.25.2': - resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.25.2': - resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.25.2': - resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.25.2': - resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.25.2': - resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.25.2': - resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.25.2': - resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.25.2': - resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.25.2': - resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.25.2': - resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.25.2': - resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.25.2': - resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.25.2': - resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.25.2': - resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.25.2': - resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.25.2': - resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/sunos-x64@0.25.2': - resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.25.2': - resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.25.2': - resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.25.2': - resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@eslint-community/eslint-utils@4.5.1': - resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - - '@eslint/config-array@0.20.0': - resolution: {integrity: sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/config-helpers@0.2.1': - resolution: {integrity: sha512-RI17tsD2frtDu/3dmI7QRrD4bedNKPM08ziRYaC5AhkGrzIAJelm9kJU1TznK+apx6V+cqRz8tfpEeG3oIyjxw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.12.0': - resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/core@0.13.0': - resolution: {integrity: sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/js@9.24.0': - resolution: {integrity: sha512-uIY/y3z0uvOGX8cp1C2fiC4+ZmBhp6yZWkojtHL1YEMnRt1Y63HB9TM17proGEmeG7HeUY+UP36F0aknKYTpYA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/object-schema@2.1.6': - resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@eslint/plugin-kit@0.2.8': - resolution: {integrity: sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@fontsource/firago@5.2.5': - resolution: {integrity: sha512-11vSR9Vyh0Tp/ChtheVSsK3yP9UGUUV5xJCdSOE8xNsQH/NZIGF36p0aeFTQ6uzBBaxaVjCGm0LEIFmxAwJoRw==} - - '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} - engines: {node: '>=18.18.0'} - - '@humanfs/node@0.16.6': - resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} - engines: {node: '>=18.18.0'} - - '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - - '@humanwhocodes/retry@0.3.1': - resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} - engines: {node: '>=18.18'} - - '@humanwhocodes/retry@0.4.2': - resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} - engines: {node: '>=18.18'} - - '@jridgewell/gen-mapping@0.3.8': - resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} - engines: {node: '>=6.0.0'} - - '@jridgewell/resolve-uri@3.1.2': - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - - '@opentelemetry/api-logs@0.57.2': - resolution: {integrity: sha512-uIX52NnTM0iBh84MShlpouI7UKqkZ7MrUszTmaypHBu4r7NofznSnQRfJ+uUeDtQDj6w8eFGg5KBLDAwAPz1+A==} - engines: {node: '>=14'} - - '@opentelemetry/api@1.9.0': - resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} - engines: {node: '>=8.0.0'} - - '@opentelemetry/context-async-hooks@1.30.1': - resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/core@1.30.1': - resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/instrumentation-amqplib@0.46.1': - resolution: {integrity: sha512-AyXVnlCf/xV3K/rNumzKxZqsULyITJH6OVLiW6730JPRqWA7Zc9bvYoVNpN6iOpTU8CasH34SU/ksVJmObFibQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-connect@0.43.1': - resolution: {integrity: sha512-ht7YGWQuV5BopMcw5Q2hXn3I8eG8TH0J/kc/GMcW4CuNTgiP6wCu44BOnucJWL3CmFWaRHI//vWyAhaC8BwePw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-dataloader@0.16.1': - resolution: {integrity: sha512-K/qU4CjnzOpNkkKO4DfCLSQshejRNAJtd4esgigo/50nxCB6XCyi1dhAblUHM9jG5dRm8eu0FB+t87nIo99LYQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-express@0.47.1': - resolution: {integrity: sha512-QNXPTWteDclR2B4pDFpz0TNghgB33UMjUt14B+BZPmtH1MwUFAfLHBaP5If0Z5NZC+jaH8oF2glgYjrmhZWmSw==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fastify@0.44.2': - resolution: {integrity: sha512-arSp97Y4D2NWogoXRb8CzFK3W2ooVdvqRRtQDljFt9uC3zI6OuShgey6CVFC0JxT1iGjkAr1r4PDz23mWrFULQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-fs@0.19.1': - resolution: {integrity: sha512-6g0FhB3B9UobAR60BGTcXg4IHZ6aaYJzp0Ki5FhnxyAPt8Ns+9SSvgcrnsN2eGmk3RWG5vYycUGOEApycQL24A==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-generic-pool@0.43.1': - resolution: {integrity: sha512-M6qGYsp1cURtvVLGDrPPZemMFEbuMmCXgQYTReC/IbimV5sGrLBjB+/hANUpRZjX67nGLdKSVLZuQQAiNz+sww==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-graphql@0.47.1': - resolution: {integrity: sha512-EGQRWMGqwiuVma8ZLAZnExQ7sBvbOx0N/AE/nlafISPs8S+QtXX+Viy6dcQwVWwYHQPAcuY3bFt3xgoAwb4ZNQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-hapi@0.45.2': - resolution: {integrity: sha512-7Ehow/7Wp3aoyCrZwQpU7a2CnoMq0XhIcioFuKjBb0PLYfBfmTsFTUyatlHu0fRxhwcRsSQRTvEhmZu8CppBpQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-http@0.57.2': - resolution: {integrity: sha512-1Uz5iJ9ZAlFOiPuwYg29Bf7bJJc/GeoeJIFKJYQf67nTVKFe8RHbEtxgkOmK4UGZNHKXcpW4P8cWBYzBn1USpg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-ioredis@0.47.1': - resolution: {integrity: sha512-OtFGSN+kgk/aoKgdkKQnBsQFDiG8WdCxu+UrHr0bXScdAmtSzLSraLo7wFIb25RVHfRWvzI5kZomqJYEg/l1iA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-kafkajs@0.7.1': - resolution: {integrity: sha512-OtjaKs8H7oysfErajdYr1yuWSjMAectT7Dwr+axIoZqT9lmEOkD/H/3rgAs8h/NIuEi2imSXD+vL4MZtOuJfqQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-knex@0.44.1': - resolution: {integrity: sha512-U4dQxkNhvPexffjEmGwCq68FuftFK15JgUF05y/HlK3M6W/G2iEaACIfXdSnwVNe9Qh0sPfw8LbOPxrWzGWGMQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-koa@0.47.1': - resolution: {integrity: sha512-l/c+Z9F86cOiPJUllUCt09v+kICKvT+Vg1vOAJHtHPsJIzurGayucfCMq2acd/A/yxeNWunl9d9eqZ0G+XiI6A==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-lru-memoizer@0.44.1': - resolution: {integrity: sha512-5MPkYCvG2yw7WONEjYj5lr5JFehTobW7wX+ZUFy81oF2lr9IPfZk9qO+FTaM0bGEiymwfLwKe6jE15nHn1nmHg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongodb@0.52.0': - resolution: {integrity: sha512-1xmAqOtRUQGR7QfJFfGV/M2kC7wmI2WgZdpru8hJl3S0r4hW0n3OQpEHlSGXJAaNFyvT+ilnwkT+g5L4ljHR6g==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mongoose@0.46.1': - resolution: {integrity: sha512-3kINtW1LUTPkiXFRSSBmva1SXzS/72we/jL22N+BnF3DFcoewkdkHPYOIdAAk9gSicJ4d5Ojtt1/HeibEc5OQg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql2@0.45.2': - resolution: {integrity: sha512-h6Ad60FjCYdJZ5DTz1Lk2VmQsShiViKe0G7sYikb0GHI0NVvApp2XQNRHNjEMz87roFttGPLHOYVPlfy+yVIhQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-mysql@0.45.1': - resolution: {integrity: sha512-TKp4hQ8iKQsY7vnp/j0yJJ4ZsP109Ht6l4RHTj0lNEG1TfgTrIH5vJMbgmoYXWzNHAqBH2e7fncN12p3BP8LFg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-pg@0.51.1': - resolution: {integrity: sha512-QxgjSrxyWZc7Vk+qGSfsejPVFL1AgAJdSBMYZdDUbwg730D09ub3PXScB9d04vIqPriZ+0dqzjmQx0yWKiCi2Q==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-redis-4@0.46.1': - resolution: {integrity: sha512-UMqleEoabYMsWoTkqyt9WAzXwZ4BlFZHO40wr3d5ZvtjKCHlD4YXLm+6OLCeIi/HkX7EXvQaz8gtAwkwwSEvcQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-tedious@0.18.1': - resolution: {integrity: sha512-5Cuy/nj0HBaH+ZJ4leuD7RjgvA844aY2WW+B5uLcWtxGjRZl3MNLuxnNg5DYWZNPO+NafSSnra0q49KWAHsKBg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/instrumentation-undici@0.10.1': - resolution: {integrity: sha512-rkOGikPEyRpMCmNu9AQuV5dtRlDmJp2dK5sw8roVshAGoB6hH/3QjDtRhdwd75SsJwgynWUNRUYe0wAkTo16tQ==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.7.0 - - '@opentelemetry/instrumentation@0.57.2': - resolution: {integrity: sha512-BdBGhQBh8IjZ2oIIX6F2/Q3LKm/FDDKi6ccYKcBTeilh6SNdNKveDOLk73BkSJjQLJk6qe4Yh+hHw1UPhCDdrg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.3.0 - - '@opentelemetry/redis-common@0.36.2': - resolution: {integrity: sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==} - engines: {node: '>=14'} - - '@opentelemetry/resources@1.30.1': - resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/sdk-trace-base@1.30.1': - resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': '>=1.0.0 <1.10.0' - - '@opentelemetry/semantic-conventions@1.28.0': - resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} - engines: {node: '>=14'} - - '@opentelemetry/semantic-conventions@1.30.0': - resolution: {integrity: sha512-4VlGgo32k2EQ2wcCY3vEU28A0O13aOtHz3Xt2/2U5FAh9EfhD6t6DqL5Z6yAnRCntbTFDU4YfbpyzSlHNWycPw==} - engines: {node: '>=14'} - - '@opentelemetry/sql-common@0.40.1': - resolution: {integrity: sha512-nSDlnHSqzC3pXn/wZEZVLuAuJ1MYMXPBwtv2qAbCa3847SaHItdE7SzUq/Jtb0KZmh1zfAbNi3AAMjztTT4Ugg==} - engines: {node: '>=14'} - peerDependencies: - '@opentelemetry/api': ^1.1.0 - - '@parcel/watcher-android-arm64@2.5.1': - resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [android] - - '@parcel/watcher-darwin-arm64@2.5.1': - resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [darwin] - - '@parcel/watcher-darwin-x64@2.5.1': - resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [darwin] - - '@parcel/watcher-freebsd-x64@2.5.1': - resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [freebsd] - - '@parcel/watcher-linux-arm-glibc@2.5.1': - resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm-musl@2.5.1': - resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} - engines: {node: '>= 10.0.0'} - cpu: [arm] - os: [linux] - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-arm64-musl@2.5.1': - resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [linux] - - '@parcel/watcher-linux-x64-glibc@2.5.1': - resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-linux-x64-musl@2.5.1': - resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [linux] - - '@parcel/watcher-win32-arm64@2.5.1': - resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} - engines: {node: '>= 10.0.0'} - cpu: [arm64] - os: [win32] - - '@parcel/watcher-win32-ia32@2.5.1': - resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} - engines: {node: '>= 10.0.0'} - cpu: [ia32] - os: [win32] - - '@parcel/watcher-win32-x64@2.5.1': - resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} - engines: {node: '>= 10.0.0'} - cpu: [x64] - os: [win32] - - '@parcel/watcher@2.5.1': - resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} - engines: {node: '>= 10.0.0'} - - '@polka/url@1.0.0-next.28': - resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} - - '@popperjs/core@2.11.8': - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - - '@prisma/instrumentation@6.5.0': - resolution: {integrity: sha512-morJDtFRoAp5d/KENEm+K6Y3PQcn5bCvpJ5a9y3V3DNMrNy/ZSn2zulPGj+ld+Xj2UYVoaMJ8DpBX/o6iF6OiA==} - peerDependencies: - '@opentelemetry/api': ^1.8 - - '@rollup/plugin-commonjs@28.0.3': - resolution: {integrity: sha512-pyltgilam1QPdn+Zd9gaCfOLcnjMEJ9gV+bTw6/r73INdvzf1ah9zLIJBm+kW7R6IUFIQ1YO+VqZtYxZNWFPEQ==} - engines: {node: '>=16.0.0 || 14 >= 14.17'} - peerDependencies: - rollup: ^2.68.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-json@6.1.0': - resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/plugin-node-resolve@16.0.1': - resolution: {integrity: sha512-tk5YCxJWIG81umIvNkSod2qK5KyQW19qcBF/B78n1bjtOON6gzKoVeSzAE8yHCZEDmqkHKkxplExA8KzdJLJpA==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^2.78.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/pluginutils@5.1.4': - resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 - peerDependenciesMeta: - rollup: - optional: true - - '@rollup/rollup-android-arm-eabi@4.39.0': - resolution: {integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.39.0': - resolution: {integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.39.0': - resolution: {integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.39.0': - resolution: {integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.39.0': - resolution: {integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.39.0': - resolution: {integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.39.0': - resolution: {integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm-musleabihf@4.39.0': - resolution: {integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==} - cpu: [arm] - os: [linux] - - '@rollup/rollup-linux-arm64-gnu@4.39.0': - resolution: {integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-arm64-musl@4.39.0': - resolution: {integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==} - cpu: [arm64] - os: [linux] - - '@rollup/rollup-linux-loongarch64-gnu@4.39.0': - resolution: {integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==} - cpu: [loong64] - os: [linux] - - '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': - resolution: {integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==} - cpu: [ppc64] - os: [linux] - - '@rollup/rollup-linux-riscv64-gnu@4.39.0': - resolution: {integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-riscv64-musl@4.39.0': - resolution: {integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.39.0': - resolution: {integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==} - cpu: [s390x] - os: [linux] - - '@rollup/rollup-linux-x64-gnu@4.39.0': - resolution: {integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-linux-x64-musl@4.39.0': - resolution: {integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==} - cpu: [x64] - os: [linux] - - '@rollup/rollup-win32-arm64-msvc@4.39.0': - resolution: {integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.39.0': - resolution: {integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.39.0': - resolution: {integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==} - cpu: [x64] - os: [win32] - - '@sentry-internal/browser-utils@9.11.0': - resolution: {integrity: sha512-XS71kRf7lw5St/Jc9G2Viy1cKgqGoPHqUAykXEtFt38JVXdf1TY/dSbKv/PAgNqMvC1xvdTsN0HF/81o7DNUEA==} - engines: {node: '>=18'} - - '@sentry-internal/feedback@9.11.0': - resolution: {integrity: sha512-50KiRmrF1Ldr+KoRawqcCYVk7TAVP8K/I81Jk9YWwlp1+Pu1ArpYDmTNCLXTgoyiyO38aHefKGZJX6AKFuSsUQ==} - engines: {node: '>=18'} - - '@sentry-internal/replay-canvas@9.11.0': - resolution: {integrity: sha512-ZcRg8TWfF0ucjK2i+4TY/blRNJ7YKrgMpx19pFj6eCOJ1K8geSkAFPIfDHcQEwIU1ZTN+HiCwx0JvTI9YZxjfg==} - engines: {node: '>=18'} - - '@sentry-internal/replay@9.11.0': - resolution: {integrity: sha512-0k24h58O/2VQw1dwT/zQiWvUzLNQxpxbrVN/MYPT7czSEhI+1bX8fxMHXZORl2JqhetImMXzxH3XkuHQPEqQMg==} - engines: {node: '>=18'} - - '@sentry/babel-plugin-component-annotate@3.2.4': - resolution: {integrity: sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA==} - engines: {node: '>= 14'} - - '@sentry/browser@9.11.0': - resolution: {integrity: sha512-DSDj8wQJoiLqqOcntl+7phjd8l8KN9A0vaV7mZNHWbrHU3MVwXqTyLyERRLC6wi0t7F5kqczqa3xLmKjK/fMZg==} - engines: {node: '>=18'} - - '@sentry/bundler-plugin-core@3.2.4': - resolution: {integrity: sha512-YMj9XW5W2JA89EeweE7CPKLDz245LBsI1JhCmqpt/bjSvmsSIAAPsLYnvIJBS3LQFm0OhtG8NB54PTi96dAcMA==} - engines: {node: '>= 14'} - - '@sentry/cli-darwin@2.42.2': - resolution: {integrity: sha512-GtJSuxER7Vrp1IpxdUyRZzcckzMnb4N5KTW7sbTwUiwqARRo+wxS+gczYrS8tdgtmXs5XYhzhs+t4d52ITHMIg==} - engines: {node: '>=10'} - os: [darwin] - - '@sentry/cli-linux-arm64@2.42.2': - resolution: {integrity: sha512-BOxzI7sgEU5Dhq3o4SblFXdE9zScpz6EXc5Zwr1UDZvzgXZGosUtKVc7d1LmkrHP8Q2o18HcDWtF3WvJRb5Zpw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux, freebsd] - - '@sentry/cli-linux-arm@2.42.2': - resolution: {integrity: sha512-7udCw+YL9lwq+9eL3WLspvnuG+k5Icg92YE7zsteTzWLwgPVzaxeZD2f8hwhsu+wmL+jNqbpCRmktPteh3i2mg==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux, freebsd] - - '@sentry/cli-linux-i686@2.42.2': - resolution: {integrity: sha512-Sw/dQp5ZPvKnq3/y7wIJyxTUJYPGoTX/YeMbDs8BzDlu9to2LWV3K3r7hE7W1Lpbaw4tSquUHiQjP5QHCOS7aQ==} - engines: {node: '>=10'} - cpu: [x86, ia32] - os: [linux, freebsd] - - '@sentry/cli-linux-x64@2.42.2': - resolution: {integrity: sha512-mU4zUspAal6TIwlNLBV5oq6yYqiENnCWSxtSQVzWs0Jyq97wtqGNG9U+QrnwjJZ+ta/hvye9fvL2X25D/RxHQw==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux, freebsd] - - '@sentry/cli-win32-i686@2.42.2': - resolution: {integrity: sha512-iHvFHPGqgJMNqXJoQpqttfsv2GI3cGodeTq4aoVLU/BT3+hXzbV0x1VpvvEhncJkDgDicJpFLM8sEPHb3b8abw==} - engines: {node: '>=10'} - cpu: [x86, ia32] - os: [win32] - - '@sentry/cli-win32-x64@2.42.2': - resolution: {integrity: sha512-vPPGHjYoaGmfrU7xhfFxG7qlTBacroz5NdT+0FmDn6692D8IvpNXl1K+eV3Kag44ipJBBeR8g1HRJyx/F/9ACw==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@sentry/cli@2.42.2': - resolution: {integrity: sha512-spb7S/RUumCGyiSTg8DlrCX4bivCNmU/A1hcfkwuciTFGu8l5CDc2I6jJWWZw8/0enDGxuj5XujgXvU5tr4bxg==} - engines: {node: '>= 10'} - hasBin: true - - '@sentry/cloudflare@9.11.0': - resolution: {integrity: sha512-FrqEilMsLEMuqoLsk9bLGEdgo7uwDyOjQfRwlfCm7YOX29vcVxYmQMs1qWW76Bwml3tE5ccspuOpJ9YnPm1Dfg==} - engines: {node: '>=18'} - peerDependencies: - '@cloudflare/workers-types': ^4.x - peerDependenciesMeta: - '@cloudflare/workers-types': - optional: true - - '@sentry/core@9.11.0': - resolution: {integrity: sha512-qfb4ahGZubbrNh1MnbEqyHFp87rIwQIZapyQLCaYpudXrP1biEpLOV3mMDvDJWCdX460hoOwQ3SkwipV3We/7w==} - engines: {node: '>=18'} - - '@sentry/node@9.11.0': - resolution: {integrity: sha512-luDsNDHsHkoXbL2Rf1cEKijh6hBfjzGQe09iP6kdZr+HB0bO+qoLe+nZLzSIQTWgWSt2XYNQyiLAsaMlbJZhJg==} - engines: {node: '>=18'} - - '@sentry/opentelemetry@9.11.0': - resolution: {integrity: sha512-B6RumUFGb1+Q4MymY7IZbdl1Ayz2srqf46itFr1ohE/IpwY7OWKMntop8fxyccUW3ptmPp9cPkBJOaa9UdJhSg==} - 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 - - '@sentry/svelte@9.11.0': - resolution: {integrity: sha512-pc4eC+T89c5Lk3Y2B/3MD2TO9K3DlPzOWgJJdp9AJiCZzt3s9D2XROiDwuqnRXQJRKzm0luv9LaWDUmnW24OgQ==} - engines: {node: '>=18'} - peerDependencies: - svelte: 3.x || 4.x || 5.x - - '@sentry/sveltekit@9.11.0': - resolution: {integrity: sha512-3UA3KpK/BgmKX88TuCl/WK0QSm/tUyxmytbj0imLIVjXfT50OQHtBd6rRrRtLtupb/61apbeZ+anG3V/2Hwe6A==} - engines: {node: '>=18'} - peerDependencies: - '@sveltejs/kit': 2.x - vite: '*' - peerDependenciesMeta: - vite: - optional: true - - '@sentry/vite-plugin@3.2.4': - resolution: {integrity: sha512-ZRn5TLlq5xtwKOqaWP+XqS1PYVfbBCgsbMk7wW2Ly6EgF9wYePvtLqKgYnE3hwPg2LpBnRPR2ti1ohlUkR+wXA==} - engines: {node: '>= 14'} - - '@sveltejs/acorn-typescript@1.0.5': - resolution: {integrity: sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==} - peerDependencies: - acorn: ^8.9.0 - - '@sveltejs/adapter-node@5.2.12': - resolution: {integrity: sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==} - peerDependencies: - '@sveltejs/kit': ^2.4.0 - - '@sveltejs/kit@2.20.4': - resolution: {integrity: sha512-B3Y1mb1Qjt57zXLVch5tfqsK/ebHe6uYTcFSnGFNwRpId3+fplLgQK6Z2zhDVBezSsPuhDq6Pry+9PA88ocN6Q==} - engines: {node: '>=18.13'} - hasBin: true - 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 - - '@sveltejs/vite-plugin-svelte-inspector@4.0.1': - resolution: {integrity: sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==} - 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 - - '@sveltejs/vite-plugin-svelte@5.0.3': - resolution: {integrity: sha512-MCFS6CrQDu1yGwspm4qtli0e63vaPCehf6V7pIMP15AsWgMKrqDGCPFF/0kn4SP0ii4aySu4Pa62+fIRGFMjgw==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22} - peerDependencies: - svelte: ^5.0.0 - vite: ^6.0.0 - - '@sveltekit-i18n/base@1.3.7': - resolution: {integrity: sha512-kg1kql1/ro/lIudwFiWrv949Q07gmweln87tflUZR51MNdXXzK4fiJQv5Mw50K/CdQ5BOk/dJ0WOH2vOtBI6yw==} - peerDependencies: - svelte: '>=3.49.0' - - '@sveltekit-i18n/parser-default@1.1.1': - resolution: {integrity: sha512-/gtzLlqm/sox7EoPKD56BxGZktK/syGc79EbJAPWY5KVitQD9SM0TP8yJCqDxTVPk7Lk0WJhrBGUE2Nn0f5M1w==} - - '@sveltestrap/sveltestrap@7.1.0': - resolution: {integrity: sha512-TpIx25kqLV+z+VD3yfqYayOI1IaCeWFbT0uqM6NfA4vQgDs9PjFwmjkU4YEAlV/ngs9e7xPmaRWE7lkrg4Miow==} - peerDependencies: - svelte: ^4.0.0 || ^5.0.0 || ^5.0.0-next.0 - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/cookie@0.6.0': - resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} - - '@types/eslint@9.6.1': - resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} - - '@types/estree@1.0.7': - resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} - - '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - - '@types/linkify-it@5.0.0': - resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - - '@types/luxon@3.6.2': - resolution: {integrity: sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw==} - - '@types/markdown-it@14.1.2': - resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - - '@types/mdurl@2.0.0': - resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - - '@types/mysql@2.15.26': - resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - - '@types/node@22.14.0': - resolution: {integrity: sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==} - - '@types/pg-pool@2.0.6': - resolution: {integrity: sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==} - - '@types/pg@8.6.1': - resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} - - '@types/resolve@1.20.2': - resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - - '@types/sanitize-html@2.15.0': - resolution: {integrity: sha512-71Z6PbYsVKfp4i6Jvr37s5ql6if1Q/iJQT80NbaSi7uGaG8CqBMXP0pk/EsURAOuGdk5IJCd/vnzKrR7S3Txsw==} - - '@types/shimmer@1.2.0': - resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} - - '@types/tedious@4.0.14': - resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} - - '@typescript-eslint/eslint-plugin@8.29.0': - resolution: {integrity: sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - 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' - - '@typescript-eslint/parser@8.29.0': - resolution: {integrity: sha512-8C0+jlNJOwQso2GapCVWWfW/rzaq7Lbme+vGUFKE31djwNncIpgXD7Cd4weEsDdkoZDjH0lwwr3QDQFuyrMg9g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/scope-manager@8.29.0': - resolution: {integrity: sha512-aO1PVsq7Gm+tcghabUpzEnVSFMCU4/nYIgC2GOatJcllvWfnhrgW0ZEbnTxm36QsikmCN1K/6ZgM7fok2I7xNw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/type-utils@8.29.0': - resolution: {integrity: sha512-ahaWQ42JAOx+NKEf5++WC/ua17q5l+j1GFrbbpVKzFL/tKVc0aYY8rVSYUpUvt2hUP1YBr7mwXzx+E/DfUWI9Q==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/types@8.29.0': - resolution: {integrity: sha512-wcJL/+cOXV+RE3gjCyl/V2G877+2faqvlgtso/ZRbTCnZazh0gXhe+7gbAnfubzN2bNsBtZjDvlh7ero8uIbzg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/typescript-estree@8.29.0': - resolution: {integrity: sha512-yOfen3jE9ISZR/hHpU/bmNvTtBW1NjRbkSFdZOksL1N+ybPEE7UVGMwqvS6CP022Rp00Sb0tdiIkhSCe6NI8ow==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/utils@8.29.0': - resolution: {integrity: sha512-gX/A0Mz9Bskm8avSWFcK0gP7cZpbY4AIo6B0hWYFCaIsz750oaiWR4Jr2CI+PQhfW1CpcQr9OlfPS+kMFegjXA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - '@typescript-eslint/visitor-keys@8.29.0': - resolution: {integrity: sha512-Sne/pVz8ryR03NFK21VpN88dZ2FdQXOlq3VIklbrTYEt8yXtRFr9tvUhqvCeKjqYk5FSim37sHbooT6vzBTZcg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - acorn-import-attributes@1.9.5: - resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} - peerDependencies: - acorn: ^8 - - acorn-jsx@5.3.2: - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - - acorn@8.14.1: - resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} - engines: {node: '>=0.4.0'} - hasBin: true - - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - aria-query@5.3.2: - resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} - engines: {node: '>= 0.4'} - - ast-types@0.16.1: - resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} - engines: {node: '>=4'} - - axobject-query@4.1.0: - resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} - engines: {node: '>= 0.4'} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base64-arraybuffer@1.0.2: - resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} - engines: {node: '>= 0.6.0'} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - bootstrap-icons@1.11.3: - resolution: {integrity: sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==} - - bootstrap@5.3.5: - resolution: {integrity: sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==} - peerDependencies: - '@popperjs/core': ^2.11.8 - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} - - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.24.4: - resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true - - callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - - caniuse-lite@1.0.30001712: - resolution: {integrity: sha512-MBqPpGYYdQ7/hfKiet9SCI+nmN5/hp4ZzveOJubl5DTAMa5oggjAuoi0Z4onBpKPFI2ePGnQuQIzF3VxDjDJig==} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} - - chokidar@4.0.3: - resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} - engines: {node: '>= 14.16.0'} - - cjs-module-lexer@1.4.3: - resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} - - clsx@2.1.1: - resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} - engines: {node: '>=6'} - - 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==} - - commondir@1.0.1: - resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie@0.6.0: - resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} - engines: {node: '>= 0.6'} - - cross-spawn@7.0.6: - resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} - engines: {node: '>= 8'} - - cssesc@3.0.0: - resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} - engines: {node: '>=4'} - hasBin: true - - debug@4.4.0: - resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - - deepmerge@4.3.1: - resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} - engines: {node: '>=0.10.0'} - - detect-libc@1.0.3: - resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} - engines: {node: '>=0.10'} - hasBin: true - - devalue@5.1.1: - resolution: {integrity: sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==} - - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.2.2: - resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - - dotenv@16.4.7: - resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} - engines: {node: '>=12'} - - electron-to-chromium@1.5.132: - resolution: {integrity: sha512-QgX9EBvWGmvSRa74zqfnG7+Eno0Ak0vftBll0Pt2/z5b3bEGYL6OUXLgKPtvx73dn3dvwrlyVkjPKRRlhLYTEg==} - - entities@4.5.0: - resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} - engines: {node: '>=0.12'} - - esbuild@0.25.2: - resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==} - engines: {node: '>=18'} - hasBin: true - - escalade@3.2.0: - resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - eslint-compat-utils@0.5.1: - resolution: {integrity: sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==} - engines: {node: '>=12'} - peerDependencies: - eslint: '>=6.0.0' - - eslint-config-prettier@9.1.0: - resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} - hasBin: true - peerDependencies: - eslint: '>=7.0.0' - - eslint-plugin-svelte@2.46.1: - resolution: {integrity: sha512-7xYr2o4NID/f9OEYMqxsEQsCsj4KaMy4q5sANaKkAb6/QeCjYFxRmDm2S3YC3A3pl1kyPZ/syOx/i7LcWYSbIw==} - engines: {node: ^14.17.0 || >=16.0.0} - 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 - - eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-scope@8.3.0: - resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - eslint-visitor-keys@4.2.0: - resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - eslint@9.24.0: - resolution: {integrity: sha512-eh/jxIEJyZrvbWRe4XuVclLPDYSYYYgLy5zXGGxD6j8zjSAxFEzI2fL/8xNq6O2yKqVt+eF2YhV+hxjV6UKXwQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - hasBin: true - peerDependencies: - jiti: '*' - peerDependenciesMeta: - jiti: - optional: true - - esm-env@1.2.2: - resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} - - espree@10.3.0: - resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} - engines: {node: '>=0.10'} - - esrap@1.4.6: - resolution: {integrity: sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==} - - esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - - estree-walker@2.0.2: - resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} - - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - - fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - - fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - - fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - - fdir@6.4.3: - resolution: {integrity: sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} - engines: {node: '>=16.0.0'} - - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} - engines: {node: '>=16'} - - flatted@3.3.3: - resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - - forwarded-parse@2.1.2: - resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - - glob@9.3.5: - resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} - engines: {node: '>=16 || 14 >=14.17'} - - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - - globals@14.0.0: - resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} - engines: {node: '>=18'} - - globals@16.0.0: - resolution: {integrity: sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==} - engines: {node: '>=18'} - - globalyzer@0.1.0: - resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} - - globrex@0.1.2: - resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - - ignore@5.3.2: - resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} - engines: {node: '>= 4'} - - immutable@5.1.1: - resolution: {integrity: sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==} - - import-fresh@3.3.1: - resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} - engines: {node: '>=6'} - - import-in-the-middle@1.13.1: - resolution: {integrity: sha512-k2V9wNm9B+ysuelDTHjI9d5KPc4l8zAZTGqj+pcynvWkypZd857ryzN8jNC7Pg2YZXNMJcHRPpaDyCBbNyVRpA==} - - import-meta-resolve@4.1.0: - resolution: {integrity: sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==} - - imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-core-module@2.16.1: - resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} - engines: {node: '>= 0.4'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-module@1.0.0: - resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - - is-reference@1.2.1: - resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} - - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsesc@3.1.0: - resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} - engines: {node: '>=6'} - hasBin: true - - json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} - - json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - - json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - - json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - - keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - - kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - - known-css-properties@0.35.0: - resolution: {integrity: sha512-a/RAk2BfKk+WFGhhOCAYqSiFLc34k8Mt/6NWRI4joER0EYUzXIcFivjjnoD3+XU1DggLn/tZc3DOAgke7l8a4A==} - - levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} - - linkify-it@5.0.0: - resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - - locate-character@3.0.0: - resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - - lru-cache@10.4.3: - resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - - lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - - luxon@3.6.1: - resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} - engines: {node: '>=12'} - - magic-string@0.30.17: - resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} - - magic-string@0.30.7: - resolution: {integrity: sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==} - engines: {node: '>=12'} - - magic-string@0.30.8: - resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} - engines: {node: '>=12'} - - markdown-it@14.1.0: - resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} - hasBin: true - - mdurl@2.0.0: - resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - minidenticons@4.2.1: - resolution: {integrity: sha512-oWfFivA0lOx/V/bO/YIJbthB26lV8JXYvhnv9zM2hNd3fzsHTXQ6c6bWZPcvhD3nnOB+lQk/D9lF43BXixrN8g==} - engines: {node: '>=15.14.0'} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@8.0.4: - resolution: {integrity: sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==} - engines: {node: '>=16 || 14 >=14.17'} - - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - minipass@4.2.8: - resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} - engines: {node: '>=8'} - - minipass@7.1.2: - resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} - engines: {node: '>=16 || 14 >=14.17'} - - module-details-from-path@1.0.3: - resolution: {integrity: sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==} - - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - - mrmime@2.0.1: - resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} - engines: {node: '>=10'} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - - node-addon-api@7.1.1: - resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-releases@2.0.19: - resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} - engines: {node: '>= 0.8.0'} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - - parse-srcset@1.0.2: - resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.11.1: - resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} - engines: {node: '>=16 || 14 >=14.18'} - - pg-int8@1.0.1: - resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} - engines: {node: '>=4.0.0'} - - pg-protocol@1.8.0: - resolution: {integrity: sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==} - - pg-types@2.2.0: - resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} - engines: {node: '>=4'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - picomatch@4.0.2: - resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} - engines: {node: '>=12'} - - postcss-load-config@3.1.4: - resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} - engines: {node: '>= 10'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-safe-parser@6.0.0: - resolution: {integrity: sha512-FARHN8pwH+WiS2OPCxJI8FuRJpTVnn6ZNFiqAM2aeW2LwTHWWmWgIyKC6cUo0L8aeKiF/14MNvnpls6R2PBeMQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.3.3 - - postcss-scss@4.0.9: - resolution: {integrity: sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.4.29 - - postcss-selector-parser@6.1.2: - resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} - engines: {node: '>=4'} - - postcss@8.5.3: - resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} - engines: {node: ^10 || ^12 || >=14} - - postgres-array@2.0.0: - resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} - engines: {node: '>=4'} - - postgres-bytea@1.0.0: - resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} - engines: {node: '>=0.10.0'} - - postgres-date@1.0.7: - resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} - engines: {node: '>=0.10.0'} - - postgres-interval@1.2.0: - resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} - engines: {node: '>=0.10.0'} - - prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - - prettier-plugin-svelte@3.3.3: - resolution: {integrity: sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==} - peerDependencies: - prettier: ^3.0.0 - svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - - prettier@3.5.3: - resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} - engines: {node: '>=14'} - hasBin: true - - pretty-bytes@6.1.1: - resolution: {integrity: sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==} - engines: {node: ^14.13.1 || >=16.0.0} - - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - - punycode.js@2.3.1: - resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} - engines: {node: '>=6'} - - punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - readdirp@4.1.2: - resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} - engines: {node: '>= 14.18.0'} - - recast@0.23.11: - resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} - engines: {node: '>= 4'} - - require-in-the-middle@7.5.2: - resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} - engines: {node: '>=8.6.0'} - - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - - resolve@1.22.10: - resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} - engines: {node: '>= 0.4'} - hasBin: true - - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.39.0: - resolution: {integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - - sade@1.8.1: - resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} - engines: {node: '>=6'} - - sanitize-html@2.15.0: - resolution: {integrity: sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==} - - sass@1.86.3: - resolution: {integrity: sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==} - engines: {node: '>=14.0.0'} - hasBin: true - - semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - - semver@7.7.1: - resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} - engines: {node: '>=10'} - hasBin: true - - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} - - shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - - shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - - shimmer@1.2.1: - resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} - - sirv@3.0.1: - resolution: {integrity: sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==} - engines: {node: '>=18'} - - sorcery@1.0.0: - resolution: {integrity: sha512-5ay9oJE+7sNmhzl3YNG18jEEEf4AOQCM/FAqR5wMmzqd1FtRorFbJXn3w3SKOhbiQaVgHM+Q1lszZspjri7bpA==} - hasBin: true - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - svelte-bootstrap-icons@3.1.2: - resolution: {integrity: sha512-vy+qmWFfLJZxu5BaDlmaUG4uzki1rodX5ERZAP6KQdyO/2WNeGBDU4Yke3Z0NRq+VSepK86iAy+iUJvlUdsbBg==} - - svelte-check@4.1.5: - resolution: {integrity: sha512-Gb0T2IqBNe1tLB9EB1Qh+LOe+JB8wt2/rNBDGvkxQVvk8vNeAoG+vZgFB/3P5+zC7RWlyBlzm9dVjZFph/maIg==} - engines: {node: '>= 18.0.0'} - hasBin: true - peerDependencies: - svelte: ^4.0.0 || ^5.0.0-next.0 - typescript: '>=5.0.0' - - svelte-easy-crop@4.0.1: - resolution: {integrity: sha512-0k7vVpHVLrPyobSXqey5IJUmFVxOoCaQrobFEsFXpSCyK8N5jTkRj1VX6NuCOZK8XXcMAqUvV0MktB8D5x1oCw==} - peerDependencies: - svelte: ^5.0.0 - - svelte-eslint-parser@0.43.0: - resolution: {integrity: sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - svelte: - optional: true - - svelte-tippy@1.3.2: - resolution: {integrity: sha512-41f+85hwhKBRqX0UNYrgFsi34Kk/KDvUkIZXYANxkWoA2NTVTCZbUC2J8hRNZ4TRVxObTshoZRjK2co5+i6LMw==} - - svelte@5.25.7: - resolution: {integrity: sha512-0fzXbXaKfSvFUs6Wxev2h4CoEhexZotbTF9EJ4+Cg7MHW64ZnZ9+xUedZyEpgj0Tt9HrYGv9aASHkqjn9b/cPw==} - engines: {node: '>=18'} - - sveltekit-i18n@2.4.2: - resolution: {integrity: sha512-hjRWn4V4DBL8JQKJoJa3MRvn6d32Zo+rWkoSP5bsQ/XIAguPdQUZJ8LMe6Nc1rST8WEVdu9+vZI3aFdKYGR3+Q==} - peerDependencies: - svelte: '>=3.49.0' - - tiny-glob@0.2.9: - resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} - - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - - tippy.js@6.3.7: - resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - totalist@3.0.1: - resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} - engines: {node: '>=6'} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - ts-api-utils@2.1.0: - resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} - engines: {node: '>=18.12'} - peerDependencies: - typescript: '>=4.8.4' - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - tslog@4.9.3: - resolution: {integrity: sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw==} - engines: {node: '>=16'} - - type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - - typescript-eslint@8.29.0: - resolution: {integrity: sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.9.0' - - typescript@5.8.3: - resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} - engines: {node: '>=14.17'} - hasBin: true - - uc.micro@2.1.0: - resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - - undici-types@6.21.0: - resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - - unplugin@1.0.1: - resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} - - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' - - uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - vite@6.2.5: - resolution: {integrity: sha512-j023J/hCAa4pRIUH6J9HemwYfjB5llR2Ps0CWeikOtdR8+pAURAk0DoJC5/mm9kd+UgdnIy7d6HE4EAvlYhPhA==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - 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 - - vitefu@1.0.6: - resolution: {integrity: sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA==} - peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 - peerDependenciesMeta: - vite: - optional: true - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - webpack-sources@3.2.3: - resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} - engines: {node: '>=10.13.0'} - - webpack-virtual-modules@0.5.0: - resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} - engines: {node: '>=0.10.0'} - - xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} - - yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - - yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - - zimmerframe@1.1.2: - resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} - -snapshots: - - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.25.9 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.26.8': {} - - '@babel/core@7.26.10': - dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.26.2 - '@babel/generator': 7.27.0 - '@babel/helper-compilation-targets': 7.27.0 - '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) - '@babel/helpers': 7.27.0 - '@babel/parser': 7.27.0 - '@babel/template': 7.27.0 - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 - convert-source-map: 2.0.0 - debug: 4.4.0 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.27.0': - dependencies: - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - '@jridgewell/gen-mapping': 0.3.8 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 3.1.0 - - '@babel/helper-compilation-targets@7.27.0': - dependencies: - '@babel/compat-data': 7.26.8 - '@babel/helper-validator-option': 7.25.9 - browserslist: 4.24.4 - lru-cache: 5.1.1 - semver: 6.3.1 - - '@babel/helper-module-imports@7.25.9': - dependencies: - '@babel/traverse': 7.27.0 - '@babel/types': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': - dependencies: - '@babel/core': 7.26.10 - '@babel/helper-module-imports': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - '@babel/traverse': 7.27.0 - transitivePeerDependencies: - - supports-color - - '@babel/helper-string-parser@7.25.9': {} - - '@babel/helper-validator-identifier@7.25.9': {} - - '@babel/helper-validator-option@7.25.9': {} - - '@babel/helpers@7.27.0': - dependencies: - '@babel/template': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/parser@7.26.9': - dependencies: - '@babel/types': 7.27.0 - - '@babel/parser@7.27.0': - dependencies: - '@babel/types': 7.27.0 - - '@babel/template@7.27.0': - dependencies: - '@babel/code-frame': 7.26.2 - '@babel/parser': 7.27.0 - '@babel/types': 7.27.0 - - '@babel/traverse@7.27.0': - 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.4.0 - globals: 11.12.0 - transitivePeerDependencies: - - supports-color - - '@babel/types@7.27.0': - dependencies: - '@babel/helper-string-parser': 7.25.9 - '@babel/helper-validator-identifier': 7.25.9 - - '@esbuild/aix-ppc64@0.25.2': - optional: true - - '@esbuild/android-arm64@0.25.2': - optional: true - - '@esbuild/android-arm@0.25.2': - optional: true - - '@esbuild/android-x64@0.25.2': - optional: true - - '@esbuild/darwin-arm64@0.25.2': - optional: true - - '@esbuild/darwin-x64@0.25.2': - optional: true - - '@esbuild/freebsd-arm64@0.25.2': - optional: true - - '@esbuild/freebsd-x64@0.25.2': - optional: true - - '@esbuild/linux-arm64@0.25.2': - optional: true - - '@esbuild/linux-arm@0.25.2': - optional: true - - '@esbuild/linux-ia32@0.25.2': - optional: true - - '@esbuild/linux-loong64@0.25.2': - optional: true - - '@esbuild/linux-mips64el@0.25.2': - optional: true - - '@esbuild/linux-ppc64@0.25.2': - optional: true - - '@esbuild/linux-riscv64@0.25.2': - optional: true - - '@esbuild/linux-s390x@0.25.2': - optional: true - - '@esbuild/linux-x64@0.25.2': - optional: true - - '@esbuild/netbsd-arm64@0.25.2': - optional: true - - '@esbuild/netbsd-x64@0.25.2': - optional: true - - '@esbuild/openbsd-arm64@0.25.2': - optional: true - - '@esbuild/openbsd-x64@0.25.2': - optional: true - - '@esbuild/sunos-x64@0.25.2': - optional: true - - '@esbuild/win32-arm64@0.25.2': - optional: true - - '@esbuild/win32-ia32@0.25.2': - optional: true - - '@esbuild/win32-x64@0.25.2': - optional: true - - '@eslint-community/eslint-utils@4.5.1(eslint@9.24.0)': - dependencies: - eslint: 9.24.0 - eslint-visitor-keys: 3.4.3 - - '@eslint-community/regexpp@4.12.1': {} - - '@eslint/config-array@0.20.0': - dependencies: - '@eslint/object-schema': 2.1.6 - debug: 4.4.0 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - - '@eslint/config-helpers@0.2.1': {} - - '@eslint/core@0.12.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/core@0.13.0': - dependencies: - '@types/json-schema': 7.0.15 - - '@eslint/eslintrc@3.3.1': - dependencies: - ajv: 6.12.6 - debug: 4.4.0 - espree: 10.3.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.1 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - - '@eslint/js@9.24.0': {} - - '@eslint/object-schema@2.1.6': {} - - '@eslint/plugin-kit@0.2.8': - dependencies: - '@eslint/core': 0.13.0 - levn: 0.4.1 - - '@fontsource/firago@5.2.5': {} - - '@humanfs/core@0.19.1': {} - - '@humanfs/node@0.16.6': - dependencies: - '@humanfs/core': 0.19.1 - '@humanwhocodes/retry': 0.3.1 - - '@humanwhocodes/module-importer@1.0.1': {} - - '@humanwhocodes/retry@0.3.1': {} - - '@humanwhocodes/retry@0.4.2': {} - - '@jridgewell/gen-mapping@0.3.8': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 - - '@jridgewell/resolve-uri@3.1.2': {} - - '@jridgewell/set-array@1.2.1': {} - - '@jridgewell/sourcemap-codec@1.5.0': {} - - '@jridgewell/trace-mapping@0.3.25': - dependencies: - '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - - '@opentelemetry/api-logs@0.57.2': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/api@1.9.0': {} - - '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - - '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/semantic-conventions': 1.28.0 - - '@opentelemetry/instrumentation-amqplib@0.46.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-connect@0.43.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - '@types/connect': 3.4.38 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-dataloader@0.16.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-express@0.47.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fastify@0.44.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-fs@0.19.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-generic-pool@0.43.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-graphql@0.47.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-hapi@0.45.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-http@0.57.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 - forwarded-parse: 2.1.2 - semver: 7.7.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-ioredis@0.47.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-kafkajs@0.7.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-knex@0.44.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-koa@0.47.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-lru-memoizer@0.44.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongodb@0.52.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mongoose@0.46.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql2@0.45.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-mysql@0.45.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - '@types/mysql': 2.15.26 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-pg@0.51.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) - '@types/pg': 8.6.1 - '@types/pg-pool': 2.0.6 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-redis-4@0.46.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.30.0 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-tedious@0.18.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - '@types/tedious': 4.0.14 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation-undici@0.10.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/api-logs': 0.57.2 - '@types/shimmer': 1.2.0 - import-in-the-middle: 1.13.1 - require-in-the-middle: 7.5.2 - semver: 7.7.1 - shimmer: 1.2.1 - transitivePeerDependencies: - - supports-color - - '@opentelemetry/redis-common@0.36.2': {} - - '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 - - '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.28.0 - - '@opentelemetry/semantic-conventions@1.28.0': {} - - '@opentelemetry/semantic-conventions@1.30.0': {} - - '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - - '@parcel/watcher-android-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-arm64@2.5.1': - optional: true - - '@parcel/watcher-darwin-x64@2.5.1': - optional: true - - '@parcel/watcher-freebsd-x64@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-arm64-musl@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-glibc@2.5.1': - optional: true - - '@parcel/watcher-linux-x64-musl@2.5.1': - optional: true - - '@parcel/watcher-win32-arm64@2.5.1': - optional: true - - '@parcel/watcher-win32-ia32@2.5.1': - optional: true - - '@parcel/watcher-win32-x64@2.5.1': - optional: true - - '@parcel/watcher@2.5.1': - dependencies: - detect-libc: 1.0.3 - is-glob: 4.0.3 - micromatch: 4.0.8 - node-addon-api: 7.1.1 - 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 - optional: true - - '@polka/url@1.0.0-next.28': {} - - '@popperjs/core@2.11.8': {} - - '@prisma/instrumentation@6.5.0(@opentelemetry/api@1.9.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - transitivePeerDependencies: - - supports-color - - '@rollup/plugin-commonjs@28.0.3(rollup@4.39.0)': - dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.39.0) - commondir: 1.0.1 - estree-walker: 2.0.2 - fdir: 6.4.3(picomatch@4.0.2) - is-reference: 1.2.1 - magic-string: 0.30.17 - picomatch: 4.0.2 - optionalDependencies: - rollup: 4.39.0 - - '@rollup/plugin-json@6.1.0(rollup@4.39.0)': - dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.39.0) - optionalDependencies: - rollup: 4.39.0 - - '@rollup/plugin-node-resolve@16.0.1(rollup@4.39.0)': - dependencies: - '@rollup/pluginutils': 5.1.4(rollup@4.39.0) - '@types/resolve': 1.20.2 - deepmerge: 4.3.1 - is-module: 1.0.0 - resolve: 1.22.10 - optionalDependencies: - rollup: 4.39.0 - - '@rollup/pluginutils@5.1.4(rollup@4.39.0)': - dependencies: - '@types/estree': 1.0.7 - estree-walker: 2.0.2 - picomatch: 4.0.2 - optionalDependencies: - rollup: 4.39.0 - - '@rollup/rollup-android-arm-eabi@4.39.0': - optional: true - - '@rollup/rollup-android-arm64@4.39.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.39.0': - optional: true - - '@rollup/rollup-darwin-x64@4.39.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.39.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.39.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.39.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.39.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.39.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.39.0': - optional: true - - '@rollup/rollup-linux-loongarch64-gnu@4.39.0': - optional: true - - '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.39.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.39.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.39.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.39.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.39.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.39.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.39.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.39.0': - optional: true - - '@sentry-internal/browser-utils@9.11.0': - dependencies: - '@sentry/core': 9.11.0 - - '@sentry-internal/feedback@9.11.0': - dependencies: - '@sentry/core': 9.11.0 - - '@sentry-internal/replay-canvas@9.11.0': - dependencies: - '@sentry-internal/replay': 9.11.0 - '@sentry/core': 9.11.0 - - '@sentry-internal/replay@9.11.0': - dependencies: - '@sentry-internal/browser-utils': 9.11.0 - '@sentry/core': 9.11.0 - - '@sentry/babel-plugin-component-annotate@3.2.4': {} - - '@sentry/browser@9.11.0': - dependencies: - '@sentry-internal/browser-utils': 9.11.0 - '@sentry-internal/feedback': 9.11.0 - '@sentry-internal/replay': 9.11.0 - '@sentry-internal/replay-canvas': 9.11.0 - '@sentry/core': 9.11.0 - - '@sentry/bundler-plugin-core@3.2.4': - dependencies: - '@babel/core': 7.26.10 - '@sentry/babel-plugin-component-annotate': 3.2.4 - '@sentry/cli': 2.42.2 - dotenv: 16.4.7 - find-up: 5.0.0 - glob: 9.3.5 - magic-string: 0.30.8 - unplugin: 1.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@sentry/cli-darwin@2.42.2': - optional: true - - '@sentry/cli-linux-arm64@2.42.2': - optional: true - - '@sentry/cli-linux-arm@2.42.2': - optional: true - - '@sentry/cli-linux-i686@2.42.2': - optional: true - - '@sentry/cli-linux-x64@2.42.2': - optional: true - - '@sentry/cli-win32-i686@2.42.2': - optional: true - - '@sentry/cli-win32-x64@2.42.2': - optional: true - - '@sentry/cli@2.42.2': - dependencies: - https-proxy-agent: 5.0.1 - node-fetch: 2.7.0 - progress: 2.0.3 - proxy-from-env: 1.1.0 - which: 2.0.2 - 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 - transitivePeerDependencies: - - encoding - - supports-color - - '@sentry/cloudflare@9.11.0': - dependencies: - '@sentry/core': 9.11.0 - - '@sentry/core@9.11.0': {} - - '@sentry/node@9.11.0': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-amqplib': 0.46.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-connect': 0.43.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-dataloader': 0.16.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-express': 0.47.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fastify': 0.44.2(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-fs': 0.19.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-generic-pool': 0.43.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-graphql': 0.47.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-hapi': 0.45.2(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-http': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-ioredis': 0.47.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-kafkajs': 0.7.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-knex': 0.44.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-koa': 0.47.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-lru-memoizer': 0.44.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongodb': 0.52.0(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mongoose': 0.46.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql': 0.45.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-mysql2': 0.45.2(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-pg': 0.51.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-redis-4': 0.46.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-tedious': 0.18.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation-undici': 0.10.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - '@prisma/instrumentation': 6.5.0(@opentelemetry/api@1.9.0) - '@sentry/core': 9.11.0 - '@sentry/opentelemetry': 9.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0) - import-in-the-middle: 1.13.1 - transitivePeerDependencies: - - supports-color - - '@sentry/opentelemetry@9.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)': - dependencies: - '@opentelemetry/api': 1.9.0 - '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/instrumentation': 0.57.2(@opentelemetry/api@1.9.0) - '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.30.0 - '@sentry/core': 9.11.0 - - '@sentry/svelte@9.11.0(svelte@5.25.7)': - dependencies: - '@sentry/browser': 9.11.0 - '@sentry/core': 9.11.0 - magic-string: 0.30.7 - svelte: 5.25.7 - - '@sentry/sveltekit@9.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0)(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3))': - dependencies: - '@babel/parser': 7.26.9 - '@sentry/cloudflare': 9.11.0 - '@sentry/core': 9.11.0 - '@sentry/node': 9.11.0 - '@sentry/opentelemetry': 9.11.0(@opentelemetry/api@1.9.0)(@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/instrumentation@0.57.2(@opentelemetry/api@1.9.0))(@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0))(@opentelemetry/semantic-conventions@1.30.0) - '@sentry/svelte': 9.11.0(svelte@5.25.7) - '@sentry/vite-plugin': 3.2.4 - '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - magic-string: 0.30.7 - recast: 0.23.11 - sorcery: 1.0.0 - optionalDependencies: - vite: 6.2.5(@types/node@22.14.0)(sass@1.86.3) - transitivePeerDependencies: - - '@cloudflare/workers-types' - - '@opentelemetry/api' - - '@opentelemetry/context-async-hooks' - - '@opentelemetry/core' - - '@opentelemetry/instrumentation' - - '@opentelemetry/sdk-trace-base' - - '@opentelemetry/semantic-conventions' - - encoding - - supports-color - - svelte - - '@sentry/vite-plugin@3.2.4': - dependencies: - '@sentry/bundler-plugin-core': 3.2.4 - unplugin: 1.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - '@sveltejs/acorn-typescript@1.0.5(acorn@8.14.1)': - dependencies: - acorn: 8.14.1 - - '@sveltejs/adapter-node@5.2.12(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))': - dependencies: - '@rollup/plugin-commonjs': 28.0.3(rollup@4.39.0) - '@rollup/plugin-json': 6.1.0(rollup@4.39.0) - '@rollup/plugin-node-resolve': 16.0.1(rollup@4.39.0) - '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - rollup: 4.39.0 - - '@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3))': - dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - '@types/cookie': 0.6.0 - cookie: 0.6.0 - devalue: 5.1.1 - esm-env: 1.2.2 - import-meta-resolve: 4.1.0 - kleur: 4.1.5 - magic-string: 0.30.17 - mrmime: 2.0.1 - sade: 1.8.1 - set-cookie-parser: 2.7.1 - sirv: 3.0.1 - svelte: 5.25.7 - vite: 6.2.5(@types/node@22.14.0)(sass@1.86.3) - - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3))': - dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - debug: 4.4.0 - svelte: 5.25.7 - vite: 6.2.5(@types/node@22.14.0)(sass@1.86.3) - transitivePeerDependencies: - - supports-color - - '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3))': - dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)))(svelte@5.25.7)(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - debug: 4.4.0 - deepmerge: 4.3.1 - kleur: 4.1.5 - magic-string: 0.30.17 - svelte: 5.25.7 - vite: 6.2.5(@types/node@22.14.0)(sass@1.86.3) - vitefu: 1.0.6(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)) - transitivePeerDependencies: - - supports-color - - '@sveltekit-i18n/base@1.3.7(svelte@5.25.7)': - dependencies: - svelte: 5.25.7 - - '@sveltekit-i18n/parser-default@1.1.1': {} - - '@sveltestrap/sveltestrap@7.1.0(svelte@5.25.7)': - dependencies: - '@popperjs/core': 2.11.8 - svelte: 5.25.7 - - '@types/connect@3.4.38': - dependencies: - '@types/node': 22.14.0 - - '@types/cookie@0.6.0': {} - - '@types/eslint@9.6.1': - dependencies: - '@types/estree': 1.0.7 - '@types/json-schema': 7.0.15 - - '@types/estree@1.0.7': {} - - '@types/json-schema@7.0.15': {} - - '@types/linkify-it@5.0.0': {} - - '@types/luxon@3.6.2': {} - - '@types/markdown-it@14.1.2': - dependencies: - '@types/linkify-it': 5.0.0 - '@types/mdurl': 2.0.0 - - '@types/mdurl@2.0.0': {} - - '@types/mysql@2.15.26': - dependencies: - '@types/node': 22.14.0 - - '@types/node@22.14.0': - dependencies: - undici-types: 6.21.0 - - '@types/pg-pool@2.0.6': - dependencies: - '@types/pg': 8.6.1 - - '@types/pg@8.6.1': - dependencies: - '@types/node': 22.14.0 - pg-protocol: 1.8.0 - pg-types: 2.2.0 - - '@types/resolve@1.20.2': {} - - '@types/sanitize-html@2.15.0': - dependencies: - htmlparser2: 8.0.2 - - '@types/shimmer@1.2.0': {} - - '@types/tedious@4.0.14': - dependencies: - '@types/node': 22.14.0 - - '@typescript-eslint/eslint-plugin@8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0)(typescript@5.8.3))(eslint@9.24.0)(typescript@5.8.3)': - dependencies: - '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.29.0(eslint@9.24.0)(typescript@5.8.3) - '@typescript-eslint/scope-manager': 8.29.0 - '@typescript-eslint/type-utils': 8.29.0(eslint@9.24.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.29.0(eslint@9.24.0)(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.29.0 - eslint: 9.24.0 - graphemer: 1.4.0 - ignore: 5.3.2 - natural-compare: 1.4.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/parser@8.29.0(eslint@9.24.0)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/scope-manager': 8.29.0 - '@typescript-eslint/types': 8.29.0 - '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.8.3) - '@typescript-eslint/visitor-keys': 8.29.0 - debug: 4.4.0 - eslint: 9.24.0 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/scope-manager@8.29.0': - dependencies: - '@typescript-eslint/types': 8.29.0 - '@typescript-eslint/visitor-keys': 8.29.0 - - '@typescript-eslint/type-utils@8.29.0(eslint@9.24.0)(typescript@5.8.3)': - dependencies: - '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.8.3) - '@typescript-eslint/utils': 8.29.0(eslint@9.24.0)(typescript@5.8.3) - debug: 4.4.0 - eslint: 9.24.0 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/types@8.29.0': {} - - '@typescript-eslint/typescript-estree@8.29.0(typescript@5.8.3)': - dependencies: - '@typescript-eslint/types': 8.29.0 - '@typescript-eslint/visitor-keys': 8.29.0 - debug: 4.4.0 - fast-glob: 3.3.3 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.7.1 - ts-api-utils: 2.1.0(typescript@5.8.3) - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/utils@8.29.0(eslint@9.24.0)(typescript@5.8.3)': - dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0) - '@typescript-eslint/scope-manager': 8.29.0 - '@typescript-eslint/types': 8.29.0 - '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.8.3) - eslint: 9.24.0 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - '@typescript-eslint/visitor-keys@8.29.0': - dependencies: - '@typescript-eslint/types': 8.29.0 - eslint-visitor-keys: 4.2.0 - - acorn-import-attributes@1.9.5(acorn@8.14.1): - dependencies: - acorn: 8.14.1 - - acorn-jsx@5.3.2(acorn@8.14.1): - dependencies: - acorn: 8.14.1 - - acorn@8.14.1: {} - - agent-base@6.0.2: - dependencies: - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - - ansi-styles@4.3.0: - dependencies: - color-convert: 2.0.1 - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - argparse@2.0.1: {} - - aria-query@5.3.2: {} - - ast-types@0.16.1: - dependencies: - tslib: 2.8.1 - - axobject-query@4.1.0: {} - - balanced-match@1.0.2: {} - - base64-arraybuffer@1.0.2: {} - - binary-extensions@2.3.0: {} - - bootstrap-icons@1.11.3: {} - - bootstrap@5.3.5(@popperjs/core@2.11.8): - dependencies: - '@popperjs/core': 2.11.8 - - brace-expansion@1.1.11: - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - - brace-expansion@2.0.1: - dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - - browserslist@4.24.4: - dependencies: - caniuse-lite: 1.0.30001712 - electron-to-chromium: 1.5.132 - node-releases: 2.0.19 - update-browserslist-db: 1.1.3(browserslist@4.24.4) - - callsites@3.1.0: {} - - caniuse-lite@1.0.30001712: {} - - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 - - chokidar@4.0.3: - dependencies: - readdirp: 4.1.2 - - cjs-module-lexer@1.4.3: {} - - clsx@2.1.1: {} - - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - - color-name@1.1.4: {} - - commondir@1.0.1: {} - - concat-map@0.0.1: {} - - convert-source-map@2.0.0: {} - - cookie@0.6.0: {} - - cross-spawn@7.0.6: - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - - cssesc@3.0.0: {} - - debug@4.4.0: - dependencies: - ms: 2.1.3 - - deep-is@0.1.4: {} - - deepmerge@4.3.1: {} - - detect-libc@1.0.3: - optional: true - - devalue@5.1.1: {} - - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.2.2: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - - dotenv@16.4.7: {} - - electron-to-chromium@1.5.132: {} - - entities@4.5.0: {} - - esbuild@0.25.2: - 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 - - escalade@3.2.0: {} - - escape-string-regexp@4.0.0: {} - - eslint-compat-utils@0.5.1(eslint@9.24.0): - dependencies: - eslint: 9.24.0 - semver: 7.7.1 - - eslint-config-prettier@9.1.0(eslint@9.24.0): - dependencies: - eslint: 9.24.0 - - eslint-plugin-svelte@2.46.1(eslint@9.24.0)(svelte@5.25.7): - dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0) - '@jridgewell/sourcemap-codec': 1.5.0 - eslint: 9.24.0 - eslint-compat-utils: 0.5.1(eslint@9.24.0) - esutils: 2.0.3 - known-css-properties: 0.35.0 - postcss: 8.5.3 - postcss-load-config: 3.1.4(postcss@8.5.3) - postcss-safe-parser: 6.0.0(postcss@8.5.3) - postcss-selector-parser: 6.1.2 - semver: 7.7.1 - svelte-eslint-parser: 0.43.0(svelte@5.25.7) - optionalDependencies: - svelte: 5.25.7 - transitivePeerDependencies: - - ts-node - - eslint-scope@7.2.2: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-scope@8.3.0: - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - - eslint-visitor-keys@3.4.3: {} - - eslint-visitor-keys@4.2.0: {} - - eslint@9.24.0: - dependencies: - '@eslint-community/eslint-utils': 4.5.1(eslint@9.24.0) - '@eslint-community/regexpp': 4.12.1 - '@eslint/config-array': 0.20.0 - '@eslint/config-helpers': 0.2.1 - '@eslint/core': 0.12.0 - '@eslint/eslintrc': 3.3.1 - '@eslint/js': 9.24.0 - '@eslint/plugin-kit': 0.2.8 - '@humanfs/node': 0.16.6 - '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.2 - '@types/estree': 1.0.7 - '@types/json-schema': 7.0.15 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.6 - debug: 4.4.0 - escape-string-regexp: 4.0.0 - eslint-scope: 8.3.0 - eslint-visitor-keys: 4.2.0 - espree: 10.3.0 - esquery: 1.6.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 8.0.0 - find-up: 5.0.0 - glob-parent: 6.0.2 - ignore: 5.3.2 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - 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.4 - transitivePeerDependencies: - - supports-color - - esm-env@1.2.2: {} - - espree@10.3.0: - dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 4.2.0 - - espree@9.6.1: - dependencies: - acorn: 8.14.1 - acorn-jsx: 5.3.2(acorn@8.14.1) - eslint-visitor-keys: 3.4.3 - - esprima@4.0.1: {} - - esquery@1.6.0: - dependencies: - estraverse: 5.3.0 - - esrap@1.4.6: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - esrecurse@4.3.0: - dependencies: - estraverse: 5.3.0 - - estraverse@5.3.0: {} - - estree-walker@2.0.2: {} - - esutils@2.0.3: {} - - fast-deep-equal@3.1.3: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - - fast-json-stable-stringify@2.1.0: {} - - fast-levenshtein@2.0.6: {} - - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - - fdir@6.4.3(picomatch@4.0.2): - optionalDependencies: - picomatch: 4.0.2 - - file-entry-cache@8.0.0: - dependencies: - flat-cache: 4.0.1 - - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@5.0.0: - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - - flat-cache@4.0.1: - dependencies: - flatted: 3.3.3 - keyv: 4.5.4 - - flatted@3.3.3: {} - - forwarded-parse@2.1.2: {} - - fs.realpath@1.0.0: {} - - fsevents@2.3.3: - optional: true - - function-bind@1.1.2: {} - - gensync@1.0.0-beta.2: {} - - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - - glob-parent@6.0.2: - dependencies: - is-glob: 4.0.3 - - glob@9.3.5: - dependencies: - fs.realpath: 1.0.0 - minimatch: 8.0.4 - minipass: 4.2.8 - path-scurry: 1.11.1 - - globals@11.12.0: {} - - globals@14.0.0: {} - - globals@16.0.0: {} - - globalyzer@0.1.0: {} - - globrex@0.1.2: {} - - graphemer@1.4.0: {} - - has-flag@4.0.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - htmlparser2@8.0.2: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.2.2 - entities: 4.5.0 - - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - - ignore@5.3.2: {} - - immutable@5.1.1: {} - - import-fresh@3.3.1: - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - - import-in-the-middle@1.13.1: - dependencies: - acorn: 8.14.1 - acorn-import-attributes: 1.9.5(acorn@8.14.1) - cjs-module-lexer: 1.4.3 - module-details-from-path: 1.0.3 - - import-meta-resolve@4.1.0: {} - - imurmurhash@0.1.4: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-core-module@2.16.1: - dependencies: - hasown: 2.0.2 - - is-extglob@2.1.1: {} - - is-glob@4.0.3: - dependencies: - is-extglob: 2.1.1 - - is-module@1.0.0: {} - - is-number@7.0.0: {} - - is-plain-object@5.0.0: {} - - is-reference@1.2.1: - dependencies: - '@types/estree': 1.0.7 - - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.7 - - isexe@2.0.0: {} - - js-tokens@4.0.0: {} - - js-yaml@4.1.0: - dependencies: - argparse: 2.0.1 - - jsesc@3.1.0: {} - - json-buffer@3.0.1: {} - - json-schema-traverse@0.4.1: {} - - json-stable-stringify-without-jsonify@1.0.1: {} - - json5@2.2.3: {} - - keyv@4.5.4: - dependencies: - json-buffer: 3.0.1 - - kleur@4.1.5: {} - - known-css-properties@0.35.0: {} - - levn@0.4.1: - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - - lilconfig@2.1.0: {} - - linkify-it@5.0.0: - dependencies: - uc.micro: 2.1.0 - - locate-character@3.0.0: {} - - locate-path@6.0.0: - dependencies: - p-locate: 5.0.0 - - lodash.merge@4.6.2: {} - - lru-cache@10.4.3: {} - - lru-cache@5.1.1: - dependencies: - yallist: 3.1.1 - - luxon@3.6.1: {} - - magic-string@0.30.17: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - magic-string@0.30.7: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - magic-string@0.30.8: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - - markdown-it@14.1.0: - dependencies: - argparse: 2.0.1 - entities: 4.5.0 - linkify-it: 5.0.0 - mdurl: 2.0.0 - punycode.js: 2.3.1 - uc.micro: 2.1.0 - - mdurl@2.0.0: {} - - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - minidenticons@4.2.1: {} - - minimatch@3.1.2: - dependencies: - brace-expansion: 1.1.11 - - minimatch@8.0.4: - dependencies: - brace-expansion: 2.0.1 - - minimatch@9.0.5: - dependencies: - brace-expansion: 2.0.1 - - minimist@1.2.8: {} - - minipass@4.2.8: {} - - minipass@7.1.2: {} - - module-details-from-path@1.0.3: {} - - mri@1.2.0: {} - - mrmime@2.0.1: {} - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - natural-compare@1.4.0: {} - - node-addon-api@7.1.1: - optional: true - - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - - node-releases@2.0.19: {} - - normalize-path@3.0.0: {} - - optionator@0.9.4: - dependencies: - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - word-wrap: 1.2.5 - - p-limit@3.1.0: - dependencies: - yocto-queue: 0.1.0 - - p-locate@5.0.0: - dependencies: - p-limit: 3.1.0 - - parent-module@1.0.1: - dependencies: - callsites: 3.1.0 - - parse-srcset@1.0.2: {} - - path-exists@4.0.0: {} - - path-key@3.1.1: {} - - path-parse@1.0.7: {} - - path-scurry@1.11.1: - dependencies: - lru-cache: 10.4.3 - minipass: 7.1.2 - - pg-int8@1.0.1: {} - - pg-protocol@1.8.0: {} - - pg-types@2.2.0: - dependencies: - pg-int8: 1.0.1 - postgres-array: 2.0.0 - postgres-bytea: 1.0.0 - postgres-date: 1.0.7 - postgres-interval: 1.2.0 - - picocolors@1.1.1: {} - - picomatch@2.3.1: {} - - picomatch@4.0.2: {} - - postcss-load-config@3.1.4(postcss@8.5.3): - dependencies: - lilconfig: 2.1.0 - yaml: 1.10.2 - optionalDependencies: - postcss: 8.5.3 - - postcss-safe-parser@6.0.0(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-scss@4.0.9(postcss@8.5.3): - dependencies: - postcss: 8.5.3 - - postcss-selector-parser@6.1.2: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - - postcss@8.5.3: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postgres-array@2.0.0: {} - - postgres-bytea@1.0.0: {} - - postgres-date@1.0.7: {} - - postgres-interval@1.2.0: - dependencies: - xtend: 4.0.2 - - prelude-ls@1.2.1: {} - - prettier-plugin-svelte@3.3.3(prettier@3.5.3)(svelte@5.25.7): - dependencies: - prettier: 3.5.3 - svelte: 5.25.7 - - prettier@3.5.3: {} - - pretty-bytes@6.1.1: {} - - progress@2.0.3: {} - - proxy-from-env@1.1.0: {} - - punycode.js@2.3.1: {} - - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - readdirp@4.1.2: {} - - recast@0.23.11: - dependencies: - ast-types: 0.16.1 - esprima: 4.0.1 - source-map: 0.6.1 - tiny-invariant: 1.3.3 - tslib: 2.8.1 - - require-in-the-middle@7.5.2: - dependencies: - debug: 4.4.0 - module-details-from-path: 1.0.3 - resolve: 1.22.10 - transitivePeerDependencies: - - supports-color - - resolve-from@4.0.0: {} - - resolve@1.22.10: - dependencies: - is-core-module: 2.16.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - - reusify@1.1.0: {} - - rollup@4.39.0: - dependencies: - '@types/estree': 1.0.7 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.39.0 - '@rollup/rollup-android-arm64': 4.39.0 - '@rollup/rollup-darwin-arm64': 4.39.0 - '@rollup/rollup-darwin-x64': 4.39.0 - '@rollup/rollup-freebsd-arm64': 4.39.0 - '@rollup/rollup-freebsd-x64': 4.39.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.39.0 - '@rollup/rollup-linux-arm-musleabihf': 4.39.0 - '@rollup/rollup-linux-arm64-gnu': 4.39.0 - '@rollup/rollup-linux-arm64-musl': 4.39.0 - '@rollup/rollup-linux-loongarch64-gnu': 4.39.0 - '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0 - '@rollup/rollup-linux-riscv64-gnu': 4.39.0 - '@rollup/rollup-linux-riscv64-musl': 4.39.0 - '@rollup/rollup-linux-s390x-gnu': 4.39.0 - '@rollup/rollup-linux-x64-gnu': 4.39.0 - '@rollup/rollup-linux-x64-musl': 4.39.0 - '@rollup/rollup-win32-arm64-msvc': 4.39.0 - '@rollup/rollup-win32-ia32-msvc': 4.39.0 - '@rollup/rollup-win32-x64-msvc': 4.39.0 - fsevents: 2.3.3 - - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - - sade@1.8.1: - dependencies: - mri: 1.2.0 - - sanitize-html@2.15.0: - dependencies: - deepmerge: 4.3.1 - escape-string-regexp: 4.0.0 - htmlparser2: 8.0.2 - is-plain-object: 5.0.0 - parse-srcset: 1.0.2 - postcss: 8.5.3 - - sass@1.86.3: - dependencies: - chokidar: 4.0.3 - immutable: 5.1.1 - source-map-js: 1.2.1 - optionalDependencies: - '@parcel/watcher': 2.5.1 - - semver@6.3.1: {} - - semver@7.7.1: {} - - set-cookie-parser@2.7.1: {} - - shebang-command@2.0.0: - dependencies: - shebang-regex: 3.0.0 - - shebang-regex@3.0.0: {} - - shimmer@1.2.1: {} - - sirv@3.0.1: - dependencies: - '@polka/url': 1.0.0-next.28 - mrmime: 2.0.1 - totalist: 3.0.1 - - sorcery@1.0.0: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.0 - minimist: 1.2.8 - tiny-glob: 0.2.9 - - source-map-js@1.2.1: {} - - source-map@0.6.1: {} - - strip-json-comments@3.1.1: {} - - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - supports-preserve-symlinks-flag@1.0.0: {} - - svelte-bootstrap-icons@3.1.2: {} - - svelte-check@4.1.5(picomatch@4.0.2)(svelte@5.25.7)(typescript@5.8.3): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - chokidar: 4.0.3 - fdir: 6.4.3(picomatch@4.0.2) - picocolors: 1.1.1 - sade: 1.8.1 - svelte: 5.25.7 - typescript: 5.8.3 - transitivePeerDependencies: - - picomatch - - svelte-easy-crop@4.0.1(svelte@5.25.7): - dependencies: - svelte: 5.25.7 - - svelte-eslint-parser@0.43.0(svelte@5.25.7): - dependencies: - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - postcss: 8.5.3 - postcss-scss: 4.0.9(postcss@8.5.3) - optionalDependencies: - svelte: 5.25.7 - - svelte-tippy@1.3.2: - dependencies: - tippy.js: 6.3.7 - - svelte@5.25.7: - dependencies: - '@ampproject/remapping': 2.3.0 - '@jridgewell/sourcemap-codec': 1.5.0 - '@sveltejs/acorn-typescript': 1.0.5(acorn@8.14.1) - '@types/estree': 1.0.7 - acorn: 8.14.1 - aria-query: 5.3.2 - axobject-query: 4.1.0 - clsx: 2.1.1 - esm-env: 1.2.2 - esrap: 1.4.6 - is-reference: 3.0.3 - locate-character: 3.0.0 - magic-string: 0.30.17 - zimmerframe: 1.1.2 - - sveltekit-i18n@2.4.2(svelte@5.25.7): - dependencies: - '@sveltekit-i18n/base': 1.3.7(svelte@5.25.7) - '@sveltekit-i18n/parser-default': 1.1.1 - svelte: 5.25.7 - - tiny-glob@0.2.9: - dependencies: - globalyzer: 0.1.0 - globrex: 0.1.2 - - tiny-invariant@1.3.3: {} - - tippy.js@6.3.7: - dependencies: - '@popperjs/core': 2.11.8 - - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - totalist@3.0.1: {} - - tr46@0.0.3: {} - - ts-api-utils@2.1.0(typescript@5.8.3): - dependencies: - typescript: 5.8.3 - - tslib@2.8.1: {} - - tslog@4.9.3: {} - - type-check@0.4.0: - dependencies: - prelude-ls: 1.2.1 - - typescript-eslint@8.29.0(eslint@9.24.0)(typescript@5.8.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.29.0(@typescript-eslint/parser@8.29.0(eslint@9.24.0)(typescript@5.8.3))(eslint@9.24.0)(typescript@5.8.3) - '@typescript-eslint/parser': 8.29.0(eslint@9.24.0)(typescript@5.8.3) - '@typescript-eslint/utils': 8.29.0(eslint@9.24.0)(typescript@5.8.3) - eslint: 9.24.0 - typescript: 5.8.3 - transitivePeerDependencies: - - supports-color - - typescript@5.8.3: {} - - uc.micro@2.1.0: {} - - undici-types@6.21.0: {} - - unplugin@1.0.1: - dependencies: - acorn: 8.14.1 - chokidar: 3.6.0 - webpack-sources: 3.2.3 - webpack-virtual-modules: 0.5.0 - - update-browserslist-db@1.1.3(browserslist@4.24.4): - dependencies: - browserslist: 4.24.4 - escalade: 3.2.0 - picocolors: 1.1.1 - - uri-js@4.4.1: - dependencies: - punycode: 2.3.1 - - util-deprecate@1.0.2: {} - - vite@6.2.5(@types/node@22.14.0)(sass@1.86.3): - dependencies: - esbuild: 0.25.2 - postcss: 8.5.3 - rollup: 4.39.0 - optionalDependencies: - '@types/node': 22.14.0 - fsevents: 2.3.3 - sass: 1.86.3 - - vitefu@1.0.6(vite@6.2.5(@types/node@22.14.0)(sass@1.86.3)): - optionalDependencies: - vite: 6.2.5(@types/node@22.14.0)(sass@1.86.3) - - webidl-conversions@3.0.1: {} - - webpack-sources@3.2.3: {} - - webpack-virtual-modules@0.5.0: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - - which@2.0.2: - dependencies: - isexe: 2.0.0 - - word-wrap@1.2.5: {} - - xtend@4.0.2: {} - - yallist@3.1.1: {} - - yaml@1.10.2: {} - - yocto-queue@0.1.0: {} - - zimmerframe@1.1.2: {} diff --git a/build.sh b/build.sh index e14eb53..949e0b1 100755 --- a/build.sh +++ b/build.sh @@ -25,12 +25,12 @@ echo "Building Node.js frontend" cd "$ROOT_DIR/Foxnouns.Frontend" [ -d "$ROOT_DIR/Foxnouns.Frontend/build" ] && rm -r "$ROOT_DIR/Foxnouns.Frontend/build" -pnpm install -pnpm build +npm ci +npm run build mkdir "$ROOT_DIR/build/fe" -cp -r build .env* package.json pnpm-lock.yaml "$ROOT_DIR/build/fe" +cp -r build .env* package.json package-lock.json "$ROOT_DIR/build/fe" cd "$ROOT_DIR/build/fe" -pnpm install -P +NODE_ENV=production npm ci echo "Finished building Foxnouns.NET" diff --git a/package.json b/package.json index 2d79864..12db760 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,7 @@ }, "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 'pnpm watch:be' 'cd Foxnouns.Frontend && pnpm dev' 'cd rate && go run -v .'", - "format": "dotnet csharpier . && cd Foxnouns.Frontend && pnpm format" - }, - "packageManager": "pnpm@9.15.0+sha512.76e2379760a4328ec4415815bcd6628dee727af3779aaa4c914e3944156c4299921a89f976381ee107d41f12cfa4b66681ca9c718f0668fa0831ed4c6d8ba56c" + "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" + } } From bcdb2f9540871fedded4b59b70663e7d23a89c71 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 17 Apr 2025 15:29:55 +0200 Subject: [PATCH 260/261] fix(frontend): actually show eslint errors in vscode, fix eslint errors --- Foxnouns.Frontend/.vscode/settings.json | 3 ++- .../src/lib/components/GlobalNotice.svelte | 1 + .../lib/components/admin/AuditLogEntryCard.svelte | 1 + .../components/admin/ClosedReportAuditLog.svelte | 1 + Foxnouns.Frontend/src/lib/i18n/locales/en.json | 3 ++- .../src/routes/admin/reports/[id]/+page.svelte | 1 + .../src/routes/page/[page]/+page.svelte | 1 + .../src/routes/settings/prefs/+page.svelte | 14 +++++++++++++- 8 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Foxnouns.Frontend/.vscode/settings.json b/Foxnouns.Frontend/.vscode/settings.json index 5703e7f..c5d12a5 100644 --- a/Foxnouns.Frontend/.vscode/settings.json +++ b/Foxnouns.Frontend/.vscode/settings.json @@ -4,5 +4,6 @@ "i18n-ally.localesPaths": ["src/lib/i18n", "src/lib/i18n/locales"], "i18n-ally.keystyle": "nested", "explorer.sortOrder": "filesFirst", - "explorer.compactFolders": false + "explorer.compactFolders": false, + "eslint.validate": ["javascript", "javascriptreact", "svelte"] } diff --git a/Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte b/Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte index 3d1c718..c8a55f1 100644 --- a/Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte +++ b/Foxnouns.Frontend/src/lib/components/GlobalNotice.svelte @@ -34,6 +34,7 @@ {#if renderNotice}