diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 44a73ec..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "eslint.workingDirectories": [ - { - "mode": "auto" - } - ] -} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..27fda59 --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2022, Daniel Luiz Alves (danielalves96) +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c06b092 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Palmr. \ No newline at end of file diff --git a/apps/server/.dockerignore b/apps/server/.dockerignore new file mode 100644 index 0000000..e4925b1 --- /dev/null +++ b/apps/server/.dockerignore @@ -0,0 +1,9 @@ +node_modules +dist +.env +.env.* +*.log +.git +.gitignore +.next +.cache \ No newline at end of file diff --git a/apps/server/.env.example b/apps/server/.env.example new file mode 100644 index 0000000..3aec63f --- /dev/null +++ b/apps/server/.env.example @@ -0,0 +1,12 @@ +DATABASE_URL="postgresql://palmr:palmr123@localhost:5432/palmr?schema=public" + +FRONTEND_URL="http://localhost:3000" +MINIO_ENDPOINT="localhost" +MINIO_PORT="9000" +MINIO_USE_SSL="false" +MINIO_ROOT_USER="palmr" +MINIO_ROOT_PASSWORD="palmr123" +MINIO_REGION="sa-east-1" +MINIO_BUCKET_NAME="files" + +PORT=3333 diff --git a/apps/server/.gitignore b/apps/server/.gitignore new file mode 100644 index 0000000..42ec2e3 --- /dev/null +++ b/apps/server/.gitignore @@ -0,0 +1,3 @@ +node_modules +.env +dist/* diff --git a/apps/server/.prettierrc.json b/apps/server/.prettierrc.json new file mode 100644 index 0000000..af95e92 --- /dev/null +++ b/apps/server/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "printWidth": 120, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile new file mode 100644 index 0000000..afdc832 --- /dev/null +++ b/apps/server/Dockerfile @@ -0,0 +1,26 @@ +FROM node:18 + +WORKDIR /app/server + +RUN apt-get update && apt-get install -y netcat-traditional +RUN npm install -g pnpm + +COPY package*.json ./ +COPY pnpm-lock.yaml ./ +COPY prisma ./prisma/ +COPY scripts ./scripts/ + +RUN rm -rf node_modules/.prisma + +RUN pnpm install --frozen-lockfile + +RUN npx prisma generate + +COPY . . + +RUN pnpm build +RUN chmod +x ./scripts/start.sh + +EXPOSE 3333 + +CMD ["./scripts/start.sh"] diff --git a/apps/server/docker-compose-dev.yaml b/apps/server/docker-compose-dev.yaml new file mode 100644 index 0000000..d8681fd --- /dev/null +++ b/apps/server/docker-compose-dev.yaml @@ -0,0 +1,62 @@ +services: + minio: + image: minio/minio:latest + container_name: minio + ports: + - "9000:9000" + - "9001:9001" + environment: + - MINIO_ROOT_USER=palmr + - MINIO_ROOT_PASSWORD=palmr123 + volumes: + - minio_data:/data + command: server /data --console-address ":9001" + restart: "unless-stopped" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"] + interval: 10s + timeout: 5s + retries: 5 + + minio-init: + image: minio/mc:latest + container_name: minio-init + depends_on: + minio: + condition: service_healthy + entrypoint: > + sh -c " + mc alias set myminio http://minio:9000 palmr palmr123 && + mc mb myminio/files --ignore-existing + " + restart: "no" + environment: + MINIO_ROOT_USER: palmr + MINIO_ROOT_PASSWORD: palmr123 + + postgres: + image: bitnami/postgresql:17.2.0 + container_name: palmr-postgres + ports: + - "5432:5432" + environment: + - POSTGRESQL_USERNAME=palmr + - POSTGRESQL_PASSWORD=palmr123 + - POSTGRESQL_DATABASE=palmr + volumes: + - postgres_data:/bitnami/postgresql + restart: "unless-stopped" + healthcheck: + test: ["CMD", "pg_isready", "-U", "palmr"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + minio_data: + postgres_data: + +networks: + default: + name: palmr-network + driver: bridge diff --git a/apps/server/eslint.config.mjs b/apps/server/eslint.config.mjs new file mode 100644 index 0000000..d852f06 --- /dev/null +++ b/apps/server/eslint.config.mjs @@ -0,0 +1,24 @@ +import pluginJs from "@eslint/js"; +import eslintConfigPrettier from "eslint-config-prettier"; +import importPlugin from "eslint-plugin-import"; +import globals from "globals"; +import tseslint from "typescript-eslint"; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + { files: ["**/*.{js,mjs,cjs,ts}"] }, + { languageOptions: { globals: globals.browser } }, + pluginJs.configs.recommended, + importPlugin.flatConfigs.recommended, + eslintConfigPrettier, + ...tseslint.configs.recommended, + { + rules: { + "import/no-unresolved": "off", + "import/no-named-as-default": "off", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-require-imports": "off", + }, + }, +]; diff --git a/apps/server/package.json b/apps/server/package.json new file mode 100644 index 0000000..eaf06fe --- /dev/null +++ b/apps/server/package.json @@ -0,0 +1,53 @@ +{ + "name": "palmr-api", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", + "lint:fix": "eslint \"src/**/*.{js,jsx,ts,tsx}\" --fix", + "format": "prettier . --write", + "format:check": "prettier . --check", + "db:seed": "ts-node prisma/seed.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@fastify/cookie": "^11.0.2", + "@fastify/cors": "^10.0.2", + "@fastify/jwt": "^9.0.3", + "@fastify/multipart": "^9.0.3", + "@fastify/swagger": "^9.4.2", + "@fastify/swagger-ui": "^5.2.1", + "@prisma/client": "^6.3.1", + "@scalar/fastify-api-reference": "^1.25.116", + "bcryptjs": "^2.4.3", + "fastify": "^5.2.1", + "fastify-type-provider-zod": "^4.0.2", + "minio": "^8.0.4", + "nodemailer": "^6.10.0", + "sharp": "^0.33.5", + "zod": "^3.24.1" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/bcryptjs": "^2.4.6", + "@types/node": "^22.13.4", + "@types/nodemailer": "^6.4.17", + "eslint": "^9.19.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-import": "^2.31.0", + "globals": "^15.14.0", + "prettier": "3.4.2", + "prisma": "^6.3.1", + "ts-node": "^10.9.2", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "typescript-eslint": "^8.23.0" + } +} \ No newline at end of file diff --git a/apps/server/pnpm-lock.yaml b/apps/server/pnpm-lock.yaml new file mode 100644 index 0000000..f6ac49f --- /dev/null +++ b/apps/server/pnpm-lock.yaml @@ -0,0 +1,4243 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@fastify/cookie': + specifier: ^11.0.2 + version: 11.0.2 + '@fastify/cors': + specifier: ^10.0.2 + version: 10.0.2 + '@fastify/jwt': + specifier: ^9.0.3 + version: 9.0.4 + '@fastify/multipart': + specifier: ^9.0.3 + version: 9.0.3 + '@fastify/swagger': + specifier: ^9.4.2 + version: 9.4.2 + '@fastify/swagger-ui': + specifier: ^5.2.1 + version: 5.2.2 + '@prisma/client': + specifier: ^6.3.1 + version: 6.4.1(prisma@6.4.1(typescript@5.7.3))(typescript@5.7.3) + '@scalar/fastify-api-reference': + specifier: ^1.25.116 + version: 1.25.122 + bcryptjs: + specifier: ^2.4.3 + version: 2.4.3 + fastify: + specifier: ^5.2.1 + version: 5.2.1 + fastify-type-provider-zod: + specifier: ^4.0.2 + version: 4.0.2(fastify@5.2.1)(zod@3.24.2) + minio: + specifier: ^8.0.4 + version: 8.0.4 + nodemailer: + specifier: ^6.10.0 + version: 6.10.0 + sharp: + specifier: ^0.33.5 + version: 0.33.5 + zod: + specifier: ^3.24.1 + version: 3.24.2 + devDependencies: + '@eslint/js': + specifier: ^9.19.0 + version: 9.20.0 + '@trivago/prettier-plugin-sort-imports': + specifier: ^5.2.2 + version: 5.2.2(prettier@3.4.2) + '@types/bcryptjs': + specifier: ^2.4.6 + version: 2.4.6 + '@types/node': + specifier: ^22.13.4 + version: 22.13.4 + '@types/nodemailer': + specifier: ^6.4.17 + version: 6.4.17 + eslint: + specifier: ^9.19.0 + version: 9.20.1 + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.0.1(eslint@9.20.1) + eslint-plugin-import: + specifier: ^2.31.0 + version: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1) + globals: + specifier: ^15.14.0 + version: 15.15.0 + prettier: + specifier: 3.4.2 + version: 3.4.2 + prisma: + specifier: ^6.3.1 + version: 6.4.1(typescript@5.7.3) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@22.13.4)(typescript@5.7.3) + tsx: + specifier: ^4.19.2 + version: 4.19.3 + typescript: + specifier: ^5.7.3 + version: 5.7.3 + typescript-eslint: + specifier: ^8.23.0 + version: 8.24.1(eslint@9.20.1)(typescript@5.7.3) + +packages: + + '@babel/code-frame@7.26.2': + resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.26.9': + resolution: {integrity: sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==} + engines: {node: '>=6.9.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/parser@7.26.9': + resolution: {integrity: sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.26.9': + resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.26.9': + resolution: {integrity: sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.26.9': + resolution: {integrity: sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==} + engines: {node: '>=6.9.0'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@esbuild/aix-ppc64@0.25.0': + resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.0': + resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.0': + resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.0': + resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.0': + resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.0': + resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.0': + resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.0': + resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.0': + resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.0': + resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.0': + resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.0': + resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.0': + resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.0': + resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.0': + resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.0': + resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.0': + resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.0': + resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.0': + resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.0': + resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.0': + resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.0': + resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.0': + resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.0': + resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.0': + resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + engines: {node: '>=18'} + 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.2': + resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.11.0': + resolution: {integrity: sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==} + 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/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.20.0': + resolution: {integrity: sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==} + 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.7': + resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@fastify/accept-negotiator@2.0.1': + resolution: {integrity: sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==} + + '@fastify/ajv-compiler@4.0.2': + resolution: {integrity: sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==} + + '@fastify/busboy@3.1.1': + resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==} + + '@fastify/cookie@11.0.2': + resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==} + + '@fastify/cors@10.0.2': + resolution: {integrity: sha512-DGdxOG36sS/tZv1NFiCJGi7wGuXOSPL2CmNX5PbOVKx0C6LuIALRMrqLByHTCcX1Rbl8NJ9IWlJex32bzydvlw==} + + '@fastify/deepmerge@2.0.2': + resolution: {integrity: sha512-3wuLdX5iiiYeZWP6bQrjqhrcvBIf0NHbQH1Ur1WbHvoiuTYUEItgygea3zs8aHpiitn0lOB8gX20u1qO+FDm7Q==} + + '@fastify/error@4.0.0': + resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==} + + '@fastify/fast-json-stringify-compiler@5.0.2': + resolution: {integrity: sha512-YdR7gqlLg1xZAQa+SX4sMNzQHY5pC54fu9oC5aYSUqBhyn6fkLkrdtKlpVdCNPlwuUuXA1PjFTEmvMF6ZVXVGw==} + + '@fastify/forwarded@3.0.0': + resolution: {integrity: sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==} + + '@fastify/jwt@9.0.4': + resolution: {integrity: sha512-OJ5moNArmagwYIAeAE5xRI/cnZk7Nvuc+kz7Q+EWzODBszBfQaaeRABIkPD8nIkDrOh1fOnlNyrSpNV/QomiaQ==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/multipart@9.0.3': + resolution: {integrity: sha512-pJogxQCrT12/6I5Fh6jr3narwcymA0pv4B0jbC7c6Bl9wnrxomEUnV0d26w6gUls7gSXmhG8JGRMmHFIPsxt1g==} + + '@fastify/proxy-addr@5.0.0': + resolution: {integrity: sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==} + + '@fastify/send@3.3.1': + resolution: {integrity: sha512-6pofeVwaHN+E/MAofCwDqkWUliE3i++jlD0VH/LOfU8TJlCkMUSgKvA9bawDdVXxjve7XrdYMyDmkiYaoGWEtA==} + + '@fastify/static@8.1.1': + resolution: {integrity: sha512-TW9eyVHJLytZNpBlSIqd0bl1giJkEaRaPZG+5AT3L/OBKq9U8D7g/OYmc2NPQZnzPURGhMt3IAWuyVkvd2nOkQ==} + + '@fastify/swagger-ui@5.2.2': + resolution: {integrity: sha512-jf8xe+D8Xjc8TqrZhtlJImOWihd8iYFu8dhM01mGg+F04CKUM0zGB9aADE9nxzRUszyWp3wn+uWk89nbAoBMCw==} + + '@fastify/swagger@9.4.2': + resolution: {integrity: sha512-WjSUu6QnmysLx1GeX7+oQAQUG/vBK5L8Qzcsht2SEpZiykpHURefMZpf+u3XbwSuH7TjeWOPgGIJIsEgj8AvxQ==} + + '@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'} + + '@img/sharp-darwin-arm64@0.33.5': + resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.33.5': + resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.0.4': + resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.0.4': + resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.0.4': + resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linux-arm@1.0.5': + resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + cpu: [arm] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.0.4': + resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + cpu: [s390x] + os: [linux] + + '@img/sharp-libvips-linux-x64@1.0.4': + resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + cpu: [x64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + cpu: [arm64] + os: [linux] + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + cpu: [x64] + os: [linux] + + '@img/sharp-linux-arm64@0.33.5': + resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linux-arm@0.33.5': + resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + + '@img/sharp-linux-s390x@0.33.5': + resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + + '@img/sharp-linux-x64@0.33.5': + resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-linuxmusl-arm64@0.33.5': + resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + + '@img/sharp-linuxmusl-x64@0.33.5': + resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + + '@img/sharp-wasm32@0.33.5': + resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-ia32@0.33.5': + resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.33.5': + resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@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==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lukeed/ms@2.0.2': + resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} + engines: {node: '>=8'} + + '@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'} + + '@prisma/client@6.4.1': + resolution: {integrity: sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/debug@6.4.1': + resolution: {integrity: sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==} + + '@prisma/engines-version@6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d': + resolution: {integrity: sha512-Xq54qw55vaCGrGgIJqyDwOq0TtjZPJEWsbQAHugk99hpDf2jcEeQhUcF+yzEsSqegBaDNLA4IC8Nn34sXmkiTQ==} + + '@prisma/engines@6.4.1': + resolution: {integrity: sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==} + + '@prisma/fetch-engine@6.4.1': + resolution: {integrity: sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==} + + '@prisma/get-platform@6.4.1': + resolution: {integrity: sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==} + + '@rtsao/scc@1.1.0': + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + + '@scalar/fastify-api-reference@1.25.122': + resolution: {integrity: sha512-JieuQ3UYcnOZK4rM5K73hCKgHutI5G6icbivNfu0bIW2KsVVe/GFWeb0rDS4f8V5TEYydRQcKJXfJQsBe5SJwQ==} + engines: {node: '>=18'} + + '@scalar/openapi-types@0.1.8': + resolution: {integrity: sha512-iufA5/6hPCmRIVD2eh7qGpoKvoA08Gw/qUb2JECifBtAwA93fo7+1k9uHK440f2LMJsbxIzA+nv7RS0BmfiO/g==} + engines: {node: '>=18'} + + '@scalar/types@0.0.34': + resolution: {integrity: sha512-q01ctijmHArM5KOny2zU+sHfhpsgOAENrDENecK2TsQNn5FYLmFZouMKeW2M6F7KFLPZnFxUiL/rT88b6Rp/Kg==} + engines: {node: '>=18'} + + '@trivago/prettier-plugin-sort-imports@5.2.2': + resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==} + engines: {node: '>18.12'} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/bcryptjs@2.4.6': + resolution: {integrity: sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==} + + '@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/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@22.13.4': + resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} + + '@types/nodemailer@6.4.17': + resolution: {integrity: sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==} + + '@typescript-eslint/eslint-plugin@8.24.1': + resolution: {integrity: sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==} + 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.8.0' + + '@typescript-eslint/parser@8.24.1': + resolution: {integrity: sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==} + 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/scope-manager@8.24.1': + resolution: {integrity: sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/type-utils@8.24.1': + resolution: {integrity: sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==} + 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/types@8.24.1': + resolution: {integrity: sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.24.1': + resolution: {integrity: sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.8.0' + + '@typescript-eslint/utils@8.24.1': + resolution: {integrity: sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==} + 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.24.1': + resolution: {integrity: sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@unhead/schema@1.11.19': + resolution: {integrity: sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg==} + + '@zxing/text-encoding@0.9.0': + resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.1.0: + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlastindex@1.2.5: + resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bcryptjs@2.4.3: + resolution: {integrity: sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==} + + block-stream2@2.1.0: + resolution: {integrity: sha512-suhjmLI57Ewpmq00qaygS8UgEq2ly2PCItenIyhMqVjo4t4pGzqMvfgJuX8iWTeSDdfSSqS6j38fL4ToNL7Pfg==} + + bn.js@4.12.1: + resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + + 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'} + + browser-or-node@2.1.1: + resolution: {integrity: sha512-8CVjaLJGuSKMVTxJ2DpBl5XnlNDiT4cQFeuCJJrvJmts9YrTZDizTX7PjC2s6W4x+MBGZeEY6dGMrF04/6Hgqg==} + + buffer-crc32@1.0.0: + resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} + engines: {node: '>=8.0.0'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.3: + resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + engines: {node: '>= 0.4'} + + 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'} + + 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==} + + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + + color@4.2.3: + resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} + engines: {node: '>=12.5.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: 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 + + decode-uri-component@0.2.2: + resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} + engines: {node: '>=0.10'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.0.3: + resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + engines: {node: '>=8'} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + es-abstract@1.23.9: + resolution: {integrity: sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild-register@3.6.0: + resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} + peerDependencies: + esbuild: '>=0.12 <1' + + esbuild@0.25.0: + resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.0.1: + resolution: {integrity: sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-import-resolver-node@0.3.9: + resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + + eslint-module-utils@2.12.0: + resolution: {integrity: sha512-wALZ0HFoytlyh/1+4wuZ9FJCD/leWHQzzrxJ8+rebyReSLk7LApMyd3WJaLVoN+D5+WIdJyDK1c6JnE65V4Zyg==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: '*' + eslint-import-resolver-node: '*' + eslint-import-resolver-typescript: '*' + eslint-import-resolver-webpack: '*' + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + eslint: + optional: true + eslint-import-resolver-node: + optional: true + eslint-import-resolver-typescript: + optional: true + eslint-import-resolver-webpack: + optional: true + + eslint-plugin-import@2.31.0: + resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==} + engines: {node: '>=4'} + peerDependencies: + '@typescript-eslint/parser': '*' + eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 + peerDependenciesMeta: + '@typescript-eslint/parser': + optional: true + + 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.20.1: + resolution: {integrity: sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + 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'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + 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-json-stringify@6.0.1: + resolution: {integrity: sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==} + + fast-jwt@5.0.5: + resolution: {integrity: sha512-Ch94zewwBjRznO0r76NFI5FDT0lOtnzkWVO4r7+d7E2WKuf7WW1FVOWRpv7QGEFlXzz9OAayrb5BhEmkOkwjhg==} + engines: {node: '>=20'} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-redact@3.5.0: + resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} + engines: {node: '>=6'} + + fast-uri@3.0.6: + resolution: {integrity: sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==} + + fast-xml-parser@4.5.3: + resolution: {integrity: sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig==} + hasBin: true + + fastfall@1.5.1: + resolution: {integrity: sha512-KH6p+Z8AKPXnmA7+Iz2Lh8ARCMr+8WNPVludm1LGkZoD2MjY6LVnRMtTKhkdzI+jr0RzQWXKzKyBJm1zoHEL4Q==} + engines: {node: '>=0.10.0'} + + fastify-plugin@4.5.1: + resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} + + fastify-plugin@5.0.1: + resolution: {integrity: sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==} + + fastify-type-provider-zod@4.0.2: + resolution: {integrity: sha512-FDRzSL3ZuoZ+4YDevR1YOinmDKkxOdy3QB9dDR845sK+bQvDroPKhHAXLEAOObDxL7SMA0OZN/A4osrNBTdDTQ==} + peerDependencies: + fastify: ^5.0.0 + zod: ^3.14.2 + + fastify@5.2.1: + resolution: {integrity: sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==} + + fastparallel@2.4.1: + resolution: {integrity: sha512-qUmhxPgNHmvRjZKBFUNI0oZuuH9OlSIOXmJ98lhKPxMZZ7zS/Fi0wRHOihDSz0R1YiIOjxzOY4bq65YTcdBi2Q==} + + fastq@1.19.0: + resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} + + fastseries@1.7.2: + resolution: {integrity: sha512-dTPFrPGS8SNSzAt7u/CbMKCJ3s01N04s4JFbORHcmyvVfVKmbhMD1VtRbh5enGHxkaQDqWyLefiKOGGmohGDDQ==} + + 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'} + + filter-obj@1.1.0: + resolution: {integrity: sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==} + engines: {node: '>=0.10.0'} + + find-my-way@9.2.0: + resolution: {integrity: sha512-d3uCir8Hmg7W1Ywp8nKf2lJJYU9Nwinvo+1D39Dn09nz65UKXIxUh7j7K8zeWhxqe1WrkS7FJyON/Q/3lPoc6w==} + engines: {node: '>=14'} + + 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==} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + foreground-child@3.3.0: + resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + engines: {node: '>=14'} + + 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==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + get-intrinsic@1.2.7: + resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.0: + resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} + + 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@11.0.1: + resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==} + engines: {node: 20 || >=22} + hasBin: true + + 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@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + ipaddr.js@2.2.0: + resolution: {integrity: sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==} + engines: {node: '>= 10'} + + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.3.2: + resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jackspeak@4.0.3: + resolution: {integrity: sha512-oSwM7q8PTHQWuZAlp995iPpPJ4Vkl7qT0ZRD+9duL9j2oBy6KcTfyxc8mEuHJYC+z/kbps80aJLkaNzTOrf/kw==} + engines: {node: 20 || >=22} + + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + + 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-ref-resolver@2.0.1: + resolution: {integrity: sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==} + + json-schema-resolver@3.0.0: + resolution: {integrity: sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==} + engines: {node: '>=20'} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + 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==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lru-cache@11.0.2: + resolution: {integrity: sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==} + engines: {node: 20 || >=22} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + 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'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + + 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'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minio@8.0.4: + resolution: {integrity: sha512-GVW7y2PNbzjjFJ9opVMGKvDNuRkyz3bMt1q7UrHs7bsKFWLXbSvMPffjE/HkVYWUjlD8kQwMaeqiHhhvZJJOfQ==} + engines: {node: ^16 || ^18 || >=20} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mnemonist@0.39.8: + resolution: {integrity: sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + nodemailer@6.10.0: + resolution: {integrity: sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==} + engines: {node: '>=6.0.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.groupby@1.0.3: + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + 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'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + 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@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-std-serializers@7.0.0: + resolution: {integrity: sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==} + + pino@9.6.0: + resolution: {integrity: sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==} + hasBin: true + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.4.2: + resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} + engines: {node: '>=14'} + hasBin: true + + prisma@6.4.1: + resolution: {integrity: sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + query-string@7.1.3: + resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} + engines: {node: '>=6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safe-regex2@4.0.1: + resolution: {integrity: sha512-goqsB+bSlOmVX+CiFX2PFc1OV88j5jvBqIM+DgqrucHnUguAUNtiNOs+aTadq2NqsLQ+TQ3UEVG3gtSFcdlkCg==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.4.1: + resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + + secure-json-parse@3.0.2: + resolution: {integrity: sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==} + + 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==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + sharp@0.33.5: + resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + + 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'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + simple-swizzle@0.2.2: + resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} + + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + + split-on-first@1.1.0: + resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} + engines: {node: '>=6'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + steed@1.1.3: + resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==} + + stream-chain@2.2.5: + resolution: {integrity: sha512-1TJmBx6aSWqZ4tx7aTpBDXK0/e2hhcNSTV8+CbFJtDjbb+I1mZ8lHit0Grw9GRT+6JbIrrDd8esncgBi8aBXGA==} + + stream-json@1.9.1: + resolution: {integrity: sha512-uWkjJ+2Nt/LO9Z/JyKZbMusL8Dkh97uUBTv3AJQ74y07lVahLY4eEFsPsE97pxYBwr8nnjMAIch5eqI0gPShyw==} + + strict-uri-encode@2.0.0: + resolution: {integrity: sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==} + engines: {node: '>=4'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strnum@1.1.1: + resolution: {integrity: sha512-O7aCHfYCamLCctjAiaucmE+fHf2DYHkus2OKCn4Wv03sykfFtgeECn505X6K4mPl8CRNd/qurC9guq+ynoN4pw==} + + 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'} + + thread-stream@3.1.0: + resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==} + + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-api-utils@2.0.1: + resolution: {integrity: sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.19.3: + resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.24.1: + resolution: {integrity: sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==} + 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@5.7.3: + resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + engines: {node: '>=14.17'} + hasBin: true + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@6.20.0: + resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + util@0.12.5: + resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + web-encoding@1.1.5: + resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.18: + resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + engines: {node: '>= 0.4'} + + 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'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + yaml@2.7.0: + resolution: {integrity: sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==} + engines: {node: '>= 14'} + hasBin: true + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zhead@2.2.4: + resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} + + zod-to-json-schema@3.24.2: + resolution: {integrity: sha512-pNUqrcSxuuB3/+jBbU8qKUbTbDqYUaG1vf5cXFjbhGgoUuA1amO/y4Q8lzfOhHU8HNPK6VFJ18lBDKj3OHyDsg==} + peerDependencies: + zod: ^3.24.1 + + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + +snapshots: + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/generator@7.26.9': + dependencies: + '@babel/parser': 7.26.9 + '@babel/types': 7.26.9 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/parser@7.26.9': + dependencies: + '@babel/types': 7.26.9 + + '@babel/template@7.26.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.26.9 + '@babel/types': 7.26.9 + + '@babel/traverse@7.26.9': + dependencies: + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.26.9 + '@babel/parser': 7.26.9 + '@babel/template': 7.26.9 + '@babel/types': 7.26.9 + debug: 4.4.0 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.26.9': + dependencies: + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@esbuild/aix-ppc64@0.25.0': + optional: true + + '@esbuild/android-arm64@0.25.0': + optional: true + + '@esbuild/android-arm@0.25.0': + optional: true + + '@esbuild/android-x64@0.25.0': + optional: true + + '@esbuild/darwin-arm64@0.25.0': + optional: true + + '@esbuild/darwin-x64@0.25.0': + optional: true + + '@esbuild/freebsd-arm64@0.25.0': + optional: true + + '@esbuild/freebsd-x64@0.25.0': + optional: true + + '@esbuild/linux-arm64@0.25.0': + optional: true + + '@esbuild/linux-arm@0.25.0': + optional: true + + '@esbuild/linux-ia32@0.25.0': + optional: true + + '@esbuild/linux-loong64@0.25.0': + optional: true + + '@esbuild/linux-mips64el@0.25.0': + optional: true + + '@esbuild/linux-ppc64@0.25.0': + optional: true + + '@esbuild/linux-riscv64@0.25.0': + optional: true + + '@esbuild/linux-s390x@0.25.0': + optional: true + + '@esbuild/linux-x64@0.25.0': + optional: true + + '@esbuild/netbsd-arm64@0.25.0': + optional: true + + '@esbuild/netbsd-x64@0.25.0': + optional: true + + '@esbuild/openbsd-arm64@0.25.0': + optional: true + + '@esbuild/openbsd-x64@0.25.0': + optional: true + + '@esbuild/sunos-x64@0.25.0': + optional: true + + '@esbuild/win32-arm64@0.25.0': + optional: true + + '@esbuild/win32-ia32@0.25.0': + optional: true + + '@esbuild/win32-x64@0.25.0': + optional: true + + '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1)': + dependencies: + eslint: 9.20.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.19.2': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.0 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/core@0.11.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.12.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.2.0': + 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.20.0': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.2.7': + dependencies: + '@eslint/core': 0.12.0 + levn: 0.4.1 + + '@fastify/accept-negotiator@2.0.1': {} + + '@fastify/ajv-compiler@4.0.2': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + + '@fastify/busboy@3.1.1': {} + + '@fastify/cookie@11.0.2': + dependencies: + cookie: 1.0.2 + fastify-plugin: 5.0.1 + + '@fastify/cors@10.0.2': + dependencies: + fastify-plugin: 5.0.1 + mnemonist: 0.39.8 + + '@fastify/deepmerge@2.0.2': {} + + '@fastify/error@4.0.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.2': + dependencies: + fast-json-stringify: 6.0.1 + + '@fastify/forwarded@3.0.0': {} + + '@fastify/jwt@9.0.4': + dependencies: + '@fastify/error': 4.0.0 + '@lukeed/ms': 2.0.2 + fast-jwt: 5.0.5 + fastify-plugin: 5.0.1 + steed: 1.1.3 + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/multipart@9.0.3': + dependencies: + '@fastify/busboy': 3.1.1 + '@fastify/deepmerge': 2.0.2 + '@fastify/error': 4.0.0 + fastify-plugin: 5.0.1 + secure-json-parse: 3.0.2 + + '@fastify/proxy-addr@5.0.0': + dependencies: + '@fastify/forwarded': 3.0.0 + ipaddr.js: 2.2.0 + + '@fastify/send@3.3.1': + dependencies: + '@lukeed/ms': 2.0.2 + escape-html: 1.0.3 + fast-decode-uri-component: 1.0.1 + http-errors: 2.0.0 + mime: 3.0.0 + + '@fastify/static@8.1.1': + dependencies: + '@fastify/accept-negotiator': 2.0.1 + '@fastify/send': 3.3.1 + content-disposition: 0.5.4 + fastify-plugin: 5.0.1 + fastq: 1.19.0 + glob: 11.0.1 + + '@fastify/swagger-ui@5.2.2': + dependencies: + '@fastify/static': 8.1.1 + fastify-plugin: 5.0.1 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.7.0 + + '@fastify/swagger@9.4.2': + dependencies: + fastify-plugin: 5.0.1 + json-schema-resolver: 3.0.0 + openapi-types: 12.1.3 + rfdc: 1.4.1 + yaml: 2.7.0 + transitivePeerDependencies: + - supports-color + + '@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': {} + + '@img/sharp-darwin-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.0.4 + optional: true + + '@img/sharp-darwin-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.0.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.0.5': + optional: true + + '@img/sharp-libvips-linux-s390x@1.0.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.0.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.0.4': + optional: true + + '@img/sharp-linux-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.0.4 + optional: true + + '@img/sharp-linux-arm@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.0.5 + optional: true + + '@img/sharp-linux-s390x@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.0.4 + optional: true + + '@img/sharp-linux-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.33.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + optional: true + + '@img/sharp-wasm32@0.33.5': + dependencies: + '@emnapi/runtime': 1.3.1 + optional: true + + '@img/sharp-win32-ia32@0.33.5': + optional: true + + '@img/sharp-win32-x64@0.33.5': + optional: true + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@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 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.0 + + '@lukeed/ms@2.0.2': {} + + '@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.0 + + '@prisma/client@6.4.1(prisma@6.4.1(typescript@5.7.3))(typescript@5.7.3)': + optionalDependencies: + prisma: 6.4.1(typescript@5.7.3) + typescript: 5.7.3 + + '@prisma/debug@6.4.1': {} + + '@prisma/engines-version@6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d': {} + + '@prisma/engines@6.4.1': + dependencies: + '@prisma/debug': 6.4.1 + '@prisma/engines-version': 6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d + '@prisma/fetch-engine': 6.4.1 + '@prisma/get-platform': 6.4.1 + + '@prisma/fetch-engine@6.4.1': + dependencies: + '@prisma/debug': 6.4.1 + '@prisma/engines-version': 6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d + '@prisma/get-platform': 6.4.1 + + '@prisma/get-platform@6.4.1': + dependencies: + '@prisma/debug': 6.4.1 + + '@rtsao/scc@1.1.0': {} + + '@scalar/fastify-api-reference@1.25.122': + dependencies: + '@scalar/types': 0.0.34 + fastify-plugin: 4.5.1 + + '@scalar/openapi-types@0.1.8': {} + + '@scalar/types@0.0.34': + dependencies: + '@scalar/openapi-types': 0.1.8 + '@unhead/schema': 1.11.19 + + '@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.4.2)': + dependencies: + '@babel/generator': 7.26.9 + '@babel/parser': 7.26.9 + '@babel/traverse': 7.26.9 + '@babel/types': 7.26.9 + javascript-natural-sort: 0.7.1 + lodash: 4.17.21 + prettier: 3.4.2 + transitivePeerDependencies: + - supports-color + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/bcryptjs@2.4.6': {} + + '@types/estree@1.0.6': {} + + '@types/json-schema@7.0.15': {} + + '@types/json5@0.0.29': {} + + '@types/node@22.13.4': + dependencies: + undici-types: 6.20.0 + + '@types/nodemailer@6.4.17': + dependencies: + '@types/node': 22.13.4 + + '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/scope-manager': 8.24.1 + '@typescript-eslint/type-utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.24.1 + eslint: 9.20.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 2.0.1(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.24.1 + '@typescript-eslint/types': 8.24.1 + '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) + '@typescript-eslint/visitor-keys': 8.24.1 + debug: 4.4.0 + eslint: 9.20.1 + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.24.1': + dependencies: + '@typescript-eslint/types': 8.24.1 + '@typescript-eslint/visitor-keys': 8.24.1 + + '@typescript-eslint/type-utils@8.24.1(eslint@9.20.1)(typescript@5.7.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + debug: 4.4.0 + eslint: 9.20.1 + ts-api-utils: 2.0.1(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.24.1': {} + + '@typescript-eslint/typescript-estree@8.24.1(typescript@5.7.3)': + dependencies: + '@typescript-eslint/types': 8.24.1 + '@typescript-eslint/visitor-keys': 8.24.1 + 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.0.1(typescript@5.7.3) + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.24.1(eslint@9.20.1)(typescript@5.7.3)': + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) + '@typescript-eslint/scope-manager': 8.24.1 + '@typescript-eslint/types': 8.24.1 + '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) + eslint: 9.20.1 + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.24.1': + dependencies: + '@typescript-eslint/types': 8.24.1 + eslint-visitor-keys: 4.2.0 + + '@unhead/schema@1.11.19': + dependencies: + hookable: 5.5.3 + zhead: 2.2.4 + + '@zxing/text-encoding@0.9.0': + optional: true + + abstract-logging@2.0.1: {} + + acorn-jsx@5.3.2(acorn@8.14.0): + dependencies: + acorn: 8.14.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.14.0 + + acorn@8.14.0: {} + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + 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 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.0.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.1.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.1: {} + + arg@4.1.3: {} + + argparse@2.0.1: {} + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.3 + is-array-buffer: 3.0.5 + + array-includes@3.1.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + get-intrinsic: 1.2.7 + is-string: 1.1.1 + + array.prototype.findlastindex@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + is-array-buffer: 3.0.5 + + asn1.js@5.4.1: + dependencies: + bn.js: 4.12.1 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + async-function@1.0.0: {} + + async@3.2.6: {} + + atomic-sleep@1.0.0: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + avvio@9.1.0: + dependencies: + '@fastify/error': 4.0.0 + fastq: 1.19.0 + + balanced-match@1.0.2: {} + + bcryptjs@2.4.3: {} + + block-stream2@2.1.0: + dependencies: + readable-stream: 3.6.2 + + bn.js@4.12.1: {} + + 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 + + browser-or-node@2.1.1: {} + + buffer-crc32@1.0.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.2.7 + set-function-length: 1.2.2 + + call-bound@1.0.3: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.2.7 + + callsites@3.1.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.2 + + color@4.2.3: + dependencies: + color-convert: 2.0.1 + color-string: 1.9.1 + + concat-map@0.0.1: {} + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + cookie@1.0.2: {} + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + debug@3.2.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0: + dependencies: + ms: 2.1.3 + + decode-uri-component@0.2.2: {} + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + depd@2.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.0.3: {} + + diff@4.0.2: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + es-abstract@1.23.9: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.18 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-register@3.6.0(esbuild@0.25.0): + dependencies: + debug: 4.4.0 + esbuild: 0.25.0 + transitivePeerDependencies: + - supports-color + + esbuild@0.25.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.0 + '@esbuild/android-arm': 0.25.0 + '@esbuild/android-arm64': 0.25.0 + '@esbuild/android-x64': 0.25.0 + '@esbuild/darwin-arm64': 0.25.0 + '@esbuild/darwin-x64': 0.25.0 + '@esbuild/freebsd-arm64': 0.25.0 + '@esbuild/freebsd-x64': 0.25.0 + '@esbuild/linux-arm': 0.25.0 + '@esbuild/linux-arm64': 0.25.0 + '@esbuild/linux-ia32': 0.25.0 + '@esbuild/linux-loong64': 0.25.0 + '@esbuild/linux-mips64el': 0.25.0 + '@esbuild/linux-ppc64': 0.25.0 + '@esbuild/linux-riscv64': 0.25.0 + '@esbuild/linux-s390x': 0.25.0 + '@esbuild/linux-x64': 0.25.0 + '@esbuild/netbsd-arm64': 0.25.0 + '@esbuild/netbsd-x64': 0.25.0 + '@esbuild/openbsd-arm64': 0.25.0 + '@esbuild/openbsd-x64': 0.25.0 + '@esbuild/sunos-x64': 0.25.0 + '@esbuild/win32-arm64': 0.25.0 + '@esbuild/win32-ia32': 0.25.0 + '@esbuild/win32-x64': 0.25.0 + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.0.1(eslint@9.20.1): + dependencies: + eslint: 9.20.1 + + eslint-import-resolver-node@0.3.9: + dependencies: + debug: 3.2.7 + is-core-module: 2.16.1 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1): + dependencies: + debug: 3.2.7 + optionalDependencies: + '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + eslint: 9.20.1 + eslint-import-resolver-node: 0.3.9 + transitivePeerDependencies: + - supports-color + + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1): + dependencies: + '@rtsao/scc': 1.1.0 + array-includes: 3.1.8 + array.prototype.findlastindex: 1.2.5 + array.prototype.flat: 1.3.3 + array.prototype.flatmap: 1.3.3 + debug: 3.2.7 + doctrine: 2.1.0 + eslint: 9.20.1 + eslint-import-resolver-node: 0.3.9 + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1) + hasown: 2.0.2 + is-core-module: 2.16.1 + is-glob: 4.0.3 + minimatch: 3.1.2 + object.fromentries: 2.0.8 + object.groupby: 1.0.3 + object.values: 1.2.1 + semver: 6.3.1 + string.prototype.trimend: 1.0.9 + tsconfig-paths: 3.15.0 + optionalDependencies: + '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + transitivePeerDependencies: + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - supports-color + + 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.20.1: + dependencies: + '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.19.2 + '@eslint/core': 0.11.0 + '@eslint/eslintrc': 3.2.0 + '@eslint/js': 9.20.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.6 + chalk: 4.1.2 + 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 + 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 + + espree@10.3.0: + dependencies: + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eventemitter3@5.0.1: {} + + fast-decode-uri-component@1.0.1: {} + + 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-json-stringify@6.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.0.6 + json-schema-ref-resolver: 2.0.1 + rfdc: 1.4.1 + + fast-jwt@5.0.5: + dependencies: + '@lukeed/ms': 2.0.2 + asn1.js: 5.4.1 + ecdsa-sig-formatter: 1.0.11 + mnemonist: 0.39.8 + + fast-levenshtein@2.0.6: {} + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-redact@3.5.0: {} + + fast-uri@3.0.6: {} + + fast-xml-parser@4.5.3: + dependencies: + strnum: 1.1.1 + + fastfall@1.5.1: + dependencies: + reusify: 1.0.4 + + fastify-plugin@4.5.1: {} + + fastify-plugin@5.0.1: {} + + fastify-type-provider-zod@4.0.2(fastify@5.2.1)(zod@3.24.2): + dependencies: + '@fastify/error': 4.0.0 + fastify: 5.2.1 + zod: 3.24.2 + zod-to-json-schema: 3.24.2(zod@3.24.2) + + fastify@5.2.1: + dependencies: + '@fastify/ajv-compiler': 4.0.2 + '@fastify/error': 4.0.0 + '@fastify/fast-json-stringify-compiler': 5.0.2 + '@fastify/proxy-addr': 5.0.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.0.1 + find-my-way: 9.2.0 + light-my-request: 6.6.0 + pino: 9.6.0 + process-warning: 4.0.1 + rfdc: 1.4.1 + secure-json-parse: 3.0.2 + semver: 7.7.1 + toad-cache: 3.7.0 + + fastparallel@2.4.1: + dependencies: + reusify: 1.0.4 + xtend: 4.0.2 + + fastq@1.19.0: + dependencies: + reusify: 1.0.4 + + fastseries@1.7.2: + dependencies: + reusify: 1.0.4 + xtend: 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 + + filter-obj@1.1.0: {} + + find-my-way@9.2.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 4.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: {} + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + foreground-child@3.3.0: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + get-intrinsic@1.2.7: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + + get-tsconfig@4.10.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@11.0.1: + dependencies: + foreground-child: 3.3.0 + jackspeak: 4.0.3 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 2.0.0 + + globals@11.12.0: {} + + globals@14.0.0: {} + + globals@15.15.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + gopd@1.2.0: {} + + graphemer@1.4.0: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + ipaddr.js@2.2.0: {} + + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + + is-arrayish@0.3.2: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.3 + + is-fullwidth-code-point@3.0.0: {} + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.3 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.3 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.3 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.3 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.3 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.18 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.3 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + jackspeak@4.0.3: + dependencies: + '@isaacs/cliui': 8.0.2 + + javascript-natural-sort@0.7.1: {} + + 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-ref-resolver@2.0.1: + dependencies: + dequal: 2.0.3 + + json-schema-resolver@3.0.0: + dependencies: + debug: 4.4.0 + fast-uri: 3.0.6 + rfdc: 1.4.1 + transitivePeerDependencies: + - supports-color + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + light-my-request@6.6.0: + dependencies: + cookie: 1.0.2 + process-warning: 4.0.1 + set-cookie-parser: 2.7.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + lru-cache@11.0.2: {} + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@3.0.0: {} + + minimalistic-assert@1.0.1: {} + + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.11 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.1 + + minimist@1.2.8: {} + + minio@8.0.4: + dependencies: + async: 3.2.6 + block-stream2: 2.1.0 + browser-or-node: 2.1.1 + buffer-crc32: 1.0.0 + eventemitter3: 5.0.1 + fast-xml-parser: 4.5.3 + ipaddr.js: 2.2.0 + lodash: 4.17.21 + mime-types: 2.1.35 + query-string: 7.1.3 + stream-json: 1.9.1 + through2: 4.0.2 + web-encoding: 1.1.5 + xml2js: 0.6.2 + + minipass@7.1.2: {} + + mnemonist@0.39.8: + dependencies: + obliterator: 2.0.5 + + ms@2.1.3: {} + + natural-compare@1.4.0: {} + + nodemailer@6.10.0: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + + object.groupby@1.0.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + obliterator@2.0.5: {} + + on-exit-leak-free@2.1.2: {} + + openapi-types@12.1.3: {} + + 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 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.2.7 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.2 + minipass: 7.1.2 + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.0.0: {} + + pino@9.6.0: + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.5.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.0.0 + process-warning: 4.0.1 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 3.1.0 + + possible-typed-array-names@1.1.0: {} + + prelude-ls@1.2.1: {} + + prettier@3.4.2: {} + + prisma@6.4.1(typescript@5.7.3): + dependencies: + '@prisma/engines': 6.4.1 + esbuild: 0.25.0 + esbuild-register: 3.6.0(esbuild@0.25.0) + optionalDependencies: + fsevents: 2.3.3 + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + process-warning@4.0.1: {} + + punycode@2.3.1: {} + + query-string@7.1.3: + dependencies: + decode-uri-component: 0.2.2 + filter-obj: 1.1.0 + split-on-first: 1.1.0 + strict-uri-encode: 2.0.0 + + queue-microtask@1.2.3: {} + + quick-format-unescaped@4.0.4: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + real-require@0.2.0: {} + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.2.7 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-pkg-maps@1.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 + + ret@0.5.0: {} + + reusify@1.0.4: {} + + rfdc@1.4.1: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + get-intrinsic: 1.2.7 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-buffer@5.2.1: {} + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safe-regex2@4.0.1: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sax@1.4.1: {} + + secure-json-parse@3.0.2: {} + + semver@6.3.1: {} + + semver@7.7.1: {} + + set-cookie-parser@2.7.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.7 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + setprototypeof@1.2.0: {} + + sharp@0.33.5: + dependencies: + color: 4.2.3 + detect-libc: 2.0.3 + semver: 7.7.1 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.33.5 + '@img/sharp-darwin-x64': 0.33.5 + '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-linux-arm': 0.33.5 + '@img/sharp-linux-arm64': 0.33.5 + '@img/sharp-linux-s390x': 0.33.5 + '@img/sharp-linux-x64': 0.33.5 + '@img/sharp-linuxmusl-arm64': 0.33.5 + '@img/sharp-linuxmusl-x64': 0.33.5 + '@img/sharp-wasm32': 0.33.5 + '@img/sharp-win32-ia32': 0.33.5 + '@img/sharp-win32-x64': 0.33.5 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.7 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + simple-swizzle@0.2.2: + dependencies: + is-arrayish: 0.3.2 + + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + + split-on-first@1.1.0: {} + + split2@4.2.0: {} + + statuses@2.0.1: {} + + steed@1.1.3: + dependencies: + fastfall: 1.5.1 + fastparallel: 2.4.1 + fastq: 1.19.0 + fastseries: 1.7.2 + reusify: 1.0.4 + + stream-chain@2.2.5: {} + + stream-json@1.9.1: + dependencies: + stream-chain: 2.2.5 + + strict-uri-encode@2.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.23.9 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.3 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.0: + dependencies: + ansi-regex: 6.1.0 + + strip-bom@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strnum@1.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + thread-stream@3.1.0: + dependencies: + real-require: 0.2.0 + + through2@4.0.2: + dependencies: + readable-stream: 3.6.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toad-cache@3.7.0: {} + + toidentifier@1.0.1: {} + + ts-api-utils@2.0.1(typescript@5.7.3): + dependencies: + typescript: 5.7.3 + + ts-node@10.9.2(@types/node@22.13.4)(typescript@5.7.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 22.13.4 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.7.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: + optional: true + + tsx@4.19.3: + dependencies: + esbuild: 0.25.0 + get-tsconfig: 4.10.0 + optionalDependencies: + fsevents: 2.3.3 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.3 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.24.1(eslint@9.20.1)(typescript@5.7.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + eslint: 9.20.1 + typescript: 5.7.3 + transitivePeerDependencies: + - supports-color + + typescript@5.7.3: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.3 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@6.20.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + util@0.12.5: + dependencies: + inherits: 2.0.4 + is-arguments: 1.2.0 + is-generator-function: 1.1.0 + is-typed-array: 1.1.15 + which-typed-array: 1.1.18 + + v8-compile-cache-lib@3.0.1: {} + + web-encoding@1.1.5: + dependencies: + util: 0.12.5 + optionalDependencies: + '@zxing/text-encoding': 0.9.0 + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.3 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.18 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.18: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.3 + for-each: 0.3.5 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + + xml2js@0.6.2: + dependencies: + sax: 1.4.1 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + xtend@4.0.2: {} + + yaml@2.7.0: {} + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + zhead@2.2.4: {} + + zod-to-json-schema@3.24.2(zod@3.24.2): + dependencies: + zod: 3.24.2 + + zod@3.24.2: {} diff --git a/apps/server/prisma/dev.db b/apps/server/prisma/dev.db new file mode 100644 index 0000000..e69de29 diff --git a/apps/server/prisma/migrations/20250220194959_init/migration.sql b/apps/server/prisma/migrations/20250220194959_init/migration.sql new file mode 100644 index 0000000..ef34bd7 --- /dev/null +++ b/apps/server/prisma/migrations/20250220194959_init/migration.sql @@ -0,0 +1,178 @@ +-- CreateTable +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "firstName" TEXT NOT NULL, + "lastName" TEXT NOT NULL, + "username" TEXT NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "image" TEXT, + "isAdmin" BOOLEAN NOT NULL DEFAULT false, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "files" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "extension" TEXT NOT NULL, + "size" BIGINT NOT NULL, + "objectName" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "files_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "shares" ( + "id" TEXT NOT NULL, + "name" TEXT, + "views" INTEGER NOT NULL DEFAULT 0, + "expiration" TIMESTAMP(3), + "description" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "creatorId" TEXT, + "securityId" TEXT NOT NULL, + + CONSTRAINT "shares_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "share_security" ( + "id" TEXT NOT NULL, + "password" TEXT, + "maxViews" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "share_security_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "share_recipients" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "shareId" TEXT NOT NULL, + + CONSTRAINT "share_recipients_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "app_configs" ( + "id" TEXT NOT NULL, + "key" TEXT NOT NULL, + "value" TEXT NOT NULL, + "type" TEXT NOT NULL, + "group" TEXT NOT NULL, + "isSystem" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "app_configs_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "login_attempts" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "attempts" INTEGER NOT NULL DEFAULT 1, + "lastAttempt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "login_attempts_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "password_resets" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "used" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "password_resets_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "share_aliases" ( + "id" TEXT NOT NULL, + "alias" TEXT NOT NULL, + "shareId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "share_aliases_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_ShareFiles" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_ShareFiles_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE UNIQUE INDEX "users_username_key" ON "users"("username"); + +-- CreateIndex +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "shares_securityId_key" ON "shares"("securityId"); + +-- CreateIndex +CREATE UNIQUE INDEX "app_configs_key_key" ON "app_configs"("key"); + +-- CreateIndex +CREATE UNIQUE INDEX "login_attempts_userId_key" ON "login_attempts"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "password_resets_token_key" ON "password_resets"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "share_aliases_alias_key" ON "share_aliases"("alias"); + +-- CreateIndex +CREATE UNIQUE INDEX "share_aliases_shareId_key" ON "share_aliases"("shareId"); + +-- CreateIndex +CREATE INDEX "_ShareFiles_B_index" ON "_ShareFiles"("B"); + +-- AddForeignKey +ALTER TABLE "files" ADD CONSTRAINT "files_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "shares" ADD CONSTRAINT "shares_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "shares" ADD CONSTRAINT "shares_securityId_fkey" FOREIGN KEY ("securityId") REFERENCES "share_security"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "share_recipients" ADD CONSTRAINT "share_recipients_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "shares"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "login_attempts" ADD CONSTRAINT "login_attempts_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "password_resets" ADD CONSTRAINT "password_resets_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "share_aliases" ADD CONSTRAINT "share_aliases_shareId_fkey" FOREIGN KEY ("shareId") REFERENCES "shares"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ShareFiles" ADD CONSTRAINT "_ShareFiles_A_fkey" FOREIGN KEY ("A") REFERENCES "files"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_ShareFiles" ADD CONSTRAINT "_ShareFiles_B_fkey" FOREIGN KEY ("B") REFERENCES "shares"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/server/prisma/migrations/migration_lock.toml b/apps/server/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..648c57f --- /dev/null +++ b/apps/server/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" \ No newline at end of file diff --git a/apps/server/prisma/schema.prisma b/apps/server/prisma/schema.prisma new file mode 100644 index 0000000..4c626c8 --- /dev/null +++ b/apps/server/prisma/schema.prisma @@ -0,0 +1,145 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(cuid()) + firstName String + lastName String + username String @unique + email String @unique + password String + image String? + isAdmin Boolean @default(false) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + files File[] + shares Share[] + + loginAttempts LoginAttempt? + + passwordResets PasswordReset[] + + @@map("users") +} + +model File { + id String @id @default(cuid()) + name String + description String? + extension String + size BigInt + objectName String + + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + shares Share[] @relation("ShareFiles") + + @@map("files") +} + +model Share { + id String @id @default(cuid()) + name String? + views Int @default(0) + expiration DateTime? + description String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + creatorId String? + creator User? @relation(fields: [creatorId], references: [id], onDelete: SetNull) + + securityId String @unique + security ShareSecurity @relation(fields: [securityId], references: [id]) + + files File[] @relation("ShareFiles") + recipients ShareRecipient[] + + alias ShareAlias? + + @@map("shares") +} + +model ShareSecurity { + id String @id @default(cuid()) + password String? + maxViews Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + share Share? + + @@map("share_security") +} + +model ShareRecipient { + id String @id @default(cuid()) + email String + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + shareId String + share Share @relation(fields: [shareId], references: [id], onDelete: Cascade) + + @@map("share_recipients") +} + +model AppConfig { + id String @id @default(cuid()) + key String @unique + value String + type String + group String + isSystem Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("app_configs") +} + +model LoginAttempt { + id String @id @default(cuid()) + userId String @unique + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + attempts Int @default(1) + lastAttempt DateTime @default(now()) + + @@map("login_attempts") +} + +model PasswordReset { + id String @id @default(cuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + token String @unique + expiresAt DateTime + used Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("password_resets") +} + +model ShareAlias { + id String @id @default(cuid()) + alias String @unique + shareId String @unique + share Share @relation(fields: [shareId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("share_aliases") +} diff --git a/apps/server/prisma/seed.ts b/apps/server/prisma/seed.ts new file mode 100644 index 0000000..4596d21 --- /dev/null +++ b/apps/server/prisma/seed.ts @@ -0,0 +1,178 @@ +import { prisma } from "../src/shared/prisma"; +import bcrypt from "bcryptjs"; +import crypto from "node:crypto"; + +const defaultConfigs = [ + // General Configurations + { + key: "appName", + value: "Palmr. ", + type: "string", + group: "general", + }, + { + key: "showHomePage", + value: "true", + type: "boolean", + group: "general", + }, + { + key: "appDescription", + value: "Secure and simple file sharing - Your personal cloud", + type: "string", + group: "general", + }, + { + key: "appLogo", + value: "https://i.ibb.co/xSjDkgVT/image-1.png", + type: "string", + group: "general", + }, + // Storage Configurations + { + key: "maxFileSize", + value: "1073741824", // 1GB in bytes + type: "bigint", + group: "storage", + }, + { + key: "maxTotalStoragePerUser", + value: "10737418240", // 10GB in bytes + type: "bigint", + group: "storage", + }, + // Security Configurations + { + key: "jwtSecret", + value: crypto.randomBytes(64).toString("hex"), + type: "string", + group: "security", + }, + { + key: "maxLoginAttempts", + value: "5", + type: "number", + group: "security", + }, + { + key: "loginBlockDuration", + value: "600", // 10 minutes in seconds + type: "number", + group: "security", + }, + { + key: "passwordMinLength", + value: "8", + type: "number", + group: "security", + }, + // Email Configurations + { + key: "smtpEnabled", + value: "false", + type: "boolean", + group: "email", + }, + { + key: "smtpHost", + value: "smtp.gmail.com", + type: "string", + group: "email", + }, + { + key: "smtpPort", + value: "587", + type: "number", + group: "email", + }, + { + key: "smtpUser", + value: "your-email@gmail.com", + type: "string", + group: "email", + }, + { + key: "smtpPass", + value: "your-app-specific-password", + type: "string", + group: "email", + }, + { + key: "smtpFromName", + value: "Palmr", + type: "string", + group: "email", + }, + { + key: "smtpFromEmail", + value: "noreply@palmr.app", + type: "string", + group: "email", + }, + { + key: "passwordResetTokenExpiration", + value: "3600", + type: "number", + group: "security", + }, +]; + +async function main() { + const existingUsers = await prisma.user.count(); + + if (existingUsers === 0) { + const adminEmail = "admin@example.com"; + const adminPassword = "admin123"; + const hashedPassword = await bcrypt.hash(adminPassword, 10); + + const adminUser = await prisma.user.upsert({ + where: { email: adminEmail }, + update: {}, + create: { + firstName: "Admin", + lastName: "User", + username: "admin", + email: adminEmail, + password: hashedPassword, + isAdmin: true, + isActive: true, + }, + }); + + console.log("Admin user seeded:", adminUser); + } else { + console.log("Users already exist, skipping admin user creation..."); + } + + console.log("Seeding app configurations..."); + + for (const config of defaultConfigs) { + if (config.key === "jwtSecret") { + const existingSecret = await prisma.appConfig.findUnique({ + where: { key: "jwtSecret" }, + }); + + if (existingSecret) { + console.log("JWT secret already exists, skipping..."); + continue; + } + } + + await prisma.appConfig.upsert({ + where: { key: config.key }, + update: config, + create: config, + }); + } + + console.log("App configurations seeded successfully!"); +} + +main() + .catch((error) => { + console.error("Error during seed:", error); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/apps/server/scripts/start.sh b/apps/server/scripts/start.sh new file mode 100644 index 0000000..684eada --- /dev/null +++ b/apps/server/scripts/start.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +echo "Waiting for PostgreSQL..." +while ! nc -z postgres 5432; do + sleep 1 +done +echo "PostgreSQL is up!" + +echo "Running migrations and seed..." +npx prisma generate --schema=./prisma/schema.prisma +npx prisma migrate deploy +pnpm db:seed + +echo "Starting application..." +pnpm start \ No newline at end of file diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts new file mode 100644 index 0000000..baee61f --- /dev/null +++ b/apps/server/src/app.ts @@ -0,0 +1,69 @@ +import { registerSwagger } from "./config/swagger.config"; +import { prisma } from "./shared/prisma"; +import fastifyCookie from "@fastify/cookie"; +import { fastifyCors } from "@fastify/cors"; +import fastifyJwt from "@fastify/jwt"; +import { fastifySwaggerUi } from "@fastify/swagger-ui"; +import { fastify } from "fastify"; +import { validatorCompiler, serializerCompiler, ZodTypeProvider } from "fastify-type-provider-zod"; +import crypto from "node:crypto"; + +export async function buildApp() { + const jwtConfig = await prisma.appConfig.findUnique({ + where: { key: "jwtSecret" }, + }); + + const JWT_SECRET = jwtConfig?.value || crypto.randomBytes(64).toString("hex"); + + const app = fastify({ + ajv: { + customOptions: { + removeAdditional: false, + }, + }, + }).withTypeProvider(); + + app.setValidatorCompiler(validatorCompiler); + app.setSerializerCompiler(serializerCompiler); + + app.addSchema({ + $id: "dateFormat", + type: "string", + format: "date-time", + }); + + app.register(fastifyCors, { + origin: true, + credentials: true, + }); + + app.register(fastifyCookie); + app.register(fastifyJwt, { + secret: JWT_SECRET, + cookie: { + cookieName: "token", + signed: false, + }, + sign: { + expiresIn: "1d", + }, + }); + + app.decorateRequest("jwtSign", function (this: any, payload: object, options?: object) { + return this.server.jwt.sign(payload, options); + }); + + registerSwagger(app); + app.register(fastifySwaggerUi, { + routePrefix: "/swagger", + }); + + app.register(require("@scalar/fastify-api-reference"), { + routePrefix: "/docs", + configuration: { + theme: "deepSpace", + }, + }); + + return app; +} diff --git a/apps/server/src/config/minio.config.ts b/apps/server/src/config/minio.config.ts new file mode 100644 index 0000000..e6e88a9 --- /dev/null +++ b/apps/server/src/config/minio.config.ts @@ -0,0 +1,13 @@ +import { env } from "../env"; +import { Client } from "minio"; + +export const minioClient = new Client({ + endPoint: env.MINIO_ENDPOINT, + port: Number(env.MINIO_PORT), + useSSL: env.MINIO_USE_SSL === "true", + accessKey: env.MINIO_ROOT_USER, + secretKey: env.MINIO_ROOT_PASSWORD, + region: env.MINIO_REGION, +}); + +export const bucketName = env.MINIO_BUCKET_NAME; diff --git a/apps/server/src/config/swagger.config.ts b/apps/server/src/config/swagger.config.ts new file mode 100644 index 0000000..acb838b --- /dev/null +++ b/apps/server/src/config/swagger.config.ts @@ -0,0 +1,27 @@ +import { fastifySwagger } from "@fastify/swagger"; +import { jsonSchemaTransform } from "fastify-type-provider-zod"; + +export function registerSwagger(app: any) { + app.register(fastifySwagger, { + openapi: { + info: { + title: "🌴 Palmr. API", + description: "API documentation for Palmr file sharing system", + version: "1.0.0", + }, + tags: [ + { name: "Health", description: "Health check endpoints" }, + { + name: "Authentication", + description: "Authentication related endpoints", + }, + { name: "User", description: "User management endpoints" }, + { name: "File", description: "File management endpoints" }, + { name: "Share", description: "File sharing endpoints" }, + { name: "Storage", description: "Storage management endpoints" }, + { name: "App", description: "Application configuration endpoints" }, + ], + }, + transform: jsonSchemaTransform, + }); +} diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts new file mode 100644 index 0000000..c20a5a0 --- /dev/null +++ b/apps/server/src/env.ts @@ -0,0 +1,17 @@ +import { z } from "zod"; + +const envSchema = z.object({ + FRONTEND_URL: z.string().url().min(1), + MINIO_ENDPOINT: z.string().min(1), + MINIO_PORT: z.string().min(1), + MINIO_USE_SSL: z.string().min(1), + MINIO_ROOT_PASSWORD: z.string().min(1), + MINIO_ROOT_USER: z.string().min(1), + MINIO_REGION: z.string().min(1), + MINIO_BUCKET_NAME: z.string().min(1), + + PORT: z.string().min(1), + DATABASE_URL: z.string().min(1), +}); + +export const env = envSchema.parse(process.env); diff --git a/apps/server/src/modules/app/controller.ts b/apps/server/src/modules/app/controller.ts new file mode 100644 index 0000000..bbb392d --- /dev/null +++ b/apps/server/src/modules/app/controller.ts @@ -0,0 +1,87 @@ +import { LogoService } from "./logo.service"; +import { AppService } from "./service"; +import { MultipartFile } from "@fastify/multipart"; +import { FastifyReply, FastifyRequest } from "fastify"; + +export class AppController { + private appService = new AppService(); + private logoService = new LogoService(); + + async getAppInfo(request: FastifyRequest, reply: FastifyReply) { + try { + const appInfo = await this.appService.getAppInfo(); + return reply.send(appInfo); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async getAllConfigs(request: FastifyRequest, reply: FastifyReply) { + try { + const configs = await this.appService.getAllConfigs(); + return reply.send({ configs }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async updateConfig(request: FastifyRequest, reply: FastifyReply) { + try { + const { key } = request.params as { key: string }; + const { value } = request.body as { value: string }; + + const config = await this.appService.updateConfig(key, value); + return reply.send({ config }); + } catch (error: any) { + if (error.message === "Configuration not found") { + return reply.status(404).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async bulkUpdateConfigs(request: FastifyRequest, reply: FastifyReply) { + try { + const updates = request.body as Array<{ key: string; value: string }>; + const configs = await this.appService.bulkUpdateConfigs(updates); + return reply.send({ configs }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async uploadLogo(request: FastifyRequest, reply: FastifyReply) { + try { + const file = (request.body as any).file as MultipartFile; + if (!file) { + return reply.status(400).send({ error: "No file uploaded" }); + } + + const buffer = await file.toBuffer(); + const imageUrl = await this.logoService.uploadLogo(buffer); + + // Get current logo URL to delete it + const currentLogo = await this.appService.updateConfig("appLogo", imageUrl); + if (currentLogo && currentLogo.value !== imageUrl) { + await this.logoService.deleteLogo(currentLogo.value); + } + + return reply.send({ logo: imageUrl }); + } catch (error: any) { + console.error("Upload error:", error); + return reply.status(400).send({ error: error.message }); + } + } + + async removeLogo(request: FastifyRequest, reply: FastifyReply) { + try { + const currentLogo = await this.appService.updateConfig("appLogo", ""); + if (currentLogo && currentLogo.value) { + await this.logoService.deleteLogo(currentLogo.value); + } + return reply.send({ message: "Logo removed successfully" }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } +} diff --git a/apps/server/src/modules/app/dto.ts b/apps/server/src/modules/app/dto.ts new file mode 100644 index 0000000..ba66cf0 --- /dev/null +++ b/apps/server/src/modules/app/dto.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +export const UpdateConfigSchema = z.object({ + key: z.string().describe("The config key"), + value: z.string().describe("The config value"), +}); + +export const BulkUpdateConfigSchema = z.array( + z.object({ + key: z.string().describe("The config key"), + value: z.string().describe("The config value"), + }) +); + +export const ConfigResponseSchema = z.object({ + key: z.string().describe("The config key"), + value: z.string().describe("The config value"), + type: z.string().describe("The config type"), + group: z.string().describe("The config group"), + updatedAt: z.date().describe("The config update date"), +}); diff --git a/apps/server/src/modules/app/logo.service.ts b/apps/server/src/modules/app/logo.service.ts new file mode 100644 index 0000000..bedb47e --- /dev/null +++ b/apps/server/src/modules/app/logo.service.ts @@ -0,0 +1,64 @@ +import { minioClient } from "../../config/minio.config"; +import { randomUUID } from "crypto"; +import sharp from "sharp"; + +export class LogoService { + private readonly bucketName = "logos"; + + constructor() { + this.initializeBucket(); + } + + private async initializeBucket() { + try { + const bucketExists = await minioClient.bucketExists(this.bucketName); + if (!bucketExists) { + await minioClient.makeBucket(this.bucketName, "sa-east-1"); + const policy = { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { AWS: ["*"] }, + Action: ["s3:GetObject"], + Resource: [`arn:aws:s3:::${this.bucketName}/*`], + }, + ], + }; + await minioClient.setBucketPolicy(this.bucketName, JSON.stringify(policy)); + } + } catch (error) { + console.error("Error initializing logo bucket:", error); + } + } + + async uploadLogo(imageBuffer: Buffer): Promise { + try { + const metadata = await sharp(imageBuffer).metadata(); + if (!metadata.width || !metadata.height) { + throw new Error("Invalid image file"); + } + + const webpBuffer = await sharp(imageBuffer).resize(256, 256, { fit: "contain" }).webp({ quality: 80 }).toBuffer(); + + const objectName = `app/${randomUUID()}.webp`; + await minioClient.putObject(this.bucketName, objectName, webpBuffer); + + const publicUrl = `${process.env.MINIO_PUBLIC_URL}/${this.bucketName}/${objectName}`; + return publicUrl; + } catch (error) { + console.error("Error uploading logo:", error); + throw error; + } + } + + async deleteLogo(imageUrl: string) { + try { + const objectName = imageUrl.split(`/${this.bucketName}/`)[1]; + await minioClient.removeObject(this.bucketName, objectName); + } catch (error) { + console.error("Error deleting logo:", error); + throw error; + } + } +} diff --git a/apps/server/src/modules/app/routes.ts b/apps/server/src/modules/app/routes.ts new file mode 100644 index 0000000..ace8664 --- /dev/null +++ b/apps/server/src/modules/app/routes.ts @@ -0,0 +1,167 @@ +import { AppController } from "./controller"; +import { ConfigResponseSchema, BulkUpdateConfigSchema } from "./dto"; +import { FastifyInstance } from "fastify"; +import { z } from "zod"; + +export async function appRoutes(app: FastifyInstance) { + const appController = new AppController(); + + const adminPreValidation = async (request: any, reply: any) => { + try { + await request.jwtVerify(); + if (!request.user.isAdmin) { + return reply.status(403).send({ + error: "Access restricted to administrators", + }); + } + } catch (err) { + console.error(err); + return reply.status(401).send({ + error: ".", + }); + } + }; + + app.get( + "/app/info", + { + schema: { + tags: ["App"], + operationId: "getAppInfo", + summary: "Get application base information", + description: "Get application base information", + response: { + 200: z.object({ + appName: z.string().describe("The application name"), + appDescription: z.string().describe("The application description"), + appLogo: z.string().describe("The application logo"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.getAppInfo.bind(appController) + ); + + app.patch( + "/app/configs/:key", + { + preValidation: adminPreValidation, + schema: { + tags: ["App"], + operationId: "updateConfig", + summary: "Update a configuration value", + description: "Update a configuration value (admin only)", + params: z.object({ + key: z.string().describe("The config key"), + }), + body: z.object({ + value: z.string().describe("The config value"), + }), + response: { + 200: z.object({ + config: ConfigResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.updateConfig.bind(appController) + ); + + app.get( + "/app/configs", + { + preValidation: adminPreValidation, + schema: { + tags: ["App"], + operationId: "getAllConfigs", + summary: "List all configurations", + description: "List all configurations (admin only)", + response: { + 200: z.object({ + configs: z.array(ConfigResponseSchema), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.getAllConfigs.bind(appController) + ); + + app.patch( + "/app/configs", + { + preValidation: adminPreValidation, + schema: { + tags: ["App"], + operationId: "bulkUpdateConfigs", + summary: "Bulk update configuration values", + description: "Bulk update configuration values (admin only)", + body: BulkUpdateConfigSchema, + response: { + 200: z.object({ + configs: z.array(ConfigResponseSchema), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.bulkUpdateConfigs.bind(appController) + ); + + app.post( + "/app/logo", + { + preValidation: adminPreValidation, + schema: { + tags: ["App"], + operationId: "uploadLogo", + summary: "Upload app logo", + description: "Upload a new app logo (admin only)", + consumes: ["multipart/form-data"], + body: z.object({ + file: z.any().describe("Image file (JPG, PNG, GIF)"), + }), + response: { + 200: z.object({ + logo: z.string().describe("The logo URL"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.uploadLogo.bind(appController) + ); + + app.delete( + "/app/logo", + { + preValidation: adminPreValidation, + schema: { + tags: ["App"], + operationId: "removeLogo", + summary: "Remove app logo", + description: "Remove the current app logo (admin only)", + response: { + 200: z.object({ + message: z.string().describe("Success message"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + appController.removeLogo.bind(appController) + ); +} diff --git a/apps/server/src/modules/app/service.ts b/apps/server/src/modules/app/service.ts new file mode 100644 index 0000000..46b87c2 --- /dev/null +++ b/apps/server/src/modules/app/service.ts @@ -0,0 +1,78 @@ +import { prisma } from "../../shared/prisma"; +import { ConfigService } from "../config/service"; + +export class AppService { + private configService = new ConfigService(); + + async getAppInfo() { + const [appName, appDescription, appLogo] = await Promise.all([ + this.configService.getValue("appName"), + this.configService.getValue("appDescription"), + this.configService.getValue("appLogo"), + ]); + + return { + appName, + appDescription, + appLogo, + }; + } + + async getAllConfigs() { + return prisma.appConfig.findMany({ + where: { + key: { + not: "jwtSecret", + }, + }, + orderBy: { + group: "asc", + }, + }); + } + + async updateConfig(key: string, value: string) { + if (key === "jwtSecret") { + throw new Error("JWT Secret cannot be updated through this endpoint"); + } + + const config = await prisma.appConfig.findUnique({ + where: { key }, + }); + + if (!config) { + throw new Error("Configuration not found"); + } + + return prisma.appConfig.update({ + where: { key }, + data: { value }, + }); + } + + async bulkUpdateConfigs(updates: Array<{ key: string; value: string }>) { + if (updates.some((update) => update.key === "jwtSecret")) { + throw new Error("JWT Secret cannot be updated through this endpoint"); + } + + const keys = updates.map((update) => update.key); + const existingConfigs = await prisma.appConfig.findMany({ + where: { key: { in: keys } }, + }); + + if (existingConfigs.length !== keys.length) { + const existingKeys = existingConfigs.map((config) => config.key); + const missingKeys = keys.filter((key) => !existingKeys.includes(key)); + throw new Error(`Configurations not found: ${missingKeys.join(", ")}`); + } + + return prisma.$transaction( + updates.map((update) => + prisma.appConfig.update({ + where: { key: update.key }, + data: { value: update.value }, + }) + ) + ); + } +} diff --git a/apps/server/src/modules/auth/controller.ts b/apps/server/src/modules/auth/controller.ts new file mode 100644 index 0000000..50acbee --- /dev/null +++ b/apps/server/src/modules/auth/controller.ts @@ -0,0 +1,77 @@ +import { LoginSchema, RequestPasswordResetSchema, createResetPasswordSchema } from "./dto"; +import { AuthService } from "./service"; +import { FastifyReply, FastifyRequest } from "fastify"; + +export class AuthController { + private authService = new AuthService(); + + async login(request: FastifyRequest, reply: FastifyReply) { + try { + const input = LoginSchema.parse(request.body); + const user = await this.authService.login(input); + const token = await request.jwtSign({ + userId: user.id, + isAdmin: user.isAdmin, + }); + + // Set token in HTTP-only cookie + reply.setCookie("token", token, { + httpOnly: true, + path: "/", + secure: process.env.NODE_ENV === "production", // Only send over HTTPS in production + sameSite: "strict", // Protect against CSRF + }); + + // Return only user data + return reply.send({ user }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async logout(request: FastifyRequest, reply: FastifyReply) { + reply.clearCookie("token", { path: "/" }); + return reply.send({ message: "Logout successful" }); + } + + async requestPasswordReset(request: FastifyRequest, reply: FastifyReply) { + try { + const { email } = RequestPasswordResetSchema.parse(request.body); + await this.authService.requestPasswordReset(email); + return reply.send({ + message: "If an account exists with this email, a password reset link will be sent.", + }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async resetPassword(request: FastifyRequest, reply: FastifyReply) { + try { + const schema = await createResetPasswordSchema(); + const input = schema.parse(request.body); + await this.authService.resetPassword(input.token, input.password); + return reply.send({ message: "Password reset successfully" }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async getCurrentUser(request: FastifyRequest, reply: FastifyReply) { + try { + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const user = await this.authService.getUserById(userId); + if (!user) { + return reply.status(404).send({ error: "User not found" }); + } + + return reply.send({ user }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } +} diff --git a/apps/server/src/modules/auth/dto.ts b/apps/server/src/modules/auth/dto.ts new file mode 100644 index 0000000..1ad5f65 --- /dev/null +++ b/apps/server/src/modules/auth/dto.ts @@ -0,0 +1,36 @@ +import { ConfigService } from "../config/service"; +import { z } from "zod"; + +const configService = new ConfigService(); + +export const createPasswordSchema = async () => { + const minLength = Number(await configService.getValue("passwordMinLength")); + return z.string().min(minLength, `Password must be at least ${minLength} characters`).describe("User password"); +}; + +export const LoginSchema = z.object({ + email: z.string().email("Invalid email").describe("User email"), + password: z.string().min(6, "Password must be at least 6 characters").describe("User password"), +}); +export type LoginInput = z.infer; + +export const RequestPasswordResetSchema = z.object({ + email: z.string().email("Invalid email").describe("User email"), +}); + +export const BaseResetPasswordSchema = z.object({ + token: z.string().min(1, "Token is required").describe("Reset password token"), +}); + +export type BaseResetPasswordInput = z.infer; + +export const createResetPasswordSchema = async () => { + const minLength = Number(await configService.getValue("passwordMinLength")); + return BaseResetPasswordSchema.extend({ + password: z.string().min(minLength, `Password must be at least ${minLength} characters`).describe("User password"), + }); +}; + +export type ResetPasswordInput = BaseResetPasswordInput & { + password: string; +}; diff --git a/apps/server/src/modules/auth/routes.ts b/apps/server/src/modules/auth/routes.ts new file mode 100644 index 0000000..534608c --- /dev/null +++ b/apps/server/src/modules/auth/routes.ts @@ -0,0 +1,148 @@ +import { ConfigService } from "../config/service"; +import { validatePasswordMiddleware } from "../user/middleware"; +import { AuthController } from "./controller"; +import { RequestPasswordResetSchema, createResetPasswordSchema } from "./dto"; +import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { z } from "zod"; + +const configService = new ConfigService(); + +const createPasswordSchema = async () => { + const minLength = Number(await configService.getValue("passwordMinLength")); + return z.string().min(minLength, `Password must be at least ${minLength} characters`).describe("User password"); +}; + +export async function authRoutes(app: FastifyInstance) { + const authController = new AuthController(); + + const passwordSchema = await createPasswordSchema(); + const loginSchema = z.object({ + email: z.string().email("Invalid email").describe("User email"), + password: passwordSchema, + }); + + app.post( + "/auth/login", + { + schema: { + tags: ["Authentication"], + operationId: "login", + summary: "Login", + description: "Performs login and returns user data", + body: loginSchema, + response: { + 200: z.object({ + user: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + }), + 400: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + authController.login.bind(authController) + ); + + app.post( + "/auth/logout", + { + schema: { + tags: ["Authentication"], + operationId: "logout", + summary: "Logout", + description: "Performs logout by clearing the token cookie", + response: { + 200: z.object({ message: z.string().describe("Logout message") }), + }, + }, + }, + authController.logout.bind(authController) + ); + + app.post( + "/auth/forgot-password", + { + schema: { + tags: ["Authentication"], + operationId: "requestPasswordReset", + summary: "Request Password Reset", + description: "Request password reset email", + body: RequestPasswordResetSchema, + response: { + 200: z.object({ + message: z.string().describe("Reset password email sent"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + authController.requestPasswordReset.bind(authController) + ); + + app.post( + "/auth/reset-password", + { + preValidation: validatePasswordMiddleware, + schema: { + tags: ["Authentication"], + operationId: "resetPassword", + summary: "Reset Password", + description: "Reset password using token", + body: await createResetPasswordSchema(), + response: { + 200: z.object({ + message: z.string().describe("Reset password message"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + authController.resetPassword.bind(authController) + ); + + app.get( + "/auth/me", + { + schema: { + tags: ["Authentication"], + operationId: "getCurrentUser", + summary: "Get Current User", + description: "Returns the current authenticated user's information", + response: { + 200: z.object({ + user: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + }), + 401: z.object({ error: z.string().describe("Error message") }), + }, + }, + preValidation: async (request: FastifyRequest, reply: FastifyReply) => { + try { + await request.jwtVerify(); + } catch (err) { + console.error(err); + reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + }, + }, + authController.getCurrentUser.bind(authController) + ); +} diff --git a/apps/server/src/modules/auth/service.ts b/apps/server/src/modules/auth/service.ts new file mode 100644 index 0000000..9ad0027 --- /dev/null +++ b/apps/server/src/modules/auth/service.ts @@ -0,0 +1,206 @@ +import { prisma } from "../../shared/prisma"; +import { ConfigService } from "../config/service"; +import { EmailService } from "../email/service"; +import { UserResponseSchema } from "../user/dto"; +import { PrismaUserRepository } from "../user/repository"; +import { LoginInput } from "./dto"; +import bcrypt from "bcryptjs"; +import crypto from "node:crypto"; + +export class AuthService { + private userRepository = new PrismaUserRepository(); + private configService = new ConfigService(); + private emailService = new EmailService(); + + async login(data: LoginInput) { + const user = await this.userRepository.findUserByEmail(data.email); + if (!user) { + throw new Error("Invalid credentials"); + } + + if (!user.isActive) { + throw new Error("Account is inactive. Please contact an administrator."); + } + + const maxAttempts = Number(await this.configService.getValue("maxLoginAttempts")); + const blockDurationSeconds = Number(await this.configService.getValue("loginBlockDuration")); + const blockDuration = blockDurationSeconds * 1000; + + const loginAttempt = await prisma.loginAttempt.findUnique({ + where: { userId: user.id }, + }); + + if (loginAttempt) { + if (loginAttempt.attempts >= maxAttempts && Date.now() - loginAttempt.lastAttempt.getTime() < blockDuration) { + const remainingTime = Math.ceil( + (blockDuration - (Date.now() - loginAttempt.lastAttempt.getTime())) / 1000 / 60 + ); + throw new Error(`Too many failed attempts. Please try again in ${remainingTime} minutes.`); + } + + if (Date.now() - loginAttempt.lastAttempt.getTime() >= blockDuration) { + await prisma.loginAttempt.delete({ + where: { userId: user.id }, + }); + } + } + + const isValid = await bcrypt.compare(data.password, user.password); + + if (!isValid) { + await prisma.loginAttempt.upsert({ + where: { userId: user.id }, + create: { + userId: user.id, + attempts: 1, + lastAttempt: new Date(), + }, + update: { + attempts: { + increment: 1, + }, + lastAttempt: new Date(), + }, + }); + + throw new Error("Invalid credentials"); + } + + if (loginAttempt) { + await prisma.loginAttempt.delete({ + where: { userId: user.id }, + }); + } + + return UserResponseSchema.parse(user); + } + + async validateLogin(email: string, password: string) { + const user = await prisma.user.findUnique({ + where: { email }, + include: { loginAttempts: true }, + }); + + if (!user) { + throw new Error("Invalid credentials"); + } + + if (user.loginAttempts) { + const maxAttempts = Number(await this.configService.getValue("maxLoginAttempts")); + const blockDurationSeconds = Number(await this.configService.getValue("loginBlockDuration")); + const blockDuration = blockDurationSeconds * 1000; // convert seconds to milliseconds + + if ( + user.loginAttempts.attempts >= maxAttempts && + Date.now() - user.loginAttempts.lastAttempt.getTime() < blockDuration + ) { + const remainingTime = Math.ceil( + (blockDuration - (Date.now() - user.loginAttempts.lastAttempt.getTime())) / 1000 / 60 + ); + throw new Error(`Too many failed attempts. Please try again in ${remainingTime} minutes.`); + } + + if (Date.now() - user.loginAttempts.lastAttempt.getTime() >= blockDuration) { + await prisma.loginAttempt.delete({ + where: { userId: user.id }, + }); + } + } + + const isValidPassword = await bcrypt.compare(password, user.password); + + if (!isValidPassword) { + await prisma.loginAttempt.upsert({ + where: { userId: user.id }, + create: { + userId: user.id, + attempts: 1, + lastAttempt: new Date(), + }, + update: { + attempts: { + increment: 1, + }, + lastAttempt: new Date(), + }, + }); + + throw new Error("Invalid credentials"); + } + + if (user.loginAttempts) { + await prisma.loginAttempt.delete({ + where: { userId: user.id }, + }); + } + + return user; + } + + async requestPasswordReset(email: string) { + const user = await this.userRepository.findUserByEmail(email); + if (!user) { + return; + } + + const token = crypto.randomBytes(128).toString("hex"); + const expirationSeconds = Number(await this.configService.getValue("passwordResetTokenExpiration")); + + await prisma.passwordReset.create({ + data: { + userId: user.id, + token, + expiresAt: new Date(Date.now() + expirationSeconds * 1000), + }, + }); + + try { + await this.emailService.sendPasswordResetEmail(email, token); + } catch (error) { + console.error("Failed to send password reset email:", error); + throw new Error("Failed to send password reset email"); + } + } + + async resetPassword(token: string, newPassword: string) { + const resetRequest = await prisma.passwordReset.findFirst({ + where: { + token, + used: false, + expiresAt: { + gt: new Date(), + }, + }, + include: { + user: true, + }, + }); + + if (!resetRequest) { + throw new Error("Invalid or expired reset token"); + } + + const hashedPassword = await bcrypt.hash(newPassword, 10); + + await prisma.$transaction([ + prisma.user.update({ + where: { id: resetRequest.userId }, + data: { password: hashedPassword }, + }), + prisma.passwordReset.update({ + where: { id: resetRequest.id }, + data: { used: true }, + }), + ]); + } + + async getUserById(userId: string) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + }); + if (!user) { + throw new Error("User not found"); + } + return UserResponseSchema.parse(user); + } +} diff --git a/apps/server/src/modules/config/service.ts b/apps/server/src/modules/config/service.ts new file mode 100644 index 0000000..b6cd20d --- /dev/null +++ b/apps/server/src/modules/config/service.ts @@ -0,0 +1,42 @@ +import { prisma } from "../../shared/prisma"; + +export class ConfigService { + async getValue(key: string): Promise { + const config = await prisma.appConfig.findUnique({ + where: { key }, + }); + + if (!config) { + throw new Error(`Configuration ${key} not found`); + } + + return config.value; + } + + async getGroupConfigs(group: string) { + const configs = await prisma.appConfig.findMany({ + where: { group }, + }); + + return configs.reduce((acc, curr) => { + let value: any = curr.value; + + switch (curr.type) { + case "number": + value = Number(value); + break; + case "boolean": + value = value === "true"; + break; + case "json": + value = JSON.parse(value); + break; + case "bigint": + value = BigInt(value); + break; + } + + return { ...acc, [curr.key]: value }; + }, {}); + } +} diff --git a/apps/server/src/modules/email/service.ts b/apps/server/src/modules/email/service.ts new file mode 100644 index 0000000..7afe1ab --- /dev/null +++ b/apps/server/src/modules/email/service.ts @@ -0,0 +1,77 @@ +import { env } from "../../env"; +import { ConfigService } from "../config/service"; +import nodemailer from "nodemailer"; + +export class EmailService { + private configService = new ConfigService(); + + private async createTransporter() { + const smtpEnabled = await this.configService.getValue("smtpEnabled"); + if (smtpEnabled !== "true") { + return null; + } + + return nodemailer.createTransport({ + host: await this.configService.getValue("smtpHost"), + port: Number(await this.configService.getValue("smtpPort")), + secure: false, + auth: { + user: await this.configService.getValue("smtpUser"), + pass: await this.configService.getValue("smtpPass"), + }, + }); + } + + async sendPasswordResetEmail(to: string, resetToken: string) { + const transporter = await this.createTransporter(); + if (!transporter) { + throw new Error("SMTP is not enabled"); + } + + const fromName = await this.configService.getValue("smtpFromName"); + const fromEmail = await this.configService.getValue("smtpFromEmail"); + const appName = await this.configService.getValue("appName"); + + await transporter.sendMail({ + from: `"${fromName}" <${fromEmail}>`, + to, + subject: `${appName} - Password Reset Request`, + html: ` +

${appName} - Password Reset Request

+

Click the link below to reset your password:

+ + Reset Password + +

This link will expire in 1 hour.

+ `, + }); + } + + async sendShareNotification(to: string, shareLink: string, shareName?: string) { + const transporter = await this.createTransporter(); + if (!transporter) { + throw new Error("SMTP is not enabled"); + } + + const fromName = await this.configService.getValue("smtpFromName"); + const fromEmail = await this.configService.getValue("smtpFromEmail"); + const appName = await this.configService.getValue("appName"); + + const shareTitle = shareName || "Files"; + + await transporter.sendMail({ + from: `"${fromName}" <${fromEmail}>`, + to, + subject: `${appName} - ${shareTitle} shared with you`, + html: ` +

${appName} - Shared Files

+

Someone has shared "${shareTitle}" with you.

+

Click the link below to access the shared files:

+ + Access Shared Files + +

Note: This share may have an expiration date or view limit.

+ `, + }); + } +} diff --git a/apps/server/src/modules/file/controller.ts b/apps/server/src/modules/file/controller.ts new file mode 100644 index 0000000..6278c1d --- /dev/null +++ b/apps/server/src/modules/file/controller.ts @@ -0,0 +1,242 @@ +import { prisma } from "../../shared/prisma"; +import { ConfigService } from "../config/service"; +import { RegisterFileSchema, RegisterFileInput, UpdateFileSchema } from "./dto"; +import { FileService } from "./service"; +import { FastifyReply, FastifyRequest } from "fastify"; + +export class FileController { + private fileService = new FileService(); + private configService = new ConfigService(); + + async getPresignedUrl(request: FastifyRequest, reply: FastifyReply) { + try { + const { filename, extension } = request.query as { + filename?: string; + extension?: string; + }; + if (!filename || !extension) { + return reply.status(400).send({ + error: "The 'filename' and 'extension' parameters are required.", + }); + } + + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const objectName = `${userId}/${Date.now()}-${filename}.${extension}`; + const expires = 3600; + + const url = await this.fileService.getPresignedPutUrl(objectName, expires); + return reply.send({ url, objectName }); + } catch (error) { + console.error("Error in getPresignedUrl:", error); + return reply.status(500).send({ error: "Internal server error." }); + } + } + + async registerFile(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const input: RegisterFileInput = RegisterFileSchema.parse(request.body); + + const maxFileSize = BigInt(await this.configService.getValue("maxFileSize")); + if (BigInt(input.size) > maxFileSize) { + const maxSizeMB = Number(maxFileSize) / (1024 * 1024); + return reply.status(400).send({ + error: `File size exceeds the maximum allowed size of ${maxSizeMB}MB`, + }); + } + + const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser")); + + const userFiles = await prisma.file.findMany({ + where: { userId }, + select: { size: true }, + }); + + const currentStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0)); + + if (currentStorage + BigInt(input.size) > maxTotalStorage) { + const availableSpace = Number(maxTotalStorage - currentStorage) / (1024 * 1024); + return reply.status(400).send({ + error: `Insufficient storage space. You have ${availableSpace.toFixed(2)}MB available`, + }); + } + + const fileRecord = await prisma.file.create({ + data: { + name: input.name, + description: input.description, + extension: input.extension, + size: BigInt(input.size), + objectName: input.objectName, + userId, + }, + }); + + const fileResponse = { + id: fileRecord.id, + name: fileRecord.name, + description: fileRecord.description, + extension: fileRecord.extension, + size: fileRecord.size.toString(), + objectName: fileRecord.objectName, + userId: fileRecord.userId, + createdAt: fileRecord.createdAt, + updatedAt: fileRecord.updatedAt, + }; + + return reply.status(201).send({ + file: fileResponse, + message: "File registered successfully.", + }); + } catch (error: any) { + console.error("Error in registerFile:", error); + return reply.status(400).send({ error: error.message }); + } + } + + async getDownloadUrl(request: FastifyRequest, reply: FastifyReply) { + try { + const { objectName: encodedObjectName } = request.params as { + objectName: string; + }; + const objectName = decodeURIComponent(encodedObjectName); + + if (!objectName) { + return reply.status(400).send({ error: "The 'objectName' parameter is required." }); + } + + const fileRecord = await prisma.file.findFirst({ where: { objectName } }); + + if (!fileRecord) { + return reply.status(404).send({ error: "File not found." }); + } + + const expires = 3600; + const url = await this.fileService.getPresignedGetUrl(objectName, expires); + return reply.send({ url, expiresIn: expires }); + } catch (error) { + console.error("Error in getDownloadUrl:", error); + return reply.status(500).send({ error: "Internal server error." }); + } + } + + async listFiles(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const files = await prisma.file.findMany({ + where: { userId }, + }); + + const filesResponse = files.map((file) => ({ + id: file.id, + name: file.name, + description: file.description, + extension: file.extension, + size: file.size.toString(), + objectName: file.objectName, + userId: file.userId, + createdAt: file.createdAt, + updatedAt: file.updatedAt, + })); + + return reply.send({ files: filesResponse }); + } catch (error) { + console.error("Error in listFiles:", error); + return reply.status(500).send({ error: "Internal server error." }); + } + } + + async deleteFile(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const { id } = request.params as { id: string }; + if (!id) { + return reply.status(400).send({ error: "The 'id' parameter is required." }); + } + + const fileRecord = await prisma.file.findUnique({ where: { id } }); + if (!fileRecord) { + return reply.status(404).send({ error: "File not found." }); + } + + const userId = (request as any).user?.userId; + if (fileRecord.userId !== userId) { + return reply.status(403).send({ error: "Access denied." }); + } + + await this.fileService.deleteObject(fileRecord.objectName); + + await prisma.file.delete({ where: { id } }); + + return reply.send({ message: "File deleted successfully." }); + } catch (error) { + console.error("Error in deleteFile:", error); + return reply.status(500).send({ error: "Internal server error." }); + } + } + + async updateFile(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const { id } = request.params as { id: string }; + const userId = (request as any).user?.userId; + + if (!userId) { + return reply.status(401).send({ + error: "Unauthorized: a valid token is required to access this resource.", + }); + } + + const updateData = UpdateFileSchema.parse(request.body); + + const fileRecord = await prisma.file.findUnique({ where: { id } }); + + if (!fileRecord) { + return reply.status(404).send({ error: "File not found." }); + } + + if (fileRecord.userId !== userId) { + return reply.status(403).send({ error: "Access denied." }); + } + + const updatedFile = await prisma.file.update({ + where: { id }, + data: updateData, + }); + + const fileResponse = { + id: updatedFile.id, + name: updatedFile.name, + description: updatedFile.description, + extension: updatedFile.extension, + size: updatedFile.size.toString(), + objectName: updatedFile.objectName, + userId: updatedFile.userId, + createdAt: updatedFile.createdAt, + updatedAt: updatedFile.updatedAt, + }; + + return reply.send({ + file: fileResponse, + message: "File updated successfully.", + }); + } catch (error: any) { + console.error("Error in updateFile:", error); + return reply.status(400).send({ error: error.message }); + } + } +} diff --git a/apps/server/src/modules/file/dto.ts b/apps/server/src/modules/file/dto.ts new file mode 100644 index 0000000..d19835b --- /dev/null +++ b/apps/server/src/modules/file/dto.ts @@ -0,0 +1,21 @@ +import { z } from "zod"; + +export const RegisterFileSchema = z.object({ + name: z.string().min(1, "O nome do arquivo é obrigatório"), + description: z.string().optional(), + extension: z.string().min(1, "A extensão é obrigatória"), + size: z.number({ + required_error: "O tamanho é obrigatório", + invalid_type_error: "O tamanho deve ser um número", + }), + objectName: z.string().min(1, "O objectName é obrigatório"), +}); + +export type RegisterFileInput = z.infer; + +export const UpdateFileSchema = z.object({ + name: z.string().optional().describe("The file name"), + description: z.string().optional().nullable().describe("The file description"), +}); + +export type UpdateFileInput = z.infer; diff --git a/apps/server/src/modules/file/routes.ts b/apps/server/src/modules/file/routes.ts new file mode 100644 index 0000000..ac334e0 --- /dev/null +++ b/apps/server/src/modules/file/routes.ts @@ -0,0 +1,197 @@ +import { FileController } from "./controller"; +import { RegisterFileSchema, UpdateFileSchema } from "./dto"; +import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { z } from "zod"; + +export async function fileRoutes(app: FastifyInstance) { + const fileController = new FileController(); + + const preValidation = async (request: FastifyRequest, reply: FastifyReply) => { + try { + await request.jwtVerify(); + } catch (err) { + console.error(err); + reply.status(401).send({ error: "Token inválido ou ausente." }); + } + }; + + app.get( + "/files/presigned-url", + { + preValidation, + schema: { + tags: ["File"], + operationId: "getPresignedUrl", + summary: "Get Presigned URL", + description: "Generates a pre-signed URL for direct upload to MinIO", + querystring: z.object({ + filename: z.string().min(1, "The filename is required").describe("The filename of the file"), + extension: z.string().min(1, "The extension is required").describe("The extension of the file"), + }), + response: { + 200: z.object({ + url: z.string().describe("The pre-signed URL"), + objectName: z.string().describe("The object name of the file"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.getPresignedUrl.bind(fileController) + ); + + app.post( + "/files", + { + schema: { + tags: ["File"], + operationId: "registerFile", + summary: "Register File Metadata", + description: "Registers file metadata in the database", + body: RegisterFileSchema, + response: { + 201: z.object({ + file: z.object({ + id: z.string().describe("The file ID"), + name: z.string().describe("The file name"), + description: z.string().nullable().describe("The file description"), + extension: z.string().describe("The file extension"), + size: z.string().describe("The file size"), // BigInt retornado como string + objectName: z.string().describe("The object name of the file"), + userId: z.string().describe("The user ID"), + createdAt: z.date().describe("The file creation date"), + updatedAt: z.date().describe("The file last update date"), + }), + message: z.string().describe("The file registration message"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.registerFile.bind(fileController) + ); + + app.get( + "/files/:objectName/download", + { + schema: { + tags: ["File"], + operationId: "getDownloadUrl", + summary: "Get Download URL", + description: "Generates a pre-signed URL for downloading a private file", + params: z.object({ + objectName: z.string().min(1, "The objectName is required"), + }), + response: { + 200: z.object({ + url: z.string().describe("The download URL"), + expiresIn: z.number().describe("The expiration time in seconds"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.getDownloadUrl.bind(fileController) + ); + + app.get( + "/files", + { + preValidation, + schema: { + tags: ["File"], + operationId: "listFiles", + summary: "List Files", + description: "Lists user files", + response: { + 200: z.object({ + files: z.array( + z.object({ + id: z.string().describe("The file ID"), + name: z.string().describe("The file name"), + description: z.string().nullable().describe("The file description"), + extension: z.string().describe("The file extension"), + size: z.string().describe("The file size"), // BigInt retornado como string + objectName: z.string().describe("The object name of the file"), + userId: z.string().describe("The user ID"), + createdAt: z.date().describe("The file creation date"), + updatedAt: z.date().describe("The file last update date"), + }) + ), + }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.listFiles.bind(fileController) + ); + + app.delete( + "/files/:id", + { + preValidation, + schema: { + tags: ["File"], + operationId: "deleteFile", + summary: "Delete File", + description: "Deletes a user file", + params: z.object({ + id: z.string().min(1, "The file id is required").describe("The file ID"), + }), + response: { + 200: z.object({ + message: z.string().describe("The file deletion message"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.deleteFile.bind(fileController) + ); + + app.patch( + "/files/:id", + { + preValidation, + schema: { + tags: ["File"], + operationId: "updateFile", + summary: "Update File Metadata", + description: "Updates file metadata in the database", + params: z.object({ + id: z.string().min(1, "The file id is required").describe("The file ID"), + }), + body: UpdateFileSchema, + response: { + 200: z.object({ + file: z.object({ + id: z.string().describe("The file ID"), + name: z.string().describe("The file name"), + description: z.string().nullable().describe("The file description"), + extension: z.string().describe("The file extension"), + size: z.string().describe("The file size"), + objectName: z.string().describe("The object name of the file"), + userId: z.string().describe("The user ID"), + createdAt: z.date().describe("The file creation date"), + updatedAt: z.date().describe("The file last update date"), + }), + message: z.string().describe("Success message"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + fileController.updateFile.bind(fileController) + ); +} diff --git a/apps/server/src/modules/file/service.ts b/apps/server/src/modules/file/service.ts new file mode 100644 index 0000000..c58aa9b --- /dev/null +++ b/apps/server/src/modules/file/service.ts @@ -0,0 +1,51 @@ +import { minioClient, bucketName } from "../../config/minio.config"; + +export class FileService { + getPresignedPutUrl(objectName: string, expires: number): Promise { + return new Promise((resolve, reject) => { + (minioClient as any).presignedPutObject(bucketName, objectName, expires, {}, (( + err: Error | null, + presignedUrl?: string + ) => { + if (err) { + console.error("Erro no presignedPutObject:", err); + reject(err); + } else if (!presignedUrl) { + reject(new Error("URL não gerada")); + } else { + resolve(presignedUrl); + } + }) as any); + }); + } + + getPresignedGetUrl(objectName: string, expires: number): Promise { + return new Promise((resolve, reject) => { + (minioClient as any).presignedGetObject(bucketName, objectName, expires, (( + err: Error | null, + presignedUrl?: string + ) => { + if (err) { + console.error("Erro no presignedGetObject:", err); + reject(err); + } else if (!presignedUrl) { + reject(new Error("URL não gerada")); + } else { + resolve(presignedUrl); + } + }) as any); + }); + } + + deleteObject(objectName: string): Promise { + return new Promise((resolve, reject) => { + (minioClient as any).removeObject(bucketName, objectName, (err: Error | null) => { + if (err) { + console.error("Erro no removeObject:", err); + return reject(err); + } + resolve(); + }); + }); + } +} diff --git a/apps/server/src/modules/health/controller.ts b/apps/server/src/modules/health/controller.ts new file mode 100644 index 0000000..37af615 --- /dev/null +++ b/apps/server/src/modules/health/controller.ts @@ -0,0 +1,8 @@ +export class HealthController { + async check() { + return { + status: "healthy", + timestamp: new Date().toISOString(), + }; + } +} diff --git a/apps/server/src/modules/health/routes.ts b/apps/server/src/modules/health/routes.ts new file mode 100644 index 0000000..7d6b9b4 --- /dev/null +++ b/apps/server/src/modules/health/routes.ts @@ -0,0 +1,26 @@ +import { HealthController } from "./controller"; +import { FastifyInstance } from "fastify"; +import { z } from "zod"; + +export async function healthRoutes(app: FastifyInstance) { + const healthController = new HealthController(); + + app.get( + "/health", + { + schema: { + tags: ["Health"], + operationId: "checkHealth", + summary: "Check API Health", + description: "Returns the health status of the API", + response: { + 200: z.object({ + status: z.string().describe("The health status"), + timestamp: z.string().describe("The timestamp of the health check"), + }), + }, + }, + }, + async () => healthController.check() + ); +} diff --git a/apps/server/src/modules/share/controller.ts b/apps/server/src/modules/share/controller.ts new file mode 100644 index 0000000..adaf1d1 --- /dev/null +++ b/apps/server/src/modules/share/controller.ts @@ -0,0 +1,297 @@ +import { + CreateShareSchema, + UpdateShareSchema, + UpdateSharePasswordSchema, + UpdateShareFilesSchema, + UpdateShareRecipientsSchema, +} from "./dto"; +import { ShareService } from "./service"; +import { FastifyReply, FastifyRequest } from "fastify"; + +export class ShareController { + private shareService = new ShareService(); + + async createShare(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const input = CreateShareSchema.parse(request.body); + const share = await this.shareService.createShare(input, userId); + return reply.status(201).send({ share }); + } catch (error: any) { + console.error("Create Share Error:", error); + if (error.errors) { + return reply.status(400).send({ error: error.errors }); + } + return reply.status(400).send({ error: error.message || "Unknown error occurred" }); + } + } + + async listUserShares(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const shares = await this.shareService.listUserShares(userId); + return reply.send({ shares }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async getShare(request: FastifyRequest, reply: FastifyReply) { + try { + const { shareId } = request.params as { shareId: string }; + const { password } = request.query as { password?: string }; + + let userId: string | undefined; + try { + await request.jwtVerify(); + userId = (request as any).user?.userId; + } catch (err) { + console.error(err); + } + + const share = await this.shareService.getShare(shareId, password, userId); + return reply.send({ share }); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + if (error.message === "Share has reached maximum views") { + return reply.status(403).send({ error: error.message }); + } + if (error.message === "Share has expired") { + return reply.status(410).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async updateShare(request: FastifyRequest, reply: FastifyReply) { + try { + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized" }); + } + + const { id, ...updateData } = UpdateShareSchema.parse(request.body); + const share = await this.shareService.updateShare(id, updateData, userId); + return reply.send({ share }); + } catch (error: any) { + console.error("Update Share Error:", error); + return reply.status(400).send({ error: error.message }); + } + } + + async updatePassword(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const { shareId } = request.params as { shareId: string }; + const { password } = UpdateSharePasswordSchema.parse(request.body); + + const share = await this.shareService.updateSharePassword(shareId, userId, password); + return reply.send({ share }); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + if (error.message === "Unauthorized to update this share") { + return reply.status(401).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async addFiles(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const { shareId } = request.params as { shareId: string }; + const { files } = UpdateShareFilesSchema.parse(request.body); + + const share = await this.shareService.addFilesToShare(shareId, userId, files); + return reply.send({ share }); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + if (error.message === "Unauthorized to update this share") { + return reply.status(401).send({ error: error.message }); + } + if (error.message.startsWith("Files not found:")) { + return reply.status(404).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async removeFiles(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const { shareId } = request.params as { shareId: string }; + const { files } = UpdateShareFilesSchema.parse(request.body); + + const share = await this.shareService.removeFilesFromShare(shareId, userId, files); + return reply.send({ share }); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + if (error.message === "Unauthorized to update this share") { + return reply.status(401).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async deleteShare(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const { id } = request.params as { id: string }; + + const share = await this.shareService.findShareById(id); + if (!share) { + return reply.status(404).send({ error: "Share not found" }); + } + + if (share.creatorId !== userId) { + return reply.status(401).send({ error: "Unauthorized to delete this share" }); + } + + const deleted = await this.shareService.deleteShare(id); + return reply.send({ share: deleted }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async addRecipients(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const { shareId } = request.params as { shareId: string }; + const { emails } = UpdateShareRecipientsSchema.parse(request.body); + + const share = await this.shareService.addRecipients(shareId, userId, emails); + return reply.send({ share }); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + if (error.message === "Unauthorized to update this share") { + return reply.status(401).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async removeRecipients(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const { shareId } = request.params as { shareId: string }; + const { emails } = UpdateShareRecipientsSchema.parse(request.body); + + const share = await this.shareService.removeRecipients(shareId, userId, emails); + return reply.send({ share }); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + if (error.message === "Unauthorized to update this share") { + return reply.status(401).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async createOrUpdateAlias(request: FastifyRequest, reply: FastifyReply) { + try { + const { shareId } = request.params as { shareId: string }; + const { alias } = request.body as { alias: string }; + const userId = (request as any).user.userId; + + const result = await this.shareService.createOrUpdateAlias(shareId, alias, userId); + return reply.send({ alias: result }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async getShareByAlias(request: FastifyRequest, reply: FastifyReply) { + try { + const { alias } = request.params as { alias: string }; + const { password } = request.query as { password?: string }; + + const share = await this.shareService.getShareByAlias(alias, password); + return reply.send({ share }); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } + + async notifyRecipients(request: FastifyRequest, reply: FastifyReply) { + try { + await request.jwtVerify(); + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." }); + } + + const { shareId } = request.params as { shareId: string }; + const { shareLink } = request.body as { shareLink: string }; + + const result = await this.shareService.notifyRecipients(shareId, userId, shareLink); + return reply.send(result); + } catch (error: any) { + if (error.message === "Share not found") { + return reply.status(404).send({ error: error.message }); + } + if (error.message === "Unauthorized to access this share") { + return reply.status(401).send({ error: error.message }); + } + if (error.message === "SMTP is not enabled") { + return reply.status(400).send({ error: error.message }); + } + return reply.status(400).send({ error: error.message }); + } + } +} diff --git a/apps/server/src/modules/share/dto.ts b/apps/server/src/modules/share/dto.ts new file mode 100644 index 0000000..3dae483 --- /dev/null +++ b/apps/server/src/modules/share/dto.ts @@ -0,0 +1,97 @@ +import { z } from "zod"; + +export const CreateShareSchema = z.object({ + name: z.string().optional().describe("The share name"), + description: z.string().optional().describe("The share description"), + expiration: z + .string() + .datetime({ + message: "Data de expiração deve estar no formato ISO 8601 (ex: 2025-02-06T13:20:49Z)", + }) + .optional(), + files: z.array(z.string()).describe("The file IDs"), + password: z.string().optional().describe("The share password"), + maxViews: z.number().optional().nullable().describe("The maximum number of views"), + recipients: z.array(z.string().email()).optional().describe("The recipient emails"), +}); + +export const UpdateShareSchema = z.object({ + id: z.string(), + name: z.string().optional(), + description: z.string().optional(), + expiration: z.string().datetime().optional(), + password: z.string().optional(), + maxViews: z.number().optional().nullable(), + recipients: z.array(z.string().email()).optional(), +}); + +export const ShareAliasResponseSchema = z.object({ + id: z.string(), + alias: z.string(), + shareId: z.string(), + createdAt: z.string(), + updatedAt: z.string(), +}); + +export const ShareResponseSchema = z.object({ + id: z.string().describe("The share ID"), + name: z.string().nullable().describe("The share name"), + description: z.string().nullable().describe("The share description"), + expiration: z.string().nullable().describe("The share expiration date"), + views: z.number().describe("The number of views"), + createdAt: z.string().describe("The share creation date"), + updatedAt: z.string().describe("The share update date"), + creatorId: z.string().describe("The creator ID"), + security: z.object({ + maxViews: z.number().nullable().describe("The maximum number of views"), + hasPassword: z.boolean().describe("Whether the share has a password"), + }), + files: z.array( + z.object({ + id: z.string().describe("The file ID"), + name: z.string().describe("The file name"), + description: z.string().nullable().describe("The file description"), + extension: z.string().describe("The file extension"), + size: z.string().describe("The file size"), + objectName: z.string().describe("The file object name"), + userId: z.string().describe("The user ID"), + createdAt: z.string().describe("The file creation date"), + updatedAt: z.string().describe("The file update date"), + }) + ), + recipients: z.array( + z.object({ + id: z.string().describe("The recipient ID"), + email: z.string().email().describe("The recipient email"), + createdAt: z.string().describe("The recipient creation date"), + updatedAt: z.string().describe("The recipient update date"), + }) + ), + alias: ShareAliasResponseSchema.nullable(), +}); + +export const UpdateSharePasswordSchema = z.object({ + password: z.string().nullable().describe("The new password. Send null to remove password"), +}); + +export const UpdateShareFilesSchema = z.object({ + files: z.array(z.string().min(1, "File ID is required").describe("The file IDs")), +}); + +export const UpdateShareRecipientsSchema = z.object({ + emails: z.array(z.string().email("Invalid email format").describe("The recipient emails")), +}); + +export const CreateShareAliasSchema = z.object({ + shareId: z.string().describe("The share ID"), + alias: z + .string() + .regex(/^[a-zA-Z0-9]+$/, "Alias must contain only letters and numbers") + .min(3, "Alias must be at least 3 characters long") + .max(30, "Alias must not exceed 30 characters") + .describe("The custom alias for the share"), +}); + +export type CreateShareInput = z.infer; +export type UpdateShareInput = z.infer; +export type ShareResponse = z.infer; diff --git a/apps/server/src/modules/share/repository.ts b/apps/server/src/modules/share/repository.ts new file mode 100644 index 0000000..030b1fe --- /dev/null +++ b/apps/server/src/modules/share/repository.ts @@ -0,0 +1,189 @@ +import { prisma } from "../../shared/prisma"; +import type { CreateShareInput } from "./dto"; +import type { Share, ShareSecurity } from "@prisma/client"; + +export interface IShareRepository { + createShare(data: CreateShareInput & { securityId: string; creatorId: string }): Promise; + findShareById(id: string): Promise< + | (Share & { + security: ShareSecurity; + files: any[]; + recipients: { email: string }[]; + }) + | null + >; + findShareBySecurityId(securityId: string): Promise<(Share & { security: ShareSecurity; files: any[] }) | null>; + updateShare(id: string, data: Partial): Promise; + updateShareSecurity(id: string, data: Partial): Promise; + deleteShare(id: string): Promise; + incrementViews(id: string): Promise; + addFilesToShare(shareId: string, fileIds: string[]): Promise; + removeFilesFromShare(shareId: string, fileIds: string[]): Promise; + findFilesByIds(fileIds: string[]): Promise; + addRecipients(shareId: string, emails: string[]): Promise; + removeRecipients(shareId: string, emails: string[]): Promise; + findSharesByUserId(userId: string): Promise; +} + +export class PrismaShareRepository implements IShareRepository { + async createShare( + data: Omit & { securityId: string; creatorId: string } + ): Promise { + const { files, recipients, expiration, ...shareData } = data; + + // Validate and filter arrays + const validFiles = (files ?? []).filter((id) => id && id.trim().length > 0); + const validRecipients = (recipients ?? []).filter((email) => email && email.trim().length > 0); + + return prisma.share.create({ + data: { + ...shareData, + expiration: expiration ? new Date(expiration) : null, + files: + validFiles.length > 0 + ? { + connect: validFiles.map((id) => ({ id })), + } + : undefined, + recipients: + validRecipients?.length > 0 + ? { + create: validRecipients.map((email) => ({ + email: email.trim().toLowerCase(), + })), + } + : undefined, + }, + }); + } + + async findShareById(id: string) { + return prisma.share.findUnique({ + where: { id }, + include: { + security: true, + files: true, + recipients: true, + alias: true, + }, + }); + } + + async findShareBySecurityId(securityId: string) { + return prisma.share.findUnique({ + where: { securityId }, + include: { + security: true, + files: true, + }, + }); + } + + async updateShare(id: string, data: Partial): Promise { + return prisma.share.update({ + where: { id }, + data, + }); + } + + async updateShareSecurity(id: string, data: Partial): Promise { + return prisma.shareSecurity.update({ + where: { id }, + data, + }); + } + + async deleteShare(id: string): Promise { + return prisma.share.delete({ + where: { id }, + }); + } + + async incrementViews(id: string): Promise { + return prisma.share.update({ + where: { id }, + data: { + views: { + increment: 1, + }, + }, + }); + } + + async addFilesToShare(shareId: string, fileIds: string[]): Promise { + await prisma.share.update({ + where: { id: shareId }, + data: { + files: { + connect: fileIds.map((id) => ({ id })), + }, + }, + }); + } + + async removeFilesFromShare(shareId: string, fileIds: string[]): Promise { + await prisma.share.update({ + where: { id: shareId }, + data: { + files: { + disconnect: fileIds.map((id) => ({ id })), + }, + }, + }); + } + + async findFilesByIds(fileIds: string[]): Promise { + return prisma.file.findMany({ + where: { + id: { + in: fileIds, + }, + }, + }); + } + + async addRecipients(shareId: string, emails: string[]): Promise { + await prisma.share.update({ + where: { id: shareId }, + data: { + recipients: { + create: emails.map((email) => ({ + email, + })), + }, + }, + }); + } + + async removeRecipients(shareId: string, emails: string[]): Promise { + await prisma.share.update({ + where: { id: shareId }, + data: { + recipients: { + deleteMany: { + email: { + in: emails, + }, + }, + }, + }, + }); + } + + async findSharesByUserId(userId: string) { + return prisma.share.findMany({ + where: { + creatorId: userId, + }, + include: { + security: true, + files: true, + recipients: true, + alias: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + } +} diff --git a/apps/server/src/modules/share/routes.ts b/apps/server/src/modules/share/routes.ts new file mode 100644 index 0000000..f0d2d2e --- /dev/null +++ b/apps/server/src/modules/share/routes.ts @@ -0,0 +1,349 @@ +import { ShareController } from "./controller"; +import { + CreateShareSchema, + ShareResponseSchema, + UpdateShareSchema, + UpdateSharePasswordSchema, + UpdateShareFilesSchema, + UpdateShareRecipientsSchema, + ShareAliasResponseSchema, +} from "./dto"; +import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { z } from "zod"; + +export async function shareRoutes(app: FastifyInstance) { + const shareController = new ShareController(); + + const preValidation = async (request: FastifyRequest, reply: FastifyReply) => { + try { + await request.jwtVerify(); + } catch (err) { + console.error(err); + reply.status(401).send({ error: "Token inválido ou ausente." }); + } + }; + + app.post( + "/shares", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "createShare", + summary: "Create a new share", + description: "Create a new share", + body: CreateShareSchema, + response: { + 201: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.createShare.bind(shareController) + ); + + app.get( + "/shares/me", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "listUserShares", + summary: "List all shares created by the authenticated user", + description: "List all shares created by the authenticated user", + response: { + 200: z.object({ + shares: z.array(ShareResponseSchema), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.listUserShares.bind(shareController) + ); + + app.get( + "/shares/:shareId", + { + schema: { + tags: ["Share"], + operationId: "getShare", + summary: "Get a share by ID", + description: "Get a share by ID", + params: z.object({ + shareId: z.string().describe("The share ID"), + }), + querystring: z.object({ + password: z.string().optional().describe("The share password"), + }), + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.getShare.bind(shareController) + ); + + app.put( + "/shares", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "updateShare", + summary: "Update a share", + description: "Update a share", + body: UpdateShareSchema, + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.updateShare.bind(shareController) + ); + + app.delete( + "/shares/:id", + { + schema: { + tags: ["Share"], + operationId: "deleteShare", + summary: "Delete a share", + description: "Delete a share", + params: z.object({ + id: z.string().describe("The share ID"), + }), + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.deleteShare.bind(shareController) + ); + + app.patch( + "/shares/:shareId/password", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "updateSharePassword", + summary: "Update share password", + params: z.object({ + shareId: z.string(), + }), + body: UpdateSharePasswordSchema, + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.updatePassword.bind(shareController) + ); + + app.post( + "/shares/:shareId/files", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "addFiles", + summary: "Add files to share", + params: z.object({ + shareId: z.string().describe("The share ID"), + }), + body: UpdateShareFilesSchema, + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.addFiles.bind(shareController) + ); + + app.delete( + "/shares/:shareId/files", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "removeFiles", + summary: "Remove files from share", + params: z.object({ + shareId: z.string().describe("The share ID"), + }), + body: UpdateShareFilesSchema, + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.removeFiles.bind(shareController) + ); + + app.post( + "/shares/:shareId/recipients", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "addRecipients", + summary: "Add recipients to a share", + params: z.object({ + shareId: z.string().describe("The share ID"), + }), + body: UpdateShareRecipientsSchema, + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.addRecipients.bind(shareController) + ); + + app.delete( + "/shares/:shareId/recipients", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "removeRecipients", + summary: "Remove recipients from a share", + description: "Remove recipients from a share", + params: z.object({ + shareId: z.string().describe("The share ID"), + }), + body: UpdateShareRecipientsSchema, + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + shareController.removeRecipients.bind(shareController) + ); + + app.post( + "/shares/:shareId/alias", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "createShareAlias", + summary: "Create or update share alias", + params: z.object({ + shareId: z.string().describe("The share ID"), + }), + body: z.object({ + alias: z + .string() + .regex(/^[a-zA-Z0-9]+$/, "Alias must contain only letters and numbers") + .min(3, "Alias must be at least 3 characters long") + .max(30, "Alias must not exceed 30 characters"), + }), + response: { + 200: z.object({ + alias: ShareAliasResponseSchema, + }), + 400: z.object({ error: z.string() }), + 401: z.object({ error: z.string() }), + 404: z.object({ error: z.string() }), + }, + }, + }, + shareController.createOrUpdateAlias.bind(shareController) + ); + + app.get( + "/shares/alias/:alias", + { + schema: { + tags: ["Share"], + operationId: "getShareByAlias", + summary: "Get share by alias", + params: z.object({ + alias: z.string().describe("The share alias"), + }), + querystring: z.object({ + password: z.string().optional().describe("The share password"), + }), + response: { + 200: z.object({ + share: ShareResponseSchema, + }), + 404: z.object({ error: z.string() }), + }, + }, + }, + shareController.getShareByAlias.bind(shareController) + ); + + app.post( + "/shares/:shareId/notify", + { + preValidation, + schema: { + tags: ["Share"], + operationId: "notifyRecipients", + summary: "Send email notification to share recipients", + description: "Send email notification with share link to all recipients", + params: z.object({ + shareId: z.string().describe("The share ID"), + }), + body: z.object({ + shareLink: z.string().url().describe("The frontend share URL"), + }), + response: { + 200: z.object({ + message: z.string().describe("Success message"), + notifiedRecipients: z.array(z.string()).describe("List of notified email addresses"), + }), + 400: z.object({ error: z.string() }), + 401: z.object({ error: z.string() }), + 404: z.object({ error: z.string() }), + }, + }, + }, + shareController.notifyRecipients.bind(shareController) + ); +} diff --git a/apps/server/src/modules/share/service.ts b/apps/server/src/modules/share/service.ts new file mode 100644 index 0000000..ca2cb76 --- /dev/null +++ b/apps/server/src/modules/share/service.ts @@ -0,0 +1,360 @@ +import { prisma } from "../../shared/prisma"; +import { EmailService } from "../email/service"; +import { CreateShareInput, UpdateShareInput, ShareResponseSchema } from "./dto"; +import { PrismaShareRepository, IShareRepository } from "./repository"; +import bcrypt from "bcryptjs"; + +export class ShareService { + constructor(private readonly shareRepository: IShareRepository = new PrismaShareRepository()) {} + + private emailService = new EmailService(); + + private formatShareResponse(share: any) { + return { + ...share, + createdAt: share.createdAt.toISOString(), + updatedAt: share.updatedAt.toISOString(), + expiration: share.expiration?.toISOString() || null, + alias: share.alias + ? { + ...share.alias, + createdAt: share.alias.createdAt.toISOString(), + updatedAt: share.alias.updatedAt.toISOString(), + } + : null, + security: { + maxViews: share.security.maxViews, + hasPassword: !!share.security.password, + }, + files: + share.files?.map((file: any) => ({ + ...file, + size: file.size.toString(), + createdAt: file.createdAt.toISOString(), + updatedAt: file.updatedAt.toISOString(), + })) || [], + recipients: + share.recipients?.map((recipient: any) => ({ + ...recipient, + createdAt: recipient.createdAt.toISOString(), + updatedAt: recipient.updatedAt.toISOString(), + })) || [], + }; + } + + async createShare(data: CreateShareInput, userId: string) { + const security = await prisma.shareSecurity.create({ + data: { + password: data.password, + maxViews: data.maxViews, + }, + }); + + const { password, maxViews, ...shareData } = data; + + const share = await this.shareRepository.createShare({ + ...shareData, + securityId: security.id, + creatorId: userId, + }); + + const shareWithRelations = await this.shareRepository.findShareById(share.id); + return ShareResponseSchema.parse(this.formatShareResponse(shareWithRelations)); + } + + async getShare(shareId: string, password?: string, userId?: string) { + const share = await this.shareRepository.findShareById(shareId); + + if (!share) { + throw new Error("Share not found"); + } + + if (userId && share.creatorId === userId) { + return ShareResponseSchema.parse(this.formatShareResponse(share)); + } + + if (share.expiration && new Date() > new Date(share.expiration)) { + throw new Error("Share has expired"); + } + + if (share.security?.maxViews && share.views >= share.security.maxViews) { + throw new Error("Share has reached maximum views"); + } + + if (share.security?.password && !password) { + throw new Error("Password required"); + } + + if (share.security?.password && password) { + const isPasswordValid = await bcrypt.compare(password, share.security.password); + if (!isPasswordValid) { + throw new Error("Invalid password"); + } + } + + await this.shareRepository.incrementViews(shareId); + + const updatedShare = await this.shareRepository.findShareById(shareId); + return ShareResponseSchema.parse(this.formatShareResponse(updatedShare)); + } + + async updateShare(shareId: string, data: Omit, userId: string) { + const { password, maxViews, recipients, ...shareData } = data; + + const share = await this.shareRepository.findShareById(shareId); + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to update this share"); + } + + if (password || maxViews !== undefined) { + await this.shareRepository.updateShareSecurity(share.securityId, { + password: password ? await bcrypt.hash(password, 10) : undefined, + maxViews: maxViews, + }); + } + + if (recipients) { + await this.shareRepository.removeRecipients( + shareId, + share.recipients.map((r) => r.email) + ); + if (recipients.length > 0) { + await this.shareRepository.addRecipients(shareId, recipients); + } + } + + await this.shareRepository.updateShare(shareId, { + ...shareData, + expiration: shareData.expiration ? new Date(shareData.expiration) : null, + }); + const shareWithRelations = await this.shareRepository.findShareById(shareId); + + return this.formatShareResponse(shareWithRelations); + } + + async deleteShare(id: string) { + const share = await this.shareRepository.findShareById(id); + if (!share) { + throw new Error("Share not found"); + } + + const deleted = await prisma.$transaction(async (tx) => { + await tx.share.update({ + where: { id }, + data: { + files: { + set: [], + }, + }, + }); + + const deletedShare = await tx.share.delete({ + where: { id }, + include: { + security: true, + files: true, + }, + }); + + if (deletedShare.security) { + await tx.shareSecurity.delete({ + where: { id: deletedShare.security.id }, + }); + } + + return deletedShare; + }); + + return ShareResponseSchema.parse(this.formatShareResponse(deleted)); + } + + async listUserShares(userId: string) { + const shares = await this.shareRepository.findSharesByUserId(userId); + return shares.map((share) => this.formatShareResponse(share)); + } + + async updateSharePassword(shareId: string, userId: string, password: string | null) { + const share = await this.shareRepository.findShareById(shareId); + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to update this share"); + } + + await this.shareRepository.updateShareSecurity(share.security.id, { + password: password ? await bcrypt.hash(password, 10) : null, + }); + + const updated = await this.shareRepository.findShareById(shareId); + return ShareResponseSchema.parse(this.formatShareResponse(updated)); + } + + async addFilesToShare(shareId: string, userId: string, fileIds: string[]) { + const share = await this.shareRepository.findShareById(shareId); + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to update this share"); + } + + const existingFiles = await this.shareRepository.findFilesByIds(fileIds); + const notFoundFiles = fileIds.filter((id) => !existingFiles.some((file) => file.id === id)); + + if (notFoundFiles.length > 0) { + throw new Error(`Files not found: ${notFoundFiles.join(", ")}`); + } + + await this.shareRepository.addFilesToShare(shareId, fileIds); + const updated = await this.shareRepository.findShareById(shareId); + return ShareResponseSchema.parse(this.formatShareResponse(updated)); + } + + async removeFilesFromShare(shareId: string, userId: string, fileIds: string[]) { + const share = await this.shareRepository.findShareById(shareId); + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to update this share"); + } + + await this.shareRepository.removeFilesFromShare(shareId, fileIds); + const updated = await this.shareRepository.findShareById(shareId); + return ShareResponseSchema.parse(this.formatShareResponse(updated)); + } + + async findShareById(id: string) { + const share = await this.shareRepository.findShareById(id); + if (!share) { + throw new Error("Share not found"); + } + return share; + } + + async addRecipients(shareId: string, userId: string, emails: string[]) { + const share = await this.shareRepository.findShareById(shareId); + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to update this share"); + } + + await this.shareRepository.addRecipients(shareId, emails); + const updated = await this.shareRepository.findShareById(shareId); + return ShareResponseSchema.parse(this.formatShareResponse(updated)); + } + + async removeRecipients(shareId: string, userId: string, emails: string[]) { + const share = await this.shareRepository.findShareById(shareId); + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to update this share"); + } + + await this.shareRepository.removeRecipients(shareId, emails); + const updated = await this.shareRepository.findShareById(shareId); + return ShareResponseSchema.parse(this.formatShareResponse(updated)); + } + + async createOrUpdateAlias(shareId: string, alias: string, userId: string) { + const share = await this.findShareById(shareId); + + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to update this share"); + } + + // Verifica se o alias já está em uso por outro share + const existingAlias = await prisma.shareAlias.findUnique({ + where: { alias }, + }); + + if (existingAlias && existingAlias.shareId !== shareId) { + throw new Error("Alias already in use"); + } + + // Cria ou atualiza o alias + const shareAlias = await prisma.shareAlias.upsert({ + where: { shareId }, + create: { shareId, alias }, + update: { alias }, + }); + + return { + ...shareAlias, + createdAt: shareAlias.createdAt.toISOString(), + updatedAt: shareAlias.updatedAt.toISOString(), + }; + } + + async getShareByAlias(alias: string, password?: string) { + const shareAlias = await prisma.shareAlias.findUnique({ + where: { alias }, + include: { + share: { + include: { + security: true, + files: true, + recipients: true, + }, + }, + }, + }); + + if (!shareAlias) { + throw new Error("Share not found"); + } + + // Reutiliza a lógica existente do getShare + return this.getShare(shareAlias.shareId, password); + } + + async notifyRecipients(shareId: string, userId: string, shareLink: string) { + const share = await this.shareRepository.findShareById(shareId); + + if (!share) { + throw new Error("Share not found"); + } + + if (share.creatorId !== userId) { + throw new Error("Unauthorized to access this share"); + } + + if (!share.recipients || share.recipients.length === 0) { + throw new Error("No recipients found for this share"); + } + + const notifiedRecipients: string[] = []; + + for (const recipient of share.recipients) { + try { + await this.emailService.sendShareNotification(recipient.email, shareLink, share.name || undefined); + notifiedRecipients.push(recipient.email); + } catch (error) { + console.error(`Failed to send email to ${recipient.email}:`, error); + } + } + + return { + message: `Successfully sent notifications to ${notifiedRecipients.length} recipients`, + notifiedRecipients, + }; + } +} diff --git a/apps/server/src/modules/storage/controller.ts b/apps/server/src/modules/storage/controller.ts new file mode 100644 index 0000000..eaae3c5 --- /dev/null +++ b/apps/server/src/modules/storage/controller.ts @@ -0,0 +1,55 @@ +import { StorageService } from "./service"; +import { FastifyRequest, FastifyReply } from "fastify"; + +export class StorageController { + private storageService = new StorageService(); + + async getDiskSpace(request: FastifyRequest, reply: FastifyReply) { + try { + let userId: string | undefined; + let isAdmin = false; + + try { + await request.jwtVerify(); + userId = (request as any).user?.userId; + isAdmin = (request as any).user?.isAdmin || false; + } catch (err) { + return reply.status(401).send({ + error: "Unauthorized: a valid token is required to access this resource.", + }); + } + + const diskSpace = await this.storageService.getDiskSpace(userId, isAdmin); + return reply.send(diskSpace); + } catch (error: any) { + return reply.status(500).send({ error: error.message }); + } + } + + async checkUploadAllowed(request: FastifyRequest, reply: FastifyReply) { + try { + const { fileSize } = request.query as { fileSize: string }; + let userId: string | undefined; + + try { + await request.jwtVerify(); + userId = (request as any).user?.userId; + } catch (err) { + return reply.status(401).send({ + error: "Unauthorized: a valid token is required to access this resource.", + }); + } + + if (!fileSize) { + return reply.status(400).send({ + error: "File size parameter is required (in bytes)", + }); + } + + const result = await this.storageService.checkUploadAllowed(Number(fileSize), userId); + return reply.send(result); + } catch (error: any) { + return reply.status(500).send({ error: error.message }); + } + } +} diff --git a/apps/server/src/modules/storage/routes.ts b/apps/server/src/modules/storage/routes.ts new file mode 100644 index 0000000..7cc4383 --- /dev/null +++ b/apps/server/src/modules/storage/routes.ts @@ -0,0 +1,61 @@ +import { StorageController } from "./controller"; +import { FastifyInstance } from "fastify"; +import { z } from "zod"; + +export async function storageRoutes(app: FastifyInstance) { + const storageController = new StorageController(); + + app.get( + "/storage/disk-space", + { + schema: { + tags: ["Storage"], + operationId: "getDiskSpace", + summary: "Get server disk space information", + description: "Get server disk space information", + response: { + 200: z.object({ + diskSizeGB: z.number().describe("The server disk size in GB"), + diskUsedGB: z.number().describe("The server disk used in GB"), + diskAvailableGB: z.number().describe("The server disk available in GB"), + uploadAllowed: z.boolean().describe("Whether file upload is allowed"), + }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + storageController.getDiskSpace.bind(storageController) + ); + + app.get( + "/storage/check-upload", + { + schema: { + tags: ["Storage"], + operationId: "checkUploadAllowed", + summary: "Check if file upload is allowed", + description: "Check if file upload is allowed based on available space (fileSize in bytes)", + querystring: z.object({ + fileSize: z.string().describe("The file size in bytes"), + }), + response: { + 200: z.object({ + diskSizeGB: z.number().describe("The server disk size in GB"), + diskUsedGB: z.number().describe("The server disk used in GB"), + diskAvailableGB: z.number().describe("The server disk available in GB"), + uploadAllowed: z.boolean().describe("Whether file upload is allowed"), + fileSizeInfo: z.object({ + bytes: z.number(), + kb: z.number(), + mb: z.number(), + gb: z.number(), + }), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 500: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + storageController.checkUploadAllowed.bind(storageController) + ); +} diff --git a/apps/server/src/modules/storage/service.ts b/apps/server/src/modules/storage/service.ts new file mode 100644 index 0000000..1004e4c --- /dev/null +++ b/apps/server/src/modules/storage/service.ts @@ -0,0 +1,113 @@ +import { ConfigService } from "../config/service"; +import { PrismaClient } from "@prisma/client"; +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); +const prisma = new PrismaClient(); + +export class StorageService { + private configService = new ConfigService(); + + async getDiskSpace( + userId?: string, + isAdmin?: boolean + ): Promise<{ + diskSizeGB: number; + diskUsedGB: number; + diskAvailableGB: number; + uploadAllowed: boolean; + }> { + try { + if (isAdmin) { + // Original implementation for admins + const command = process.platform === "win32" ? "wmic logicaldisk get size,freespace,caption" : "df -B1 ."; + + const { stdout } = await execAsync(command); + let total = 0; + let available = 0; + + if (process.platform === "win32") { + const lines = stdout.trim().split("\n").slice(1); + for (const line of lines) { + const [, size, freespace] = line.trim().split(/\s+/); + total += parseInt(size) || 0; + available += parseInt(freespace) || 0; + } + } else { + const lines = stdout.trim().split("\n"); + const [, size, , avail] = lines[1].trim().split(/\s+/); + total = parseInt(size); + available = parseInt(avail); + } + + const used = total - available; + + return { + diskSizeGB: Number((total / (1024 * 1024 * 1024)).toFixed(2)), + diskUsedGB: Number((used / (1024 * 1024 * 1024)).toFixed(2)), + diskAvailableGB: Number((available / (1024 * 1024 * 1024)).toFixed(2)), + uploadAllowed: true, + }; + } else if (userId) { + // Implementation for regular users + const maxTotalStorage = BigInt(await this.configService.getValue("maxTotalStoragePerUser")); + const maxStorageGB = Number(maxTotalStorage) / (1024 * 1024 * 1024); + + // Busca apenas os arquivos que pertencem diretamente ao usuário + const userFiles = await prisma.file.findMany({ + where: { userId }, + select: { size: true }, + }); + + // Calcula o total de espaço usado somando os arquivos + const totalUsedStorage = userFiles.reduce((acc, file) => acc + file.size, BigInt(0)); + + const usedStorageGB = Number(totalUsedStorage) / (1024 * 1024 * 1024); + const availableStorageGB = maxStorageGB - usedStorageGB; + + return { + diskSizeGB: maxStorageGB, + diskUsedGB: usedStorageGB, + diskAvailableGB: availableStorageGB, + uploadAllowed: availableStorageGB > 0, + }; + } + + throw new Error("User ID is required for non-admin users"); + } catch (error) { + console.error("Error getting disk space:", error); + throw new Error("Failed to get disk space information"); + } + } + + async checkUploadAllowed( + fileSize: number, + userId?: string + ): Promise<{ + diskSizeGB: number; + diskUsedGB: number; + diskAvailableGB: number; + uploadAllowed: boolean; + fileSizeInfo: { + bytes: number; + kb: number; + mb: number; + gb: number; + }; + }> { + const diskSpace = await this.getDiskSpace(userId); + const fileSizeGB = fileSize / (1024 * 1024 * 1024); + + return { + ...diskSpace, + uploadAllowed: diskSpace.diskAvailableGB > fileSizeGB, + fileSizeInfo: { + bytes: fileSize, + kb: Number((fileSize / 1024).toFixed(2)), + mb: Number((fileSize / (1024 * 1024)).toFixed(2)), + gb: Number((fileSize / (1024 * 1024 * 1024)).toFixed(2)), + }, + }; + } +} diff --git a/apps/server/src/modules/user/avatar.service.ts b/apps/server/src/modules/user/avatar.service.ts new file mode 100644 index 0000000..040b61a --- /dev/null +++ b/apps/server/src/modules/user/avatar.service.ts @@ -0,0 +1,80 @@ +import { minioClient } from "../../config/minio.config"; +import { PrismaClient } from "@prisma/client"; +import { randomUUID } from "crypto"; +import sharp from "sharp"; + +const prisma = new PrismaClient(); + +export class AvatarService { + private readonly bucketName = "avatars"; + + constructor() { + this.initializeBucket(); + } + + private async initializeBucket() { + try { + const bucketExists = await minioClient.bucketExists(this.bucketName); + if (!bucketExists) { + await minioClient.makeBucket(this.bucketName, "sa-east-1"); + const policy = { + Version: "2012-10-17", + Statement: [ + { + Effect: "Allow", + Principal: { AWS: ["*"] }, + Action: ["s3:GetObject"], + Resource: [`arn:aws:s3:::${this.bucketName}/*`], + }, + ], + }; + await minioClient.setBucketPolicy(this.bucketName, JSON.stringify(policy)); + } + } catch (error) { + console.error("Error initializing avatar bucket:", error); + } + } + + async uploadAvatar(userId: string, imageBuffer: Buffer): Promise { + try { + // Buscar usuário atual para verificar se tem avatar + const user = await prisma.user.findUnique({ + where: { id: userId }, + select: { image: true }, + }); + + // Deletar avatar anterior se existir + if (user?.image) { + await this.deleteAvatar(user.image); + } + + // Validar e fazer upload do novo avatar + const metadata = await sharp(imageBuffer).metadata(); + if (!metadata.width || !metadata.height) { + throw new Error("Invalid image file"); + } + + const webpBuffer = await sharp(imageBuffer).resize(256, 256, { fit: "cover" }).webp({ quality: 80 }).toBuffer(); + + const objectName = `${userId}/${randomUUID()}.webp`; + await minioClient.putObject(this.bucketName, objectName, webpBuffer); + + const publicUrl = `${process.env.MINIO_PUBLIC_URL}/${this.bucketName}/${objectName}`; + return publicUrl; + } catch (error) { + console.error("Error uploading avatar:", error); + throw error; + } + } + + async deleteAvatar(imageUrl: string) { + try { + const objectName = imageUrl.split(`/${this.bucketName}/`)[1]; + console.log("Deleting avatar:", objectName); + await minioClient.removeObject(this.bucketName, objectName); + } catch (error) { + console.error("Error deleting avatar:", error); + throw error; + } + } +} diff --git a/apps/server/src/modules/user/controller.ts b/apps/server/src/modules/user/controller.ts new file mode 100644 index 0000000..67e5efb --- /dev/null +++ b/apps/server/src/modules/user/controller.ts @@ -0,0 +1,134 @@ +import { AvatarService } from "./avatar.service"; +import { UpdateUserSchema, createRegisterUserSchema } from "./dto"; +import { UserService } from "./service"; +import { MultipartFile } from "@fastify/multipart"; +import { FastifyReply, FastifyRequest } from "fastify"; + +export class UserController { + private userService = new UserService(); + private avatarService = new AvatarService(); + + async register(request: FastifyRequest, reply: FastifyReply) { + try { + const schema = await createRegisterUserSchema(); + const input = schema.parse(request.body); + const user = await this.userService.register(input); + return reply.status(201).send({ user, message: "User created successfully" }); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async listUsers(request: FastifyRequest, reply: FastifyReply) { + try { + const users = await this.userService.listUsers(); + return reply.send(users); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async getUserById(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params as { id: string }; + const user = await this.userService.getUserById(id); + return reply.send(user); + } catch (error: any) { + return reply.status(404).send({ error: error.message }); + } + } + + async updateUser(request: FastifyRequest, reply: FastifyReply) { + try { + const input = UpdateUserSchema.parse(request.body); + const { id, ...updateData } = input; + const updatedUser = await this.userService.updateUser(id, updateData); + return reply.send(updatedUser); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async activateUser(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params as { id: string }; + const user = await this.userService.activateUser(id); + return reply.send(user); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async deactivateUser(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params as { id: string }; + const user = await this.userService.deactivateUser(id); + return reply.send(user); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async deleteUser(request: FastifyRequest, reply: FastifyReply) { + try { + const { id } = request.params as { id: string }; + const user = await this.userService.deleteUser(id); + return reply.send(user); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async updateUserImage(request: FastifyRequest, reply: FastifyReply) { + try { + const input = UpdateUserSchema.parse(request.body); + const { id, ...updateData } = input; + const updatedUser = await this.userService.updateUser(id, updateData); + return reply.send(updatedUser); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } + + async uploadAvatar(request: FastifyRequest, reply: FastifyReply) { + try { + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized" }); + } + + const file = (request.body as any).file as MultipartFile; + if (!file) { + return reply.status(400).send({ error: "No file uploaded" }); + } + + const buffer = await file.toBuffer(); + const imageUrl = await this.avatarService.uploadAvatar(userId, buffer); + const updatedUser = await this.userService.updateUserImage(userId, imageUrl); + return reply.send(updatedUser); + } catch (error: any) { + console.error("Upload error:", error); + return reply.status(400).send({ error: error.message }); + } + } + + async removeAvatar(request: FastifyRequest, reply: FastifyReply) { + try { + const userId = (request as any).user?.userId; + if (!userId) { + return reply.status(401).send({ error: "Unauthorized" }); + } + + const user = await this.userService.getUserById(userId); + if (user.image) { + await this.avatarService.deleteAvatar(user.image); + const updatedUser = await this.userService.updateUserImage(userId, null); + return reply.send(updatedUser); + } + + return reply.send(user); + } catch (error: any) { + return reply.status(400).send({ error: error.message }); + } + } +} diff --git a/apps/server/src/modules/user/dto.ts b/apps/server/src/modules/user/dto.ts new file mode 100644 index 0000000..cfe6382 --- /dev/null +++ b/apps/server/src/modules/user/dto.ts @@ -0,0 +1,54 @@ +import { ConfigService } from "../config/service"; +import { z } from "zod"; + +const configService = new ConfigService(); + +export const BaseRegisterUserSchema = z.object({ + firstName: z.string().min(1), + lastName: z.string().min(1), + username: z.string().min(3), + email: z.string().email(), + image: z.string().optional(), + isAdmin: z.boolean().optional().default(false), +}); + +export type BaseRegisterUserInput = z.infer; + +export const createRegisterUserSchema = async () => { + const minLength = Number(await configService.getValue("passwordMinLength")); + return BaseRegisterUserSchema.extend({ + password: z.string().min(minLength, `Password must be at least ${minLength} characters`), + }); +}; + +export type RegisterUserInput = BaseRegisterUserInput & { + password: string; +}; + +export const UpdateUserSchema = z.object({ + id: z.string(), + firstName: z.string().min(1).optional(), + lastName: z.string().min(1).optional(), + username: z.string().min(3).optional(), + email: z.string().email().optional(), + image: z.string().optional(), + password: z.string().optional(), + isAdmin: z.boolean().optional(), +}); + +export type UpdateUserInput = z.infer; + +export const UserResponseSchema = z.object({ + id: z.string(), + firstName: z.string(), + lastName: z.string(), + username: z.string(), + email: z.string(), + image: z.string().nullable(), + isAdmin: z.boolean(), + isActive: z.boolean(), + createdAt: z.date(), + updatedAt: z.date(), +}); + +export type UserResponse = z.infer; diff --git a/apps/server/src/modules/user/middleware.ts b/apps/server/src/modules/user/middleware.ts new file mode 100644 index 0000000..79062dc --- /dev/null +++ b/apps/server/src/modules/user/middleware.ts @@ -0,0 +1,17 @@ +import { ConfigService } from "../config/service"; +import { FastifyRequest, FastifyReply } from "fastify"; + +const configService = new ConfigService(); + +export async function validatePasswordMiddleware(request: FastifyRequest, reply: FastifyReply) { + const body = request.body as any; + if (!body.password) return; + + const minLength = Number(await configService.getValue("passwordMinLength")); + + if (body.password.length < minLength) { + return reply.status(400).send({ + error: `Password must be at least ${minLength} characters long`, + }); + } +} diff --git a/apps/server/src/modules/user/repository.ts b/apps/server/src/modules/user/repository.ts new file mode 100644 index 0000000..0300d70 --- /dev/null +++ b/apps/server/src/modules/user/repository.ts @@ -0,0 +1,72 @@ +import { prisma } from "../../shared/prisma"; +import type { RegisterUserInput, UpdateUserInput } from "./dto"; +import type { User } from "@prisma/client"; + +export interface IUserRepository { + createUser(data: RegisterUserInput & { password: string }): Promise; + findUserByEmail(email: string): Promise; + findUserById(id: string): Promise; + findUserByUsername(username: string): Promise; + listUsers(): Promise; + updateUser(data: UpdateUserInput & { password?: string }): Promise; + deleteUser(id: string): Promise; + activateUser(id: string): Promise; + deactivateUser(id: string): Promise; +} + +export class PrismaUserRepository implements IUserRepository { + async createUser(data: RegisterUserInput & { password: string }): Promise { + return prisma.user.create({ + data: { + firstName: data.firstName, + lastName: data.lastName, + username: data.username, + email: data.email, + password: data.password, + image: data.image, + }, + }); + } + + async findUserByEmail(email: string): Promise { + return prisma.user.findUnique({ where: { email } }); + } + + async findUserById(id: string): Promise { + return prisma.user.findUnique({ where: { id } }); + } + + async findUserByUsername(username: string): Promise { + return prisma.user.findUnique({ where: { username } }); + } + + async listUsers(): Promise { + return prisma.user.findMany(); + } + + async updateUser(data: UpdateUserInput & { password?: string }): Promise { + const { id, ...rest } = data; + return prisma.user.update({ + where: { id }, + data: rest, + }); + } + + async deleteUser(id: string): Promise { + return prisma.user.delete({ where: { id } }); + } + + async activateUser(id: string): Promise { + return prisma.user.update({ + where: { id }, + data: { isActive: true }, + }); + } + + async deactivateUser(id: string): Promise { + return prisma.user.update({ + where: { id }, + data: { isActive: false }, + }); + } +} diff --git a/apps/server/src/modules/user/routes.ts b/apps/server/src/modules/user/routes.ts new file mode 100644 index 0000000..f1aaffc --- /dev/null +++ b/apps/server/src/modules/user/routes.ts @@ -0,0 +1,359 @@ +import { createPasswordSchema } from "../auth/dto"; +import { UserController } from "./controller"; +import { UpdateUserSchema, UserResponseSchema } from "./dto"; +import { validatePasswordMiddleware } from "./middleware"; +import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify"; +import { z } from "zod"; + +export async function userRoutes(app: FastifyInstance) { + const userController = new UserController(); + + const preValidation = async (request: any, reply: any) => { + try { + await request.jwtVerify(); + if (!request.user.isAdmin) { + return reply + .status(403) + .send({ error: "Access restricted to administrators" }) + .description("Access restricted to administrators"); + } + } catch (err) { + console.error(err); + return reply + .status(401) + .send({ error: "Unauthorized: a valid token is required to access this resource." }) + .description("Unauthorized: a valid token is required to access this resource."); + } + }; + + const createRegisterSchema = async () => { + const passwordSchema = await createPasswordSchema(); + return z.object({ + firstName: z.string().min(1).describe("User first name"), + lastName: z.string().min(1).describe("User last name"), + username: z.string().min(3).describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().optional().describe("User profile image URL"), + password: passwordSchema.describe("User password"), + }); + }; + + const createUpdateSchema = async () => { + const passwordSchema = await createPasswordSchema(); + return UpdateUserSchema.extend({ + password: passwordSchema.optional(), + }); + }; + + app.post( + "/auth/register", + { + preValidation: [preValidation, validatePasswordMiddleware], + schema: { + tags: ["User"], + operationId: "registerUser", + summary: "Register New User", + description: "Register a new user (admin only)", + body: await createRegisterSchema(), + response: { + 201: z.object({ + user: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + message: z.string().describe("User registration message"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.register.bind(userController) + ); + + app.get( + "/users", + { + preValidation, + schema: { + tags: ["User"], + operationId: "listUsers", + summary: "List All Users", + description: "List all users (admin only)", + response: { + 200: z.array( + z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }) + ), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.listUsers.bind(userController) + ); + + app.get( + "/users/:id", + { + preValidation, + schema: { + tags: ["User"], + operationId: "getUserById", + summary: "Get User by ID", + description: "Get a user by ID (admin only)", + params: z.object({ id: z.string().describe("User ID") }), + response: { + 200: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + 404: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.getUserById.bind(userController) + ); + + app.put( + "/users", + { + preValidation, + schema: { + tags: ["User"], + operationId: "updateUser", + summary: "Update User Data", + description: "Update user data (admin only)", + body: await createUpdateSchema(), + response: { + 200: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.updateUser.bind(userController) + ); + + app.patch( + "/users/:id/activate", + { + preValidation, + schema: { + tags: ["User"], + operationId: "activateUser", + summary: "Activate User", + description: "Activate a user (admin only)", + params: z.object({ id: z.string().describe("User ID") }), + response: { + 200: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.activateUser.bind(userController) + ); + + app.patch( + "/users/:id/deactivate", + { + preValidation, + schema: { + tags: ["User"], + operationId: "deactivateUser", + summary: "Deactivate User", + description: "Deactivate a user (admin only)", + params: z.object({ id: z.string().describe("User ID") }), + response: { + 200: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.deactivateUser.bind(userController) + ); + + app.delete( + "/users/:id", + { + preValidation, + schema: { + tags: ["User"], + operationId: "deleteUser", + summary: "Delete User", + description: "Delete a user (admin only)", + params: z.object({ id: z.string().describe("User ID") }), + response: { + 200: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.deleteUser.bind(userController) + ); + + app.patch( + "/users/:id/image", + { + preValidation, + schema: { + tags: ["User"], + operationId: "updateUserImage", + summary: "Update User Image", + description: "Update user profile image (admin only)", + params: z.object({ id: z.string().describe("User ID") }), + body: z.object({ + image: z.string().url().describe("User profile image URL"), + }), + response: { + 200: z.object({ + id: z.string().describe("User ID"), + firstName: z.string().describe("User first name"), + lastName: z.string().describe("User last name"), + username: z.string().describe("User username"), + email: z.string().email().describe("User email"), + image: z.string().nullable().describe("User profile image URL"), + isAdmin: z.boolean().describe("User is admin"), + isActive: z.boolean().describe("User is active"), + createdAt: z.date().describe("User creation date"), + updatedAt: z.date().describe("User last update date"), + }), + 400: z.object({ error: z.string().describe("Error message") }), + 401: z.object({ error: z.string().describe("Error message") }), + 403: z.object({ error: z.string().describe("Error message") }), + }, + }, + }, + userController.updateUserImage.bind(userController) + ); + + app.post( + "/users/avatar", + { + preValidation, + schema: { + tags: ["User"], + operationId: "uploadAvatar", + summary: "Upload user avatar", + description: "Upload and update user profile image", + consumes: ["multipart/form-data"], + body: z.object({ + file: z.any().describe("Image file (JPG, PNG, GIF)"), + }), + response: { + 200: UserResponseSchema, + 400: z.object({ error: z.string() }), + 401: z.object({ error: z.string() }), + }, + }, + }, + userController.uploadAvatar.bind(userController) + ); + + app.delete( + "/users/avatar", + { + preValidation: async (request: FastifyRequest, reply: FastifyReply) => { + try { + await request.jwtVerify(); + } catch (err) { + console.error(err); + reply.status(401).send({ error: "Unauthorized" }); + } + }, + schema: { + tags: ["User"], + operationId: "removeAvatar", + summary: "Remove user avatar", + description: "Remove user profile image", + response: { + 200: UserResponseSchema, + 401: z.object({ error: z.string() }), + }, + }, + }, + userController.removeAvatar.bind(userController) + ); +} diff --git a/apps/server/src/modules/user/service.ts b/apps/server/src/modules/user/service.ts new file mode 100644 index 0000000..fd9c198 --- /dev/null +++ b/apps/server/src/modules/user/service.ts @@ -0,0 +1,95 @@ +import { RegisterUserInput, UserResponseSchema } from "./dto"; +import { PrismaUserRepository, IUserRepository } from "./repository"; +import { PrismaClient } from "@prisma/client"; +import bcrypt from "bcryptjs"; + +type UserWithPassword = { + id: string; + email?: string; + firstName?: string; + lastName?: string; + username?: string; + password?: string; +}; + +const prisma = new PrismaClient(); + +export class UserService { + constructor(private readonly userRepository: IUserRepository = new PrismaUserRepository()) {} + + async register(data: RegisterUserInput) { + const existingUser = await this.userRepository.findUserByEmail(data.email); + const existingUsername = await this.userRepository.findUserByUsername(data.username); + + if (existingUser) { + throw new Error("User with this email already exists"); + } + + if (existingUsername) { + throw new Error("User with this username already exists"); + } + + const hashedPassword = await bcrypt.hash(data.password, 10); + const user = await this.userRepository.createUser({ + ...data, + password: hashedPassword, + }); + return UserResponseSchema.parse(user); + } + + async listUsers() { + const users = await this.userRepository.listUsers(); + return users.map((user) => UserResponseSchema.parse(user)); + } + + async getUserById(id: string) { + const user = await this.userRepository.findUserById(id); + if (!user) { + throw new Error("User not found"); + } + return UserResponseSchema.parse(user); + } + + async updateUser(userId: string, data: Partial) { + const { password, ...rest } = data; + + const updateData: any = { ...rest }; + + if (password) { + updateData.password = await bcrypt.hash(password, 10); + } + + const user = await this.userRepository.updateUser({ + id: userId, + ...updateData, + }); + + return UserResponseSchema.parse(user); + } + + async deleteUser(id: string) { + const deleted = await this.userRepository.deleteUser(id); + return UserResponseSchema.parse(deleted); + } + + async activateUser(id: string) { + const user = await this.userRepository.activateUser(id); + return UserResponseSchema.parse(user); + } + + async deactivateUser(id: string) { + const user = await this.userRepository.deactivateUser(id); + return UserResponseSchema.parse(user); + } + + async updateUserImage(userId: string, imageUrl: string | null) { + const user = await prisma.user.update({ + where: { id: userId }, + data: { + image: imageUrl, + updatedAt: new Date(), + }, + }); + return user; + } +} diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts new file mode 100644 index 0000000..5f9086c --- /dev/null +++ b/apps/server/src/server.ts @@ -0,0 +1,44 @@ +import { buildApp } from "./app"; +import { env } from "./env"; +import { appRoutes } from "./modules/app/routes"; +import { authRoutes } from "./modules/auth/routes"; +import { fileRoutes } from "./modules/file/routes"; +import { healthRoutes } from "./modules/health/routes"; +import { shareRoutes } from "./modules/share/routes"; +import { storageRoutes } from "./modules/storage/routes"; +import { userRoutes } from "./modules/user/routes"; +import fastifyMultipart from "@fastify/multipart"; + +async function startServer() { + const app = await buildApp(); + + await app.register(fastifyMultipart, { + limits: { + fileSize: 5 * 1024 * 1024, + }, + attachFieldsToBody: true, + }); + + app.register(authRoutes); + app.register(userRoutes); + app.register(fileRoutes); + app.register(shareRoutes); + app.register(storageRoutes); + app.register(appRoutes); + app.register(healthRoutes); + + await app.listen({ + port: Number(env.PORT), + host: "0.0.0.0", + }); + + console.log(`🌴 Palmr server running on port ${env.PORT} 🌴`); + + console.log("\n📚 API Documentation:"); + console.log(` - API Reference: http://localhost:${env.PORT}/docs\n`); +} + +startServer().catch((error) => { + console.error("Error starting server:", error); + process.exit(1); +}); diff --git a/apps/server/src/shared/prisma.ts b/apps/server/src/shared/prisma.ts new file mode 100644 index 0000000..a031944 --- /dev/null +++ b/apps/server/src/shared/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export { prisma }; diff --git a/apps/server/src/types/fastify.d.ts b/apps/server/src/types/fastify.d.ts new file mode 100644 index 0000000..1e3b737 --- /dev/null +++ b/apps/server/src/types/fastify.d.ts @@ -0,0 +1,14 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import type { FastifyRequest } from "fastify"; + +declare module "fastify" { + interface FastifyRequest { + /** + * Método decorado para assinar um payload JWT. + * @param payload - Objeto que será assinado. + * @param options - Opções adicionais para a assinatura. + * @returns O token JWT assinado. + */ + jwtSign(payload: object, options?: object): string; + } +} diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json new file mode 100644 index 0000000..c87535e --- /dev/null +++ b/apps/server/tsconfig.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Node 22", + "_version": "22.0.0", + "compilerOptions": { + "lib": [ + "es2023" + ], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "./dist", + "rootDir": "./src", + "baseUrl": "./src", + "paths": { + "@/*": [ + "src/*" + ] + } + }, + "include": [ + "src/**/*" + ] +} \ No newline at end of file diff --git a/apps/web/.env b/apps/web/.env new file mode 100644 index 0000000..3d5084c --- /dev/null +++ b/apps/web/.env @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:3333 \ No newline at end of file diff --git a/apps/web/.env.example b/apps/web/.env.example new file mode 100644 index 0000000..b111bff --- /dev/null +++ b/apps/web/.env.example @@ -0,0 +1 @@ +# VITE_API_URL=http://localhost:3333 \ No newline at end of file diff --git a/apps/web/.eslintignore b/apps/web/.eslintignore new file mode 100644 index 0000000..af6ab76 --- /dev/null +++ b/apps/web/.eslintignore @@ -0,0 +1,20 @@ +.now/* +*.css +.changeset +dist +esm/* +public/* +tests/* +scripts/* +*.config.js +.DS_Store +node_modules +coverage +.next +build +!.commitlintrc.cjs +!.lintstagedrc.cjs +!jest.config.js +!plopfile.js +!react-shim.js +!tsup.config.ts \ No newline at end of file diff --git a/apps/web/.eslintrc.json b/apps/web/.eslintrc.json new file mode 100644 index 0000000..ad5fa64 --- /dev/null +++ b/apps/web/.eslintrc.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json.schemastore.org/eslintrc.json", + "env": { + "browser": false, + "es2021": true, + "node": true + }, + "extends": [ + "plugin:react/recommended", + "plugin:prettier/recommended", + "plugin:react-hooks/recommended", + "plugin:jsx-a11y/recommended" + ], + "plugins": [ + "react", + "unused-imports", + "import", + "@typescript-eslint", + "jsx-a11y", + "prettier" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 12, + "sourceType": "module" + }, + "settings": { + "react": { + "version": "detect" + } + }, + "rules": { + "no-console": "off", + "react/prop-types": "off", + "react/jsx-uses-react": "off", + "react/react-in-jsx-scope": "off", + "react-hooks/exhaustive-deps": "off", + "jsx-a11y/click-events-have-key-events": "off", + "jsx-a11y/interactive-supports-focus": "off", + "jsx-a11y/no-static-element-interactions": "off", + "prettier/prettier": "warn", + "no-unused-vars": "off", + "unused-imports/no-unused-vars": "off", + "unused-imports/no-unused-imports": "warn", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "after-used", + "ignoreRestSiblings": false, + "argsIgnorePattern": "^_.*?$" + } + ], + "import/order": [ + "off", + { + "groups": [ + "type", + "builtin", + "object", + "external", + "internal", + "parent", + "sibling", + "index" + ], + "pathGroups": [ + { + "pattern": "~/**", + "group": "external", + "position": "after" + } + ], + "newlines-between": "always" + } + ], + "react/self-closing-comp": "warn", + "react/jsx-sort-props": [ + "warn", + { + "callbacksLast": true, + "shorthandFirst": true, + "noSortAlphabetically": false, + "reservedFirst": true + } + ], + "padding-line-between-statements": [ + "warn", + { + "blankLine": "always", + "prev": "*", + "next": "return" + }, + { + "blankLine": "always", + "prev": [ + "const", + "let", + "var" + ], + "next": "*" + }, + { + "blankLine": "any", + "prev": [ + "const", + "let", + "var" + ], + "next": [ + "const", + "let", + "var" + ] + } + ] + } +} \ No newline at end of file diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 0000000..3d27651 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + + +pnpm-lock.yaml +yarn.lock +package-lock.json +bun.lockb \ No newline at end of file diff --git a/apps/web/.npmrc b/apps/web/.npmrc new file mode 100644 index 0000000..f819c90 --- /dev/null +++ b/apps/web/.npmrc @@ -0,0 +1,2 @@ +public-hoist-pattern[]=*@heroui/* +package-lock=true \ No newline at end of file diff --git a/apps/web/.prettierrc.json b/apps/web/.prettierrc.json new file mode 100644 index 0000000..af95e92 --- /dev/null +++ b/apps/web/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "plugins": ["@trivago/prettier-plugin-sort-imports"], + "printWidth": 120, + "singleQuote": false, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..5f295b0 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,20 @@ +FROM node:18-alpine AS builder + +WORKDIR /app/web + +RUN npm install -g pnpm + +COPY package.json . +COPY pnpm-lock.yaml . + +RUN pnpm install + +COPY . . + +RUN pnpm build + +EXPOSE 4173 + +ENV VITE_ROUTER_MODE=history + +CMD ["pnpm", "preview", "--host"] diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..c1bf328 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1 @@ +# 🌴 Palmr diff --git a/apps/web/favicon.ico b/apps/web/favicon.ico new file mode 100644 index 0000000..1dffba3 Binary files /dev/null and b/apps/web/favicon.ico differ diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..4d88819 --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,36 @@ + + + + + + Palmr. + + + + + + + + + +
+ + + diff --git a/apps/web/orval.config.ts b/apps/web/orval.config.ts new file mode 100644 index 0000000..fd62abd --- /dev/null +++ b/apps/web/orval.config.ts @@ -0,0 +1,10 @@ +module.exports = { + "palmr-file": { + input: "./routes.json", + output: { + mode: "single", + target: "./src/http/endpoints/palmrAPI.ts", + schemas: "./src/http/models", + }, + }, +}; diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..01bc71d --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,83 @@ +{ + "name": "vite-template", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix", + "preview": "vite preview", + "format": "prettier --write ." + }, + "dependencies": { + "@heroui/avatar": "^2.2.7", + "@heroui/badge": "^2.2.6", + "@heroui/breadcrumbs": "^2.2.7", + "@heroui/button": "2.2.10", + "@heroui/card": "^2.2.10", + "@heroui/chip": "^2.2.7", + "@heroui/code": "2.2.7", + "@heroui/divider": "^2.2.6", + "@heroui/dropdown": "2.3.10", + "@heroui/image": "^2.2.6", + "@heroui/input": "2.4.10", + "@heroui/kbd": "2.2.7", + "@heroui/link": "2.2.8", + "@heroui/modal": "^2.2.8", + "@heroui/navbar": "2.2.9", + "@heroui/progress": "^2.2.7", + "@heroui/select": "^2.4.10", + "@heroui/snippet": "2.2.11", + "@heroui/switch": "2.2.9", + "@heroui/system": "2.4.7", + "@heroui/table": "^2.2.9", + "@heroui/theme": "2.4.6", + "@hookform/resolvers": "^4.0.0", + "@react-aria/visually-hidden": "3.8.19", + "@react-types/shared": "3.27.0", + "axios": "^1.7.9", + "clsx": "2.1.1", + "date-fns": "^4.1.0", + "framer-motion": "12.4.1", + "i18next": "^24.2.2", + "i18next-browser-languagedetector": "^8.0.3", + "nanoid": "^5.0.9", + "react": "18.3.1", + "react-country-flag": "^3.1.0", + "react-dom": "18.3.1", + "react-hook-form": "^7.54.2", + "react-i18next": "^15.4.1", + "react-icons": "^5.4.0", + "react-router-dom": "6.23.0", + "sonner": "^1.7.4", + "tailwind-variants": "0.3.1", + "tailwindcss": "3.4.16", + "zod": "^3.24.1" + }, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/node": "22.13.1", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "@typescript-eslint/eslint-plugin": "8.23.0", + "@typescript-eslint/parser": "8.23.0", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "10.4.20", + "eslint": "^8.57.0", + "eslint-config-prettier": "9.1.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.10.2", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "5.2.3", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-unused-imports": "4.1.4", + "orval": "^7.5.0", + "postcss": "8.5.1", + "prettier": "3.4.2", + "typescript": "5.7.3", + "vite": "^6.1.0", + "vite-tsconfig-paths": "^4.3.2" + } +} \ No newline at end of file diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/web/routes.json b/apps/web/routes.json new file mode 100644 index 0000000..ca9ef55 --- /dev/null +++ b/apps/web/routes.json @@ -0,0 +1 @@ +{"openapi":"3.0.3","info":{"title":"🌴 Palmr. API","description":"API documentation for Palmr file sharing system","version":"1.0.0"},"components":{"schemas":{"def-0":{"type":"string","format":"date-time","title":"dateFormat"}}},"paths":{"/auth/login":{"post":{"operationId":"login","summary":"Login","tags":["Authentication"],"description":"Performs login and returns user data","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"User email"},"password":{"type":"string","minLength":8,"description":"User password"}},"required":["email","password"],"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}},"required":["user"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/auth/logout":{"post":{"operationId":"logout","summary":"Logout","tags":["Authentication"],"description":"Performs logout by clearing the token cookie","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Logout message"}},"required":["message"],"additionalProperties":false}}}}}}},"/auth/forgot-password":{"post":{"operationId":"requestPasswordReset","summary":"Request Password Reset","tags":["Authentication"],"description":"Request password reset email","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"User email"}},"required":["email"],"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Reset password email sent"}},"required":["message"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/auth/reset-password":{"post":{"operationId":"resetPassword","summary":"Reset Password","tags":["Authentication"],"description":"Reset password using token","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"string","minLength":1,"description":"Reset password token"},"password":{"type":"string","minLength":8,"description":"User password"}},"required":["token","password"],"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Reset password message"}},"required":["message"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/auth/me":{"get":{"operationId":"getCurrentUser","summary":"Get Current User","tags":["Authentication"],"description":"Returns the current authenticated user's information","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}},"required":["user"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/auth/register":{"post":{"operationId":"registerUser","summary":"Register New User","tags":["User"],"description":"Register a new user (admin only)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"firstName":{"type":"string","minLength":1,"description":"User first name"},"lastName":{"type":"string","minLength":1,"description":"User last name"},"username":{"type":"string","minLength":3,"description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","description":"User profile image URL"},"password":{"type":"string","minLength":8,"description":"User password"}},"required":["firstName","lastName","username","email","password"],"additionalProperties":false}}},"required":true},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false},"message":{"type":"string","description":"User registration message"}},"required":["user","message"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/users":{"get":{"operationId":"listUsers","summary":"List All Users","tags":["User"],"description":"List all users (admin only)","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"put":{"operationId":"updateUser","summary":"Update User Data","tags":["User"],"description":"Update user data (admin only)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"firstName":{"type":"string","minLength":1},"lastName":{"type":"string","minLength":1},"username":{"type":"string","minLength":3},"email":{"type":"string","format":"email"},"image":{"type":"string"},"password":{"type":"string","minLength":8,"description":"User password"},"isAdmin":{"type":"boolean"}},"required":["id"],"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/users/{id}":{"get":{"operationId":"getUserById","summary":"Get User by ID","tags":["User"],"description":"Get a user by ID (admin only)","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"User ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"deleteUser","summary":"Delete User","tags":["User"],"description":"Delete a user (admin only)","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"User ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/users/{id}/activate":{"patch":{"operationId":"activateUser","summary":"Activate User","tags":["User"],"description":"Activate a user (admin only)","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"User ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/users/{id}/deactivate":{"patch":{"operationId":"deactivateUser","summary":"Deactivate User","tags":["User"],"description":"Deactivate a user (admin only)","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"User ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/users/{id}/image":{"patch":{"operationId":"updateUserImage","summary":"Update User Image","tags":["User"],"description":"Update user profile image (admin only)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"image":{"type":"string","format":"uri","description":"User profile image URL"}},"required":["image"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"User ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"User ID"},"firstName":{"type":"string","description":"User first name"},"lastName":{"type":"string","description":"User last name"},"username":{"type":"string","description":"User username"},"email":{"type":"string","format":"email","description":"User email"},"image":{"type":"string","nullable":true,"description":"User profile image URL"},"isAdmin":{"type":"boolean","description":"User is admin"},"isActive":{"type":"boolean","description":"User is active"},"createdAt":{"type":"string","format":"date-time","description":"User creation date"},"updatedAt":{"type":"string","format":"date-time","description":"User last update date"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/users/avatar":{"post":{"operationId":"uploadAvatar","summary":"Upload user avatar","tags":["User"],"description":"Upload and update user profile image","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"description":"Image file (JPG, PNG, GIF)"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"image":{"type":"string","nullable":true},"isAdmin":{"type":"boolean"},"isActive":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"removeAvatar","summary":"Remove user avatar","tags":["User"],"description":"Remove user profile image","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"username":{"type":"string"},"email":{"type":"string"},"image":{"type":"string","nullable":true},"isAdmin":{"type":"boolean"},"isActive":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","firstName","lastName","username","email","image","isAdmin","isActive","createdAt","updatedAt"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/files/presigned-url":{"get":{"operationId":"getPresignedUrl","summary":"Get Presigned URL","tags":["File"],"description":"Generates a pre-signed URL for direct upload to MinIO","parameters":[{"schema":{"type":"string","minLength":1},"in":"query","name":"filename","required":true,"description":"The filename of the file"},{"schema":{"type":"string","minLength":1},"in":"query","name":"extension","required":true,"description":"The extension of the file"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"The pre-signed URL"},"objectName":{"type":"string","description":"The object name of the file"}},"required":["url","objectName"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/files":{"post":{"operationId":"registerFile","summary":"Register File Metadata","tags":["File"],"description":"Registers file metadata in the database","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"extension":{"type":"string","minLength":1},"size":{"type":"number"},"objectName":{"type":"string","minLength":1}},"required":["name","extension","size","objectName"],"additionalProperties":false}}},"required":true},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The object name of the file"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","format":"date-time","description":"The file creation date"},"updatedAt":{"type":"string","format":"date-time","description":"The file last update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false},"message":{"type":"string","description":"The file registration message"}},"required":["file","message"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"get":{"operationId":"listFiles","summary":"List Files","tags":["File"],"description":"Lists user files","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The object name of the file"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","format":"date-time","description":"The file creation date"},"updatedAt":{"type":"string","format":"date-time","description":"The file last update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}}},"required":["files"],"additionalProperties":false}}}},"500":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/files/{objectName}/download":{"get":{"operationId":"getDownloadUrl","summary":"Get Download URL","tags":["File"],"description":"Generates a pre-signed URL for downloading a private file","parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"objectName","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","description":"The download URL"},"expiresIn":{"type":"number","description":"The expiration time in seconds"}},"required":["url","expiresIn"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/files/{id}":{"delete":{"operationId":"deleteFile","summary":"Delete File","tags":["File"],"description":"Deletes a user file","parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"id","required":true,"description":"The file ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"The file deletion message"}},"required":["message"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"operationId":"updateFile","summary":"Update File Metadata","tags":["File"],"description":"Updates file metadata in the database","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"}},"additionalProperties":false}}}},"parameters":[{"schema":{"type":"string","minLength":1},"in":"path","name":"id","required":true,"description":"The file ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"file":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The object name of the file"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","format":"date-time","description":"The file creation date"},"updatedAt":{"type":"string","format":"date-time","description":"The file last update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false},"message":{"type":"string","description":"Success message"}},"required":["file","message"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares":{"post":{"operationId":"createShare","summary":"Create a new share","tags":["Share"],"description":"Create a new share","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"The share name"},"description":{"type":"string","description":"The share description"},"expiration":{"type":"string","format":"date-time"},"files":{"type":"array","items":{"type":"string"},"description":"The file IDs"},"password":{"type":"string","description":"The share password"},"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"recipients":{"type":"array","items":{"type":"string","format":"email"},"description":"The recipient emails"}},"required":["files"],"additionalProperties":false}}},"required":true},"responses":{"201":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"put":{"operationId":"updateShare","summary":"Update a share","tags":["Share"],"description":"Update a share","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"},"expiration":{"type":"string","format":"date-time"},"password":{"type":"string"},"maxViews":{"type":"number","nullable":true},"recipients":{"type":"array","items":{"type":"string","format":"email"}}},"required":["id"],"additionalProperties":false}}},"required":true},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/me":{"get":{"operationId":"listUserShares","summary":"List all shares created by the authenticated user","tags":["Share"],"description":"List all shares created by the authenticated user","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"shares":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}}},"required":["shares"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/{shareId}":{"get":{"operationId":"getShare","summary":"Get a share by ID","tags":["Share"],"description":"Get a share by ID","parameters":[{"schema":{"type":"string"},"in":"query","name":"password","required":false,"description":"The share password"},{"schema":{"type":"string"},"in":"path","name":"shareId","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/{id}":{"delete":{"operationId":"deleteShare","summary":"Delete a share","tags":["Share"],"description":"Delete a share","parameters":[{"schema":{"type":"string"},"in":"path","name":"id","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/{shareId}/password":{"patch":{"operationId":"updateSharePassword","summary":"Update share password","tags":["Share"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"password":{"type":"string","nullable":true,"description":"The new password. Send null to remove password"}},"required":["password"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"shareId","required":true}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/{shareId}/files":{"post":{"operationId":"addFiles","summary":"Add files to share","tags":["Share"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"string","minLength":1,"description":"The file IDs"}}},"required":["files"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"shareId","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"removeFiles","summary":"Remove files from share","tags":["Share"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"files":{"type":"array","items":{"type":"string","minLength":1,"description":"The file IDs"}}},"required":["files"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"shareId","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/{shareId}/recipients":{"post":{"operationId":"addRecipients","summary":"Add recipients to a share","tags":["Share"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"emails":{"type":"array","items":{"type":"string","format":"email","description":"The recipient emails"}}},"required":["emails"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"shareId","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"removeRecipients","summary":"Remove recipients from a share","tags":["Share"],"description":"Remove recipients from a share","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"emails":{"type":"array","items":{"type":"string","format":"email","description":"The recipient emails"}}},"required":["emails"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"shareId","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/{shareId}/alias":{"post":{"operationId":"createShareAlias","summary":"Create or update share alias","tags":["Share"],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"alias":{"type":"string","pattern":"^[a-zA-Z0-9]+$","minLength":3,"maxLength":30}},"required":["alias"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"shareId","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false}},"required":["alias"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/alias/{alias}":{"get":{"operationId":"getShareByAlias","summary":"Get share by alias","tags":["Share"],"parameters":[{"schema":{"type":"string"},"in":"query","name":"password","required":false,"description":"The share password"},{"schema":{"type":"string"},"in":"path","name":"alias","required":true,"description":"The share alias"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"share":{"type":"object","properties":{"id":{"type":"string","description":"The share ID"},"name":{"type":"string","nullable":true,"description":"The share name"},"description":{"type":"string","nullable":true,"description":"The share description"},"expiration":{"type":"string","nullable":true,"description":"The share expiration date"},"views":{"type":"number","description":"The number of views"},"createdAt":{"type":"string","description":"The share creation date"},"updatedAt":{"type":"string","description":"The share update date"},"creatorId":{"type":"string","description":"The creator ID"},"security":{"type":"object","properties":{"maxViews":{"type":"number","nullable":true,"description":"The maximum number of views"},"hasPassword":{"type":"boolean","description":"Whether the share has a password"}},"required":["maxViews","hasPassword"],"additionalProperties":false},"files":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The file ID"},"name":{"type":"string","description":"The file name"},"description":{"type":"string","nullable":true,"description":"The file description"},"extension":{"type":"string","description":"The file extension"},"size":{"type":"string","description":"The file size"},"objectName":{"type":"string","description":"The file object name"},"userId":{"type":"string","description":"The user ID"},"createdAt":{"type":"string","description":"The file creation date"},"updatedAt":{"type":"string","description":"The file update date"}},"required":["id","name","description","extension","size","objectName","userId","createdAt","updatedAt"],"additionalProperties":false}},"recipients":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The recipient ID"},"email":{"type":"string","format":"email","description":"The recipient email"},"createdAt":{"type":"string","description":"The recipient creation date"},"updatedAt":{"type":"string","description":"The recipient update date"}},"required":["id","email","createdAt","updatedAt"],"additionalProperties":false}},"alias":{"type":"object","properties":{"id":{"type":"string"},"alias":{"type":"string"},"shareId":{"type":"string"},"createdAt":{"type":"string"},"updatedAt":{"type":"string"}},"required":["id","alias","shareId","createdAt","updatedAt"],"additionalProperties":false,"nullable":true}},"required":["id","name","description","expiration","views","createdAt","updatedAt","creatorId","security","files","recipients","alias"],"additionalProperties":false}},"required":["share"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/shares/{shareId}/notify":{"post":{"operationId":"notifyRecipients","summary":"Send email notification to share recipients","tags":["Share"],"description":"Send email notification with share link to all recipients","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"shareLink":{"type":"string","format":"uri","description":"The frontend share URL"}},"required":["shareLink"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"shareId","required":true,"description":"The share ID"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Success message"},"notifiedRecipients":{"type":"array","items":{"type":"string"},"description":"List of notified email addresses"}},"required":["message","notifiedRecipients"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}},"required":["error"],"additionalProperties":false}}}}}}},"/storage/disk-space":{"get":{"operationId":"getDiskSpace","summary":"Get server disk space information","tags":["Storage"],"description":"Get server disk space information","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"diskSizeGB":{"type":"number","description":"The server disk size in GB"},"diskUsedGB":{"type":"number","description":"The server disk used in GB"},"diskAvailableGB":{"type":"number","description":"The server disk available in GB"},"uploadAllowed":{"type":"boolean","description":"Whether file upload is allowed"}},"required":["diskSizeGB","diskUsedGB","diskAvailableGB","uploadAllowed"],"additionalProperties":false}}}},"500":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/storage/check-upload":{"get":{"operationId":"checkUploadAllowed","summary":"Check if file upload is allowed","tags":["Storage"],"description":"Check if file upload is allowed based on available space (fileSize in bytes)","parameters":[{"schema":{"type":"string"},"in":"query","name":"fileSize","required":true,"description":"The file size in bytes"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"diskSizeGB":{"type":"number","description":"The server disk size in GB"},"diskUsedGB":{"type":"number","description":"The server disk used in GB"},"diskAvailableGB":{"type":"number","description":"The server disk available in GB"},"uploadAllowed":{"type":"boolean","description":"Whether file upload is allowed"},"fileSizeInfo":{"type":"object","properties":{"bytes":{"type":"number"},"kb":{"type":"number"},"mb":{"type":"number"},"gb":{"type":"number"}},"required":["bytes","kb","mb","gb"],"additionalProperties":false}},"required":["diskSizeGB","diskUsedGB","diskAvailableGB","uploadAllowed","fileSizeInfo"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"500":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/app/info":{"get":{"operationId":"getAppInfo","summary":"Get application base information","tags":["App"],"description":"Get application base information","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"appName":{"type":"string","description":"The application name"},"appDescription":{"type":"string","description":"The application description"},"appLogo":{"type":"string","description":"The application logo"}},"required":["appName","appDescription","appLogo"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/app/configs/{key}":{"patch":{"operationId":"updateConfig","summary":"Update a configuration value","tags":["App"],"description":"Update a configuration value (admin only)","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"value":{"type":"string","description":"The config value"}},"required":["value"],"additionalProperties":false}}},"required":true},"parameters":[{"schema":{"type":"string"},"in":"path","name":"key","required":true,"description":"The config key"}],"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","properties":{"key":{"type":"string","description":"The config key"},"value":{"type":"string","description":"The config value"},"type":{"type":"string","description":"The config type"},"group":{"type":"string","description":"The config group"},"updatedAt":{"type":"string","format":"date-time","description":"The config update date"}},"required":["key","value","type","group","updatedAt"],"additionalProperties":false}},"required":["config"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"404":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/app/configs":{"get":{"operationId":"getAllConfigs","summary":"List all configurations","tags":["App"],"description":"List all configurations (admin only)","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"configs":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"The config key"},"value":{"type":"string","description":"The config value"},"type":{"type":"string","description":"The config type"},"group":{"type":"string","description":"The config group"},"updatedAt":{"type":"string","format":"date-time","description":"The config update date"}},"required":["key","value","type","group","updatedAt"],"additionalProperties":false}}},"required":["configs"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"patch":{"operationId":"bulkUpdateConfigs","summary":"Bulk update configuration values","tags":["App"],"description":"Bulk update configuration values (admin only)","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"The config key"},"value":{"type":"string","description":"The config value"}},"required":["key","value"],"additionalProperties":false}}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"configs":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"The config key"},"value":{"type":"string","description":"The config value"},"type":{"type":"string","description":"The config type"},"group":{"type":"string","description":"The config group"},"updatedAt":{"type":"string","format":"date-time","description":"The config update date"}},"required":["key","value","type","group","updatedAt"],"additionalProperties":false}}},"required":["configs"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/app/logo":{"post":{"operationId":"uploadLogo","summary":"Upload app logo","tags":["App"],"description":"Upload a new app logo (admin only)","requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"description":"Image file (JPG, PNG, GIF)"}},"additionalProperties":false}}}},"responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"logo":{"type":"string","description":"The logo URL"}},"required":["logo"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}},"delete":{"operationId":"removeLogo","summary":"Remove app logo","tags":["App"],"description":"Remove the current app logo (admin only)","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string","description":"Success message"}},"required":["message"],"additionalProperties":false}}}},"400":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}},"403":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","description":"Error message"}},"required":["error"],"additionalProperties":false}}}}}}},"/health":{"get":{"operationId":"checkHealth","summary":"Check API Health","tags":["Health"],"description":"Returns the health status of the API","responses":{"200":{"description":"Default Response","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","description":"The health status"},"timestamp":{"type":"string","description":"The timestamp of the health check"}},"required":["status","timestamp"],"additionalProperties":false}}}}}}}},"tags":[{"name":"Health","description":"Health check endpoints"},{"name":"Authentication","description":"Authentication related endpoints"},{"name":"User","description":"User management endpoints"},{"name":"File","description":"File management endpoints"},{"name":"Share","description":"File sharing endpoints"},{"name":"Storage","description":"Storage management endpoints"},{"name":"App","description":"Application configuration endpoints"}]} \ No newline at end of file diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx new file mode 100644 index 0000000..f4f6754 --- /dev/null +++ b/apps/web/src/App.tsx @@ -0,0 +1,85 @@ +import { FilesPage } from "./pages/files/page"; +import { ForgotPasswordPage } from "./pages/forgot-password/page"; +import { ResetPasswordPage } from "./pages/reset-password/page"; +import { SettingsPage } from "./pages/settings/page"; +import { PublicSharePage } from "./pages/share/[alias]/page"; +import { SharesPage } from "./pages/shares/page"; +import { AdminProtectedRoute } from "@/components/route-protector/admin-protected-route"; +import { ProtectedRoute } from "@/components/route-protector/protected-route"; +import { DashboardPage } from "@/pages/dashboard/page"; +import { HomePage } from "@/pages/home/page"; +import { LoginPage } from "@/pages/login/page"; +import { ProfilePage } from "@/pages/profile/page"; +import { AdminAreaPage } from "@/pages/users-management/page"; +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { Route, Routes } from "react-router-dom"; + +function App() { + const { i18n } = useTranslation(); + + useEffect(() => { + document.documentElement.dir = i18n.language === "ar-SA" ? "rtl" : "ltr"; + document.documentElement.lang = i18n.language; + }, [i18n.language]); + + return ( + + } path="/" /> + } path="/login" /> + + + + } + path="/dashboard" + /> + + + + } + path="/profile" + /> + + + + } + path="/settings" + /> + + + + } + path="/admin" + /> + + + + } + path="/files" + /> + + + + } + path="/shares" + /> + } path="/s/:alias" /> + } path="/forgot-password" /> + } path="/reset-password" /> + + ); +} + +export default App; diff --git a/apps/web/src/components/general/file-selector.tsx b/apps/web/src/components/general/file-selector.tsx new file mode 100644 index 0000000..ca5dff5 --- /dev/null +++ b/apps/web/src/components/general/file-selector.tsx @@ -0,0 +1,178 @@ +import { addFiles, listFiles, removeFiles } from "@/http/endpoints"; +import { Button } from "@heroui/button"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { FaArrowLeft, FaArrowRight, FaFile } from "react-icons/fa"; +import { toast } from "sonner"; + +interface FileSelectorProps { + shareId: string; + selectedFiles: string[]; + onSave: (files: string[]) => Promise; +} + +export function FileSelector({ shareId, selectedFiles, onSave }: FileSelectorProps) { + const { t } = useTranslation(); + const [availableFiles, setAvailableFiles] = useState([]); + const [shareFiles, setShareFiles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [availableFilter, setAvailableFilter] = useState(""); + const [shareFilter, setShareFilter] = useState(""); + + useEffect(() => { + loadFiles(); + }, [shareId, selectedFiles]); + + const loadFiles = async () => { + try { + const response = await listFiles(); + const allFiles = response.data.files || []; + + setShareFiles(allFiles.filter((file) => selectedFiles.includes(file.id))); + setAvailableFiles(allFiles.filter((file) => !selectedFiles.includes(file.id))); + } catch (error) { + console.error(error); + toast.error("Failed to load files"); + } + }; + + const moveToShare = (fileId: string) => { + const file = availableFiles.find((f) => f.id === fileId); + + if (file) { + setShareFiles([...shareFiles, file]); + setAvailableFiles(availableFiles.filter((f) => f.id !== fileId)); + } + }; + + const removeFromShare = (fileId: string) => { + const file = shareFiles.find((f) => f.id === fileId); + + if (file) { + setAvailableFiles([...availableFiles, file]); + setShareFiles(shareFiles.filter((f) => f.id !== fileId)); + } + }; + + const handleSave = async () => { + try { + setIsLoading(true); + + const filesToAdd = shareFiles.filter((file) => !selectedFiles.includes(file.id)).map((file) => file.id); + + const filesToRemove = selectedFiles.filter((fileId) => !shareFiles.find((f) => f.id === fileId)); + + if (filesToAdd.length > 0) { + await addFiles(shareId, { files: filesToAdd }); + } + + if (filesToRemove.length > 0) { + await removeFiles(shareId, { files: filesToRemove }); + } + + await onSave(shareFiles.map((f) => f.id)); + toast.success("Files updated successfully"); + } catch (error) { + console.error(error); + toast.error("Failed to update files"); + } finally { + setIsLoading(false); + } + }; + + const filteredAvailableFiles = availableFiles.filter((file) => + file.name.toLowerCase().includes(availableFilter.toLowerCase()) + ); + + const filteredShareFiles = shareFiles.filter((file) => file.name.toLowerCase().includes(shareFilter.toLowerCase())); + + return ( +
+
+
+
+

+ {t("fileSelector.availableFiles", { count: filteredAvailableFiles.length })} +

+ setAvailableFilter(e.target.value)} + /> +
+
+ {filteredAvailableFiles.length === 0 ? ( +
+ {availableFilter ? t("fileSelector.noMatchingFiles") : t("fileSelector.noAvailableFiles")} +
+ ) : ( +
+ {filteredAvailableFiles.map((file) => ( +
moveToShare(file.id)} + > +
+ + + {file.name} + +
+ +
+ ))} +
+ )} +
+
+ +
+
+

{t("fileSelector.shareFiles", { count: filteredShareFiles.length })}

+ setShareFilter(e.target.value)} + /> +
+
+ {filteredShareFiles.length === 0 ? ( +
+ {shareFilter ? t("fileSelector.noMatchingFiles") : t("fileSelector.noFilesInShare")} +
+ ) : ( +
+ {filteredShareFiles.map((file) => ( +
removeFromShare(file.id)} + > +
+ + + {file.name} + +
+ +
+ ))} +
+ )} +
+
+
+ +
+ +
+
+ ); +} diff --git a/apps/web/src/components/general/language-switcher.tsx b/apps/web/src/components/general/language-switcher.tsx new file mode 100644 index 0000000..d09dab9 --- /dev/null +++ b/apps/web/src/components/general/language-switcher.tsx @@ -0,0 +1,54 @@ +import { Button } from "@heroui/button"; +import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from "@heroui/dropdown"; +import ReactCountryFlag from "react-country-flag"; +import { useTranslation } from "react-i18next"; +import { FaGlobe } from "react-icons/fa"; + +const languages = { + "en-US": "English", + "pt-BR": "Português", + "fr-FR": "Français", + "es-ES": "Español", + "de-DE": "Deutsch", + "tr-TR": "Türkçe (Turkish)", + "ru-RU": "Русский (Russian)", + "hi-IN": "हिन्दी (Hindi)", + "ar-SA": "العربية (Arabic)", + "zh-CN": "中文 (Chinese)", + "ja-JP": "日本語 (Japanese)", + "ko-KR": "한국어 (Korean)", +}; + +export function LanguageSwitcher() { + const { i18n } = useTranslation(); + + const changeLanguage = (lng: string) => { + i18n.changeLanguage(lng); + }; + + return ( + + + + + + {Object.entries(languages).map(([code, name]) => ( + changeLanguage(code)}> + + {name} + + ))} + + + ); +} diff --git a/apps/web/src/components/general/recipient-selector.tsx b/apps/web/src/components/general/recipient-selector.tsx new file mode 100644 index 0000000..6aacaf5 --- /dev/null +++ b/apps/web/src/components/general/recipient-selector.tsx @@ -0,0 +1,133 @@ +import { useShareContext } from "@/contexts/ShareContext"; +import { addRecipients, removeRecipients, notifyRecipients } from "@/http/endpoints"; +import { Button } from "@heroui/button"; +import { Input } from "@heroui/input"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { FaPlus, FaTrash, FaEnvelope, FaBell } from "react-icons/fa"; +import { toast } from "sonner"; + +interface Recipient { + id: string; + email: string; + createdAt: string; + updatedAt: string; +} + +interface RecipientSelectorProps { + shareId: string; + selectedRecipients: Recipient[]; + shareAlias?: string; + onSuccess: () => void; +} + +export function RecipientSelector({ shareId, selectedRecipients, shareAlias, onSuccess }: RecipientSelectorProps) { + const { t } = useTranslation(); + const { smtpEnabled } = useShareContext(); + const [recipients, setRecipients] = useState(selectedRecipients?.map((recipient) => recipient.email) || []); + const [newRecipient, setNewRecipient] = useState(""); + + useEffect(() => { + setRecipients(selectedRecipients?.map((recipient) => recipient.email) || []); + }, [selectedRecipients]); + + const handleAddRecipient = () => { + if (newRecipient && !recipients.includes(newRecipient)) { + addRecipients(shareId, { emails: [newRecipient] }) + .then(() => { + setRecipients([...recipients, newRecipient]); + setNewRecipient(""); + toast.success(t("recipientSelector.addSuccess")); + onSuccess(); + }) + .catch(() => { + toast.error(t("recipientSelector.addError")); + }); + } + }; + + const handleRemoveRecipient = (email: string) => { + removeRecipients(shareId, { emails: [email] }) + .then(() => { + setRecipients(recipients.filter((r) => r !== email)); + toast.success(t("recipientSelector.removeSuccess")); + onSuccess(); + }) + .catch(() => { + toast.error(t("recipientSelector.removeError")); + }); + }; + + const handleNotifyRecipients = async () => { + if (!shareAlias) return; + + const link = `${window.location.origin}/s/${shareAlias}`; + const loadingToast = toast.loading(t("recipientSelector.sendingNotifications")); + + try { + await notifyRecipients(shareId, { shareLink: link }); + toast.dismiss(loadingToast); + toast.success(t("recipientSelector.notifySuccess")); + } catch (error) { + console.error(error); + toast.dismiss(loadingToast); + toast.error(t("recipientSelector.notifyError")); + } + }; + + return ( +
+
+ } + value={newRecipient} + onChange={(e) => setNewRecipient(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && handleAddRecipient()} + /> + +
+ +
+
+

{t("recipientSelector.recipients", { count: recipients.length })}

+ {recipients.length > 0 && shareAlias && smtpEnabled === "true" && ( + + )} +
+
+
+ {recipients.length === 0 ? ( +
{t("recipientSelector.noRecipients")}
+ ) : ( + recipients.map((email, index) => ( +
+
+ + {email} +
+ +
+ )) + )} +
+
+
+
+ ); +} diff --git a/apps/web/src/components/general/theme-switch.tsx b/apps/web/src/components/general/theme-switch.tsx new file mode 100644 index 0000000..a5e156b --- /dev/null +++ b/apps/web/src/components/general/theme-switch.tsx @@ -0,0 +1,29 @@ +import { useTheme } from "@/hooks/use-theme"; +import { Button } from "@heroui/button"; +import { FC, useEffect, useState } from "react"; +import { BsMoonFill, BsSunFill } from "react-icons/bs"; + +export const ThemeSwitch: FC = () => { + const [isMounted, setIsMounted] = useState(false); + const { theme, toggleTheme } = useTheme(); + const isDark = theme === "dark"; + + useEffect(() => { + setIsMounted(true); + }, []); + + if (!isMounted) return
; + + return ( + + ); +}; diff --git a/apps/web/src/components/layout/file-manager-layout.tsx b/apps/web/src/components/layout/file-manager-layout.tsx new file mode 100644 index 0000000..ca1d88b --- /dev/null +++ b/apps/web/src/components/layout/file-manager-layout.tsx @@ -0,0 +1,56 @@ +import { Navbar } from "@/components/layout/navbar"; +import { DefaultFooter } from "@/components/ui/default-footer"; +import { Breadcrumbs, BreadcrumbItem } from "@heroui/breadcrumbs"; +import { Divider } from "@heroui/divider"; +import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { TbLayoutDashboardFilled } from "react-icons/tb"; + +interface FileManagerLayoutProps { + children: ReactNode; + title: string; + icon: ReactNode; + breadcrumbLabel?: string; + showBreadcrumb?: boolean; +} + +export function FileManagerLayout({ + children, + title, + icon, + breadcrumbLabel, + showBreadcrumb = true, +}: FileManagerLayoutProps) { + const { t } = useTranslation(); + + return ( +
+ +
+
+
+
+ {icon} +

{title}

+
+ + {showBreadcrumb && breadcrumbLabel && ( + + + + {t("navigation.dashboard")} + + + {icon} {breadcrumbLabel} + + + )} +
+ + {children} +
+
+ +
+ ); +} diff --git a/apps/web/src/components/layout/loading-screen.tsx b/apps/web/src/components/layout/loading-screen.tsx new file mode 100644 index 0000000..65793c4 --- /dev/null +++ b/apps/web/src/components/layout/loading-screen.tsx @@ -0,0 +1,28 @@ +import { GridPattern } from "@/components/ui/grid-pattern"; +import { BackgroundLights } from "@/pages/home/components/background-lights"; +import { motion } from "framer-motion"; +import { useTranslation } from "react-i18next"; + +export function LoadingScreen() { + const { t } = useTranslation(); + + return ( +
+ + +
+ + {t("common.loading")} + +
+
+ ); +} diff --git a/apps/web/src/components/layout/navbar.tsx b/apps/web/src/components/layout/navbar.tsx new file mode 100644 index 0000000..d276cf5 --- /dev/null +++ b/apps/web/src/components/layout/navbar.tsx @@ -0,0 +1,104 @@ +import { LanguageSwitcher } from "../general/language-switcher"; +import { ThemeSwitch } from "@/components/general/theme-switch"; +import { useAppInfo } from "@/contexts/app-info-context"; +import { useAuth } from "@/contexts/auth-context"; +import { logout as logoutAPI } from "@/http/endpoints"; +import { Avatar } from "@heroui/avatar"; +import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from "@heroui/dropdown"; +import { Link } from "@heroui/link"; +import { Navbar as HeroUINavbar, NavbarBrand, NavbarContent, NavbarItem } from "@heroui/navbar"; +import { useTranslation } from "react-i18next"; +import { FaCog, FaSignOutAlt, FaUser, FaUsersCog } from "react-icons/fa"; +import { useNavigate } from "react-router-dom"; + +export function Navbar() { + const { t } = useTranslation(); + const { user, isAdmin, logout } = useAuth(); + const { appName, appLogo } = useAppInfo(); + const navigate = useNavigate(); + + const handleLogout = async () => { + try { + await logoutAPI(); + logout(); + navigate("/login"); + } catch (err) { + console.error("Error logging out:", err); + } + }; + + return ( + + + + + {appLogo && {t("navbar.logoAlt")}} +

{appName}

+ +
+
+ + + + + + + + + + + + +

+ {user?.firstName} {user?.lastName} +

+

{user?.email}

+
+ + + + {t("navbar.profile")} + + + {isAdmin ? ( + + + + {t("navbar.settings")} + + + ) : null} + {isAdmin ? ( + + + + {t("navbar.usersManagement")} + + + ) : null} + + + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/layout/share-manager-layout.tsx b/apps/web/src/components/layout/share-manager-layout.tsx new file mode 100644 index 0000000..c5b91b8 --- /dev/null +++ b/apps/web/src/components/layout/share-manager-layout.tsx @@ -0,0 +1,36 @@ +import { Breadcrumbs, BreadcrumbItem } from "@heroui/breadcrumbs"; +import { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; +import { TbLayoutDashboardFilled } from "react-icons/tb"; + +interface ShareManagerLayoutProps { + children: ReactNode; + icon: ReactNode; + title: string; + breadcrumbLabel: string; +} + +export function ShareManagerLayout({ children, icon, title, breadcrumbLabel }: ShareManagerLayoutProps) { + const { t } = useTranslation(); + + return ( +
+
+ + + + {t("navigation.dashboard")} + + + {icon} {breadcrumbLabel} + + +
+ {icon} +

{title}

+
+
+ {children} +
+ ); +} diff --git a/apps/web/src/components/modals/create-share-modal.tsx b/apps/web/src/components/modals/create-share-modal.tsx new file mode 100644 index 0000000..e2c1842 --- /dev/null +++ b/apps/web/src/components/modals/create-share-modal.tsx @@ -0,0 +1,116 @@ +import { createShare } from "@/http/endpoints"; +import { Button } from "@heroui/button"; +import { Input } from "@heroui/input"; +import { Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@heroui/modal"; +import { Switch } from "@heroui/switch"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +interface CreateShareModalProps { + isOpen: boolean; + onClose: () => void; + onSuccess: () => void; +} + +export function CreateShareModal({ isOpen, onClose, onSuccess }: CreateShareModalProps) { + const { t } = useTranslation(); + const [formData, setFormData] = useState({ + name: "", + password: "", + expiresAt: "", + isPasswordProtected: false, + maxViews: "", + }); + const [isLoading, setIsLoading] = useState(false); + + const handleSubmit = async () => { + try { + setIsLoading(true); + await createShare({ + name: formData.name, + password: formData.isPasswordProtected ? formData.password : undefined, + expiration: formData.expiresAt ? new Date(formData.expiresAt).toISOString() : undefined, + maxViews: formData.maxViews ? parseInt(formData.maxViews) : undefined, + files: [], + }); + toast.success(t("createShare.success")); + onSuccess(); + onClose(); + setFormData({ + name: "", + password: "", + expiresAt: "", + isPasswordProtected: false, + maxViews: "", + }); + } catch (error) { + toast.error(t("createShare.error")); + console.error(error); + } finally { + setIsLoading(false); + } + }; + + return ( + + + {t("createShare.title")} + +
+ setFormData({ ...formData, name: e.target.value })} + /> + setFormData({ ...formData, expiresAt: e.target.value })} + /> + setFormData({ ...formData, maxViews: e.target.value })} + /> +
+ + setFormData({ + ...formData, + isPasswordProtected: checked, + password: "", + }) + } + > + {t("createShare.passwordProtection")} + +
+ {formData.isPasswordProtected && ( + setFormData({ ...formData, password: e.target.value })} + /> + )} +
+
+ + + + +
+
+ ); +} diff --git a/apps/web/src/components/modals/file-actions-modals.tsx b/apps/web/src/components/modals/file-actions-modals.tsx new file mode 100644 index 0000000..5d04c9b --- /dev/null +++ b/apps/web/src/components/modals/file-actions-modals.tsx @@ -0,0 +1,115 @@ +import { Button } from "@heroui/button"; +import { Input } from "@heroui/input"; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal"; +import { useTranslation } from "react-i18next"; + +interface FileActionsModalsProps { + fileToRename: { id: string; name: string; description?: string } | null; + fileToDelete: { id: string; name: string } | null; + onRename: (fileId: string, newName: string, description?: string) => Promise; + onDelete: (fileId: string) => Promise; + onCloseRename: () => void; + onCloseDelete: () => void; +} + +export function FileActionsModals({ + fileToRename, + fileToDelete, + onRename, + onDelete, + onCloseRename, + onCloseDelete, +}: FileActionsModalsProps) { + const { t } = useTranslation(); + + const splitFileName = (fullName: string) => { + const lastDotIndex = fullName.lastIndexOf("."); + + return lastDotIndex === -1 + ? { name: fullName, extension: "" } + : { + name: fullName.substring(0, lastDotIndex), + extension: fullName.substring(lastDotIndex), + }; + }; + + return ( + <> + + + {t("fileActions.editFile")} + + {fileToRename && ( +
+
+ { + if (e.key === "Enter" && fileToRename) { + const newName = e.currentTarget.value + splitFileName(fileToRename.name).extension; + + onRename(fileToRename.id, newName); + } + }} + /> +

+ {t("fileActions.extension")}: {splitFileName(fileToRename.name).extension} +

+
+ +
+ )} +
+ + + + +
+
+ + + + {t("fileActions.deleteFile")} + +

{t("fileActions.deleteConfirmation", { fileName: fileToDelete?.name })}

+

{t("fileActions.deleteWarning")}

+
+ + + + +
+
+ + ); +} diff --git a/apps/web/src/components/modals/file-preview-modal.tsx b/apps/web/src/components/modals/file-preview-modal.tsx new file mode 100644 index 0000000..c8d414b --- /dev/null +++ b/apps/web/src/components/modals/file-preview-modal.tsx @@ -0,0 +1,193 @@ +/* eslint-disable jsx-a11y/media-has-caption */ +import { getDownloadUrl } from "@/http/endpoints"; +import { getFileIcon } from "@/utils/file-icons"; +import { Button } from "@heroui/button"; +import { Image } from "@heroui/image"; +import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@heroui/modal"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { FaDownload } from "react-icons/fa"; +import { toast } from "sonner"; + +interface FilePreviewModalProps { + isOpen: boolean; + onClose: () => void; + file: { + name: string; + objectName: string; + type?: string; + }; +} + +export function FilePreviewModal({ isOpen, onClose, file }: FilePreviewModalProps) { + const { t } = useTranslation(); + const [previewUrl, setPreviewUrl] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (isOpen && file.objectName) { + setIsLoading(true); + setPreviewUrl(null); + loadPreview(); + } + }, [file.objectName, isOpen]); + + useEffect(() => { + return () => { + if (previewUrl) { + URL.revokeObjectURL(previewUrl); + } + }; + }, [previewUrl]); + + const loadPreview = async () => { + if (!file.objectName) return; + + try { + const encodedObjectName = encodeURIComponent(file.objectName); + const response = await getDownloadUrl(encodedObjectName); + + setPreviewUrl(response.data.url); + } catch (error) { + console.error("Failed to load preview:", error); + toast.error(t("filePreview.loadError")); + } finally { + setIsLoading(false); + } + }; + + const handleDownload = async () => { + try { + const encodedObjectName = encodeURIComponent(file.objectName); + const response = await getDownloadUrl(encodedObjectName); + const downloadUrl = response.data.url; + + const fileResponse = await fetch(downloadUrl); + const blob = await fileResponse.blob(); + const url = window.URL.createObjectURL(blob); + + const link = document.createElement("a"); + + link.href = url; + link.download = file.name; + document.body.appendChild(link); + link.click(); + + document.body.removeChild(link); + window.URL.revokeObjectURL(url); + } catch (error) { + toast.error(t("filePreview.downloadError")); + console.error(error); + } + }; + + const getFileType = () => { + const extension = file.name.split(".").pop()?.toLowerCase(); + + if (extension === "pdf") return "pdf"; + if (["jpg", "jpeg", "png", "gif", "webp"].includes(extension || "")) return "image"; + if (["mp3", "wav", "ogg", "m4a"].includes(extension || "")) return "audio"; + if (["mp4", "webm", "ogg", "mov", "avi", "mkv"].includes(extension || "")) return "video"; + + return "other"; + }; + + const renderPreview = () => { + const fileType = getFileType(); + const { icon: FileIcon, color } = getFileIcon(file.name); + + if (isLoading) { + return ( +
+
+

{t("filePreview.loading")}

+
+ ); + } + + if (!previewUrl) { + return ( +
+ +

{t("filePreview.notAvailable")}

+

{t("filePreview.downloadToView")}

+
+ ); + } + + switch (fileType) { + case "pdf": + return ( +
+