diff --git a/.all-contributorsrc b/.all-contributorsrc index 42cc445349..a1361c01ba 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -400,6 +400,15 @@ "contributions": [ "code" ] + }, + { + "login": "armandgillot", + "name": "Armand Gillot", + "avatar_url": "https://avatars.githubusercontent.com/u/79774155?v=4", + "profile": "http://www.armandgillot.fr", + "contributions": [ + "code" + ] } ], "contributorsPerLine": 7, diff --git a/.github/workflows/alpha-release.yml b/.github/workflows/alpha-release.yml index 15b3b58c23..7eda5f5423 100644 --- a/.github/workflows/alpha-release.yml +++ b/.github/workflows/alpha-release.yml @@ -50,6 +50,8 @@ jobs: uses: docker/build-push-action@v5 with: context: . + build-args: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} file: ./packages/worker/Dockerfile platforms: linux/amd64 push: true @@ -58,6 +60,8 @@ jobs: cache-to: type=registry,ref=ghcr.io/${{ github.repository_owner }}/worker:buildcache,mode=max build-images: + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} runs-on: ubuntu-latest needs: create-tag steps: @@ -81,6 +85,8 @@ jobs: uses: docker/build-push-action@v5 with: context: . + build-args: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} platforms: linux/amd64 push: true tags: ghcr.io/${{ github.repository_owner }}/runtipi:${{ needs.create-tag.outputs.tagname }} diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 84080bcd8b..1cca4b2eb2 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -49,6 +49,8 @@ jobs: - name: Build and push images uses: docker/build-push-action@v5 with: + build-args: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} context: . file: ./packages/worker/Dockerfile platforms: linux/amd64,linux/arm64 @@ -80,6 +82,8 @@ jobs: - name: Build and push images uses: docker/build-push-action@v5 with: + build-args: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} context: . platforms: linux/amd64,linux/arm64 push: true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index bd605b301c..9606ca062b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -178,7 +178,7 @@ jobs: path: playwright-report/ - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - name: Upload artifact uses: actions/upload-pages-artifact@v2 @@ -187,7 +187,7 @@ jobs: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v3 teardown: runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 480c5c1c1d..e8d80327c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,8 @@ jobs: uses: docker/build-push-action@v5 with: context: . + build-args: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} platforms: linux/amd64,linux/arm64 push: true tags: ghcr.io/${{ github.repository_owner }}/runtipi:${{ needs.create-tag.outputs.tagname }},ghcr.io/${{ github.repository_owner }}/runtipi:latest @@ -77,6 +79,8 @@ jobs: uses: docker/build-push-action@v5 with: context: . + build-args: | + SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} file: ./packages/worker/Dockerfile platforms: linux/amd64,linux/arm64 push: true diff --git a/.gitignore b/.gitignore index 6765c17b44..97792e72ca 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,6 @@ temp ./traefik/ /user-config/ + +# Sentry Config File +.sentryclirc diff --git a/.prettierrc.js b/.prettierrc.js index 18502e8fec..8a6a2a01e3 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -2,5 +2,5 @@ module.exports = { singleQuote: true, semi: true, trailingComma: 'all', - printWidth: 200, + printWidth: 150, }; diff --git a/Dockerfile b/Dockerfile index f997533bd2..d30337eed7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,6 +27,17 @@ COPY ./next.config.mjs ./next.config.mjs COPY ./public ./public COPY ./tests ./tests +# Sentry +COPY ./sentry.client.config.ts ./sentry.client.config.ts +COPY ./sentry.edge.config.ts ./sentry.edge.config.ts +COPY ./sentry.server.config.ts ./sentry.server.config.ts + +ARG SENTRY_AUTH_TOKEN +ARG SENTRY_DISABLE_AUTO_UPLOAD + +ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} +ENV SENTRY_DISABLE_AUTO_UPLOAD=${SENTRY_DISABLE_AUTO_UPLOAD} + RUN npm run build # APP diff --git a/Dockerfile.dev b/Dockerfile.dev index dcbb58475f..c3819dd024 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -20,4 +20,9 @@ COPY ./tsconfig.json ./tsconfig.json COPY ./next.config.mjs ./next.config.mjs COPY ./public ./public +# Sentry +COPY ./sentry.client.config.ts ./sentry.client.config.ts +COPY ./sentry.edge.config.ts ./sentry.edge.config.ts +COPY ./sentry.server.config.ts ./sentry.server.config.ts + CMD ["npm", "run", "dev"] diff --git a/README.md b/README.md index d2670b7a45..73f309a739 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Tipi — A personal homeserver for everyone -[![All Contributors](https://img.shields.io/badge/all_contributors-42-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-43-orange.svg?style=flat-square)](#contributors-) [![License](https://img.shields.io/github/license/runtipi/runtipi)](https://github.com/runtipi/runtipi/blob/master/LICENSE) @@ -126,6 +126,9 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d Olivier Garcia
Olivier Garcia

💻 qcoudeyr
qcoudeyr

💻 + + Armand Gillot
Armand Gillot

💻 + diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 5f85b16de5..bb42cb26bf 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -60,6 +60,8 @@ services: context: . dockerfile: ./packages/worker/Dockerfile.dev container_name: tipi-worker + ports: + - 3935:3001 healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck'] interval: 5s diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 309eaf22a2..6ea1f7bd33 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -60,6 +60,8 @@ services: context: . dockerfile: ./packages/worker/Dockerfile container_name: tipi-worker + ports: + - 3935:3001 healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck'] interval: 5s @@ -93,6 +95,8 @@ services: build: context: . dockerfile: Dockerfile + args: + - SENTRY_DISABLE_AUTO_UPLOAD=true container_name: tipi-dashboard depends_on: tipi-db: @@ -104,7 +108,7 @@ services: env_file: - .env environment: - NODE_ENV: development + NODE_ENV: production networks: - tipi_main_network ports: diff --git a/next.config.mjs b/next.config.mjs index c8d3b714fa..f0e416a72e 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,3 +1,4 @@ +import { withSentryConfig } from '@sentry/nextjs'; /** @type {import('next').NextConfig} */ const nextConfig = { swcMinify: true, @@ -32,4 +33,21 @@ const nextConfig = { }, }; -export default nextConfig; +export default withSentryConfig( + nextConfig, + { + // https://github.com/getsentry/sentry-webpack-plugin#options + silent: false, + org: 'runtipi', + project: 'runtipi-dashboard', + dryRun: process.env.SENTRY_DISABLE_AUTO_UPLOAD === 'true', + }, + { + // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ + widenClientFileUpload: true, + transpileClientSDK: false, + tunnelRoute: '/errors', + hideSourceMaps: false, + disableLogger: true, + }, +); diff --git a/package.json b/package.json index 7d74a5150d..af501fd6e2 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,16 @@ { "name": "runtipi", - "version": "2.2.1", + "version": "2.3.0", "description": "A homeserver for everyone", "scripts": { "knip": "knip", - "prepare": "mkdir -p state && echo \"{}\" > state/system-info.json && echo \"random-seed\" > state/seed", "test": "dotenv -e .env.test -- jest --colors", "test:e2e": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test", "test:e2e:ui": "NODE_ENV=test dotenv -e .env -e .env.e2e -- playwright test --ui", "test:client": "jest --colors --selectProjects client --", "test:server": "jest --colors --selectProjects server --", "test:vite": "dotenv -e .env.test -- vitest run --coverage", - "dev": "next dev", + "dev": "DEBUG=socket.io:client* next dev", "dev:watcher": "pnpm -r --filter cli dev", "db:migrate": "NODE_ENV=development dotenv -e .env.local -- tsx ./src/server/run-migrations-dev.ts", "lint": "next lint", @@ -20,8 +19,8 @@ "start": "NODE_ENV=production node server.js", "start:dev-container": "./.devcontainer/filewatcher.sh && npm run start:dev", "start:rc": "docker compose -f docker-compose.rc.yml --env-file .env up --build", - "start:dev": "npm run prepare && docker compose -f docker-compose.dev.yml up --build", - "start:prod": "npm run prepare && docker compose --env-file ./.env -f docker-compose.prod.yml up --build", + "start:dev": "docker compose -f docker-compose.dev.yml up --build", + "start:prod": "docker compose --env-file ./.env -f docker-compose.prod.yml up --build", "start:pg": "docker run --name test-db -p 5433:5432 -d --rm -e POSTGRES_PASSWORD=postgres postgres:14", "version": "echo $npm_package_version", "release:rc": "./scripts/deploy/release-rc.sh", @@ -44,13 +43,14 @@ "@radix-ui/react-tabs": "^1.0.4", "@runtipi/postgres-migrations": "^5.3.0", "@runtipi/shared": "workspace:^", + "@sentry/nextjs": "^7.86.0", "@tabler/core": "1.0.0-beta20", "@tabler/icons-react": "^2.42.0", "argon2": "^0.31.2", "bullmq": "^4.13.0", "clsx": "^2.0.0", "connect-redis": "^7.1.0", - "drizzle-orm": "^0.28.6", + "drizzle-orm": "^0.29.1", "fs-extra": "^11.1.1", "geist": "^1.2.0", "let-it-go": "^1.0.0", @@ -65,7 +65,7 @@ "react-dom": "18.2.0", "react-hook-form": "^7.48.2", "react-hot-toast": "^2.4.1", - "react-markdown": "^9.0.0", + "react-markdown": "^9.0.1", "react-select": "^5.8.0", "react-tooltip": "^5.25.0", "redaxios": "^0.5.1", @@ -76,6 +76,7 @@ "sass": "^1.69.5", "semver": "^7.5.4", "sharp": "0.32.6", + "socket.io-client": "^4.7.2", "swr": "^2.2.4", "tslib": "^2.6.2", "uuid": "^9.0.1", @@ -98,7 +99,7 @@ "@types/jest": "^29.5.11", "@types/lodash.merge": "^4.6.8", "@types/node": "20.8.10", - "@types/pg": "^8.10.7", + "@types/pg": "^8.10.9", "@types/react": "18.2.39", "@types/react-dom": "18.2.14", "@types/semver": "^7.5.4", @@ -109,7 +110,7 @@ "@vitejs/plugin-react": "^4.1.1", "@vitest/coverage-v8": "^0.34.6", "dotenv-cli": "^7.3.0", - "eslint": "8.52.0", + "eslint": "8.55.0", "eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb-typescript": "^17.1.0", "eslint-config-next": "14.0.3", @@ -124,14 +125,14 @@ "eslint-plugin-testing-library": "^6.1.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", - "knip": "^2.41.3", + "knip": "^3.6.1", "memfs": "^4.6.0", "msw": "^1.3.2", "next-router-mock": "^0.9.10", "prettier": "^3.0.3", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "tsx": "^3.14.0", + "tsx": "^4.6.2", "typescript": "5.2.2", "vite-tsconfig-paths": "^4.2.1", "vitest": "^0.34.6", diff --git a/packages/cli/assets/docker-compose.yml b/packages/cli/assets/docker-compose.yml index 58b3d5d0c2..2602db4453 100644 --- a/packages/cli/assets/docker-compose.yml +++ b/packages/cli/assets/docker-compose.yml @@ -60,6 +60,8 @@ services: container_name: tipi-worker image: ghcr.io/runtipi/worker:${TIPI_VERSION} restart: unless-stopped + ports: + - 3935:3001 healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:3000/healthcheck'] interval: 5s diff --git a/packages/cli/package.json b/packages/cli/package.json index 00857b567c..7c8411a232 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -8,10 +8,10 @@ "test": "dotenv -e .env.test vitest -- --coverage --watch=false --passWithNoTests", "test:watch": "dotenv -e .env.test vitest", "package": "npm run build && pkg package.json && chmod +x dist/bin/cli-x64 && chmod +x dist/bin/cli-arm64", - "package:m1": "npm run build && pkg package.json -t node20-darwin-arm64", + "package:m1": "npm run build && pkg package.json -t node18-darwin-arm64", "set-version": "node -e \"require('fs').writeFileSync('assets/VERSION', process.argv[1])\"", "build": "node build.js", - "build:meta": "esbuild ./src/index.ts --bundle --platform=node --target=node20 --outfile=dist/index.js --metafile=meta.json --analyze", + "build:meta": "esbuild ./src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --metafile=meta.json --analyze", "dev": "dotenv -e ../../.env nodemon", "lint": "eslint . --ext .ts", "tsc": "tsc --noEmit", @@ -20,8 +20,8 @@ "pkg": { "assets": "assets/**/*", "targets": [ - "node20-linux-x64", - "node20-linux-arm64" + "node18-linux-x64", + "node18-linux-arm64" ], "outputPath": "dist/bin" }, @@ -35,11 +35,11 @@ "dotenv-cli": "^7.3.0", "esbuild": "^0.19.4", "eslint-config-prettier": "^9.0.0", - "knip": "^2.41.3", + "knip": "^3.6.1", "memfs": "^4.6.0", "nodemon": "^3.0.1", "pkg": "^5.8.1", - "vite": "^4.5.0", + "vite": "^4.5.1", "vite-tsconfig-paths": "^4.2.1", "vitest": "^0.34.6" }, diff --git a/packages/shared/src/schemas/env-schemas.ts b/packages/shared/src/schemas/env-schemas.ts index 122fc0b000..6f529c4ffd 100644 --- a/packages/shared/src/schemas/env-schemas.ts +++ b/packages/shared/src/schemas/env-schemas.ts @@ -64,11 +64,35 @@ export const envSchema = z.object({ .optional() .transform((value) => { if (typeof value === 'boolean') return value; - return value === 'true'; + if (typeof value === 'string') return value === 'true'; + + return true; + }), + allowErrorMonitoring: z + .string() + .or(z.boolean()) + .optional() + .transform((value) => { + if (typeof value === 'boolean') return value; + if (typeof value === 'string') return value === 'true'; + + return true; }), }); export const settingsSchema = envSchema .partial() - .pick({ dnsIp: true, internalIp: true, postgresPort: true, appsRepoUrl: true, domain: true, storagePath: true, localDomain: true, demoMode: true, guestDashboard: true, allowAutoThemes: true }) + .pick({ + dnsIp: true, + internalIp: true, + postgresPort: true, + appsRepoUrl: true, + domain: true, + storagePath: true, + localDomain: true, + demoMode: true, + guestDashboard: true, + allowAutoThemes: true, + allowErrorMonitoring: true, + }) .and(z.object({ port: z.number(), sslPort: z.number(), listenIp: z.string().ip().trim() }).partial()); diff --git a/packages/shared/src/schemas/socket.ts b/packages/shared/src/schemas/socket.ts new file mode 100644 index 0000000000..910a69526e --- /dev/null +++ b/packages/shared/src/schemas/socket.ts @@ -0,0 +1,35 @@ +import { z } from 'zod'; + +export const socketEventSchema = z.union([ + z.object({ + type: z.literal('app'), + event: z.union([ + z.literal('status_change'), + z.literal('install_success'), + z.literal('install_error'), + z.literal('uninstall_success'), + z.literal('uninstall_error'), + z.literal('update_success'), + z.literal('update_error'), + z.literal('start_success'), + z.literal('start_error'), + z.literal('stop_success'), + z.literal('stop_error'), + z.literal('generate_env_success'), + z.literal('generate_env_error'), + ]), + data: z.object({ + appId: z.string(), + error: z.string().optional(), + }), + }), + z.object({ + type: z.literal('dummy'), + event: z.literal('dummy_event'), + data: z.object({ + dummy: z.string(), + }), + }), +]); + +export type SocketEvent = z.infer; diff --git a/packages/worker/.gitignore b/packages/worker/.gitignore index a60030e3cb..040d640221 100644 --- a/packages/worker/.gitignore +++ b/packages/worker/.gitignore @@ -1,2 +1,5 @@ dist/ coverage/ + +# Sentry Config File +.env.sentry-build-plugin diff --git a/packages/worker/Dockerfile b/packages/worker/Dockerfile index 9b7c5d1ec0..b12aa0918e 100644 --- a/packages/worker/Dockerfile +++ b/packages/worker/Dockerfile @@ -50,6 +50,9 @@ COPY ./packages/worker/src ./packages/worker/src COPY ./packages/worker/package.json ./packages/worker/package.json COPY ./packages/worker/assets ./packages/worker/assets +ARG SENTRY_AUTH_TOKEN +ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} + RUN pnpm -r build --filter @runtipi/worker # ---- RUNNER ---- diff --git a/packages/worker/build.js b/packages/worker/build.js index 102a666b45..327b2a50c0 100644 --- a/packages/worker/build.js +++ b/packages/worker/build.js @@ -1,6 +1,5 @@ const { build } = require('esbuild'); - -const commandArgs = process.argv.slice(2); +const { sentryEsbuildPlugin } = require('@sentry/esbuild-plugin'); async function bundle() { const start = Date.now(); @@ -11,10 +10,23 @@ async function bundle() { target: 'node20', bundle: true, color: true, - sourcemap: commandArgs.includes('--sourcemap'), + sourcemap: true, + loader: { + '.node': 'copy', + }, + minify: true, + plugins: [ + sentryEsbuildPlugin({ + authToken: process.env.SENTRY_AUTH_TOKEN, + org: 'runtipi', + project: 'runtipi-worker', + }), + ], }; - await build({ ...options, minify: true }); + await build({ + ...options, + }); console.log(`Build time: ${Date.now() - start}ms`); } diff --git a/packages/worker/nodemon.json b/packages/worker/nodemon.json index 8e05f77837..11acf9a8d9 100644 --- a/packages/worker/nodemon.json +++ b/packages/worker/nodemon.json @@ -1,5 +1,5 @@ { "watch": ["src"], - "exec": "NODE_ENV=development tsx ./src/index.ts", + "exec": "NODE_ENV=development DEBUG=socket.io:client* tsx ./src/index.ts", "ext": "js ts" } diff --git a/packages/worker/package.json b/packages/worker/package.json index f04633cb15..73d3baca7b 100644 --- a/packages/worker/package.json +++ b/packages/worker/package.json @@ -20,10 +20,10 @@ "@types/web-push": "^3.6.3", "dotenv-cli": "^7.3.0", "esbuild": "^0.19.4", - "knip": "^2.41.3", + "knip": "^3.6.1", "memfs": "^4.6.0", "nodemon": "^3.0.1", - "tsx": "^3.14.0", + "tsx": "^4.6.2", "typescript": "^5.2.2", "vite-tsconfig-paths": "^4.2.1", "vitest": "^0.34.6" @@ -31,10 +31,13 @@ "dependencies": { "@runtipi/postgres-migrations": "^5.3.0", "@runtipi/shared": "workspace:^", + "@sentry/esbuild-plugin": "^2.10.2", + "@sentry/node": "^7.86.0", "bullmq": "^4.13.0", "dotenv": "^16.3.1", "ioredis": "^5.3.2", "pg": "^8.11.3", + "socket.io": "^4.7.2", "systeminformation": "^5.21.15", "web-push": "^3.6.6", "zod": "^3.22.4" diff --git a/packages/worker/src/index.ts b/packages/worker/src/index.ts index 5c56afb142..d0188495dd 100644 --- a/packages/worker/src/index.ts +++ b/packages/worker/src/index.ts @@ -1,18 +1,28 @@ import { SystemEvent } from '@runtipi/shared'; + import http from 'node:http'; import path from 'node:path'; import Redis from 'ioredis'; import dotenv from 'dotenv'; import { Queue } from 'bullmq'; +import * as Sentry from '@sentry/node'; import { copySystemFiles, ensureFilePermissions, generateSystemEnvFile, generateTlsCertificates } from '@/lib/system'; import { runPostgresMigrations } from '@/lib/migrations'; import { startWorker } from './watcher/watcher'; import { logger } from '@/lib/logger'; import { AppExecutors } from './services'; +import { SocketManager } from './lib/socket/SocketManager'; const rootFolder = '/app'; const envFile = path.join(rootFolder, '.env'); +const setupSentry = () => { + Sentry.init({ + environment: process.env.NODE_ENV, + dsn: 'https://1cf49526d2efde9f82b6584c9c0f6912@o4504242900238336.ingest.sentry.io/4506360656035840', + }); +}; + const main = async () => { try { await logger.flush(); @@ -23,6 +33,11 @@ const main = async () => { logger.info('Generating system env file...'); const envMap = await generateSystemEnvFile(); + if (envMap.get('ALLOW_ERROR_MONITORING') === 'true') { + logger.info('Anonymous error monitoring is enabled, to disable it add "allowErrorMonitoring": false to your settings.json file'); + setupSentry(); + } + // Reload env variables after generating the env file logger.info('Reloading env variables...'); dotenv.config({ path: envFile, override: true }); @@ -83,11 +98,16 @@ const main = async () => { }); server.listen(3000, () => { + SocketManager.init(); startWorker(); }); } catch (e) { + Sentry.captureException(e); logger.error(e); - process.exit(1); + + setTimeout(() => { + process.exit(1); + }, 2000); } }; diff --git a/packages/worker/src/lib/socket/SocketManager.ts b/packages/worker/src/lib/socket/SocketManager.ts new file mode 100644 index 0000000000..e23707bf51 --- /dev/null +++ b/packages/worker/src/lib/socket/SocketManager.ts @@ -0,0 +1,35 @@ +import { SocketEvent } from '@runtipi/shared/src/schemas/socket'; +import { Server } from 'socket.io'; +import { logger } from '../logger'; + +class SocketManager { + private io: Server | null = null; + + init() { + const io = new Server(3001, { cors: { origin: '*' } }); + + io.on('connection', (socket) => { + socket.on('disconnect', () => {}); + }); + + this.io = io; + } + + async emit(event: SocketEvent) { + if (!this.io) { + logger.error('SocketManager is not initialized'); + return; + } + + const sockets = await this.io.fetchSockets(); + + // eslint-disable-next-line no-restricted-syntax + for (const socket of sockets) { + socket.emit(event.type, event); + } + } +} + +const instance = new SocketManager(); + +export { instance as SocketManager }; diff --git a/packages/worker/src/lib/system/system.helpers.ts b/packages/worker/src/lib/system/system.helpers.ts index b1a96fb287..1ed0997907 100644 --- a/packages/worker/src/lib/system/system.helpers.ts +++ b/packages/worker/src/lib/system/system.helpers.ts @@ -35,6 +35,7 @@ type EnvKeys = | 'GUEST_DASHBOARD' | 'TIPI_GID' | 'TIPI_UID' + | 'ALLOW_ERROR_MONITORING' // eslint-disable-next-line @typescript-eslint/ban-types | (string & {}); @@ -160,6 +161,7 @@ export const generateSystemEnvFile = async () => { envMap.set('GUEST_DASHBOARD', String(data.guestDashboard || 'false')); envMap.set('LOCAL_DOMAIN', data.localDomain || 'tipi.lan'); envMap.set('NODE_ENV', 'production'); + envMap.set('ALLOW_ERROR_MONITORING', String(data.allowErrorMonitoring) || 'true'); await fs.promises.writeFile(envFilePath, envMapToString(envMap)); diff --git a/packages/worker/src/services/app/__tests__/app.executors.test.ts b/packages/worker/src/services/app/__tests__/app.executors.test.ts index b185d8f78e..31324cae17 100644 --- a/packages/worker/src/services/app/__tests__/app.executors.test.ts +++ b/packages/worker/src/services/app/__tests__/app.executors.test.ts @@ -176,4 +176,41 @@ describe('test: app executors', () => { spy.mockRestore(); }); }); + + describe('test: updateApp()', () => { + it('should still update even if current compose file is broken', async () => { + // arrange + const spy = vi.spyOn(dockerHelpers, 'compose'); + const config = createAppConfig(); + + spy.mockRejectedValueOnce(new Error('test')); + spy.mockResolvedValueOnce({ stdout: 'done', stderr: '' }); + + // act + const { message, success } = await appExecutors.updateApp(config.id, config); + + // assert + expect(success).toBe(true); + expect(message).toBe(`App ${config.id} updated successfully`); + spy.mockRestore(); + }); + + it('should replace app directory with new one', async () => { + // arrange + const config = createAppConfig(); + const oldFolder = path.join(ROOT_FOLDER, 'apps', config.id); + + await fs.promises.writeFile(path.join(oldFolder, 'docker-compose.yml'), 'test'); + + // act + await appExecutors.updateApp(config.id, config); + + // assert + const exists = await pathExists(oldFolder); + const content = await fs.promises.readFile(path.join(oldFolder, 'docker-compose.yml'), 'utf-8'); + + expect(exists).toBe(true); + expect(content).not.toBe('test'); + }); + }); }); diff --git a/packages/worker/src/services/app/app.executors.ts b/packages/worker/src/services/app/app.executors.ts index bc3cf952b3..1c32db514c 100644 --- a/packages/worker/src/services/app/app.executors.ts +++ b/packages/worker/src/services/app/app.executors.ts @@ -3,12 +3,15 @@ import fs from 'fs'; import path from 'path'; import pg from 'pg'; +import * as Sentry from '@sentry/node'; import { execAsync, pathExists } from '@runtipi/shared'; +import { SocketEvent } from '@runtipi/shared/src/schemas/socket'; import { copyDataDir, generateEnvFile } from './app.helpers'; import { logger } from '@/lib/logger'; import { compose } from '@/lib/docker'; import { getEnv } from '@/lib/environment'; import { ROOT_FOLDER, STORAGE_FOLDER } from '@/config/constants'; +import { SocketManager } from '@/lib/socket/SocketManager'; const getDbClient = async () => { const { postgresHost, postgresDatabase, postgresUsername, postgresPassword, postgresPort } = getEnv(); @@ -33,12 +36,16 @@ export class AppExecutors { this.logger = logger; } - private handleAppError = (err: unknown) => { + private handleAppError = (err: unknown, appId: string, event: Extract['event']) => { + Sentry.captureException(err); + if (err instanceof Error) { + SocketManager.emit({ type: 'app', event, data: { appId, error: err.message } }); this.logger.error(`An error occurred: ${err.message}`); return { success: false, message: err.message }; } + SocketManager.emit({ type: 'app', event, data: { appId, error: String(err) } }); return { success: false, message: `An error occurred: ${err}` }; }; @@ -73,6 +80,19 @@ export class AppExecutors { } }; + public regenerateAppEnv = async (appId: string, config: Record) => { + try { + this.logger.info(`Regenerating app.env file for app ${appId}`); + await this.ensureAppDir(appId); + await generateEnvFile(appId, config); + + SocketManager.emit({ type: 'app', event: 'generate_env_success', data: { appId } }); + return { success: true, message: `App ${appId} env file regenerated successfully` }; + } catch (err) { + return this.handleAppError(err, appId, 'generate_env_error'); + } + }; + /** * Install an app from the repo * @param {string} appId - The id of the app to install @@ -80,6 +100,8 @@ export class AppExecutors { */ public installApp = async (appId: string, config: Record) => { try { + SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } }); + if (process.getuid && process.getgid) { this.logger.info(`Installing app ${appId} as User ID: ${process.getuid()}, Group ID: ${process.getgid()}`); } else { @@ -134,9 +156,11 @@ export class AppExecutors { this.logger.info(`Docker-compose up for app ${appId} finished`); + SocketManager.emit({ type: 'app', event: 'install_success', data: { appId } }); + return { success: true, message: `App ${appId} installed successfully` }; } catch (err) { - return this.handleAppError(err); + return this.handleAppError(err, appId, 'install_error'); } }; @@ -147,6 +171,7 @@ export class AppExecutors { */ public stopApp = async (appId: string, config: Record, skipEnvGeneration = false) => { try { + SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } }); this.logger.info(`Stopping app ${appId}`); await this.ensureAppDir(appId); @@ -158,14 +183,19 @@ export class AppExecutors { await compose(appId, 'rm --force --stop'); this.logger.info(`App ${appId} stopped`); + + SocketManager.emit({ type: 'app', event: 'stop_success', data: { appId } }); + return { success: true, message: `App ${appId} stopped successfully` }; } catch (err) { - return this.handleAppError(err); + return this.handleAppError(err, appId, 'stop_error'); } }; public startApp = async (appId: string, config: Record, skipEnvGeneration = false) => { try { + SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } }); + const { appDataDirPath } = this.getAppPaths(appId); this.logger.info(`Starting app ${appId}`); @@ -186,14 +216,18 @@ export class AppExecutors { this.logger.error(`Error setting permissions for app ${appId}`); }); + SocketManager.emit({ type: 'app', event: 'start_success', data: { appId } }); + return { success: true, message: `App ${appId} started successfully` }; } catch (err) { - return this.handleAppError(err); + return this.handleAppError(err, appId, 'start_error'); } }; public uninstallApp = async (appId: string, config: Record) => { try { + SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } }); + const { appDirPath, appDataDirPath } = this.getAppPaths(appId); this.logger.info(`Uninstalling app ${appId}`); @@ -221,21 +255,30 @@ export class AppExecutors { }); this.logger.info(`App ${appId} uninstalled`); + + SocketManager.emit({ type: 'app', event: 'uninstall_success', data: { appId } }); + return { success: true, message: `App ${appId} uninstalled successfully` }; } catch (err) { - return this.handleAppError(err); + return this.handleAppError(err, appId, 'uninstall_error'); } }; public updateApp = async (appId: string, config: Record) => { try { + SocketManager.emit({ type: 'app', event: 'status_change', data: { appId } }); + const { appDirPath, repoPath } = this.getAppPaths(appId); this.logger.info(`Updating app ${appId}`); await this.ensureAppDir(appId); await generateEnvFile(appId, config); - await compose(appId, 'up --detach --force-recreate --remove-orphans'); - await compose(appId, 'down --rmi all --remove-orphans'); + try { + await compose(appId, 'up --detach --force-recreate --remove-orphans'); + await compose(appId, 'down --rmi all --remove-orphans'); + } catch (err) { + logger.warn(`App ${appId} has likely a broken docker-compose.yml file. Continuing with update...`); + } this.logger.info(`Deleting folder ${appDirPath}`); await fs.promises.rm(appDirPath, { recursive: true, force: true }); @@ -245,20 +288,11 @@ export class AppExecutors { await compose(appId, 'pull'); - return { success: true, message: `App ${appId} updated successfully` }; - } catch (err) { - return this.handleAppError(err); - } - }; + SocketManager.emit({ type: 'app', event: 'update_success', data: { appId } }); - public regenerateAppEnv = async (appId: string, config: Record) => { - try { - this.logger.info(`Regenerating app.env file for app ${appId}`); - await this.ensureAppDir(appId); - await generateEnvFile(appId, config); - return { success: true, message: `App ${appId} env file regenerated successfully` }; + return { success: true, message: `App ${appId} updated successfully` }; } catch (err) { - return this.handleAppError(err); + return this.handleAppError(err, appId, 'update_error'); } }; diff --git a/packages/worker/src/services/repo/repo.executors.ts b/packages/worker/src/services/repo/repo.executors.ts index 9f1d6b5a79..ed34cea4c4 100644 --- a/packages/worker/src/services/repo/repo.executors.ts +++ b/packages/worker/src/services/repo/repo.executors.ts @@ -1,5 +1,6 @@ import path from 'path'; import { execAsync, pathExists } from '@runtipi/shared'; +import * as Sentry from '@sentry/node'; import { getRepoHash, getRepoBaseUrlAndBranch } from './repo.helpers'; import { logger } from '@/lib/logger'; @@ -15,6 +16,8 @@ export class RepoExecutors { * @param {unknown} err */ private handleRepoError = (err: unknown) => { + Sentry.captureException(err); + if (err instanceof Error) { this.logger.error(`An error occurred: ${err.message}`); return { success: false, message: err.message }; diff --git a/packages/worker/src/services/system/system.executors.ts b/packages/worker/src/services/system/system.executors.ts index 80f6342137..32f1f7c7ff 100644 --- a/packages/worker/src/services/system/system.executors.ts +++ b/packages/worker/src/services/system/system.executors.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import path from 'path'; import si from 'systeminformation'; +import * as Sentry from '@sentry/node'; import { logger } from '@/lib/logger'; import { ROOT_FOLDER } from '@/config/constants'; @@ -12,6 +13,8 @@ export class SystemExecutors { } private handleSystemError = (err: unknown) => { + Sentry.captureException(err); + if (err instanceof Error) { this.logger.error(`An error occurred: ${err.message}`); return { success: false, message: err.message }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e48d698fec..5382510483 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ importers: '@runtipi/shared': specifier: workspace:^ version: link:packages/shared + '@sentry/nextjs': + specifier: ^7.86.0 + version: 7.86.0(next@14.0.1)(react@18.2.0) '@tabler/core': specifier: 1.0.0-beta20 version: 1.0.0-beta20 @@ -63,8 +66,8 @@ importers: specifier: ^7.1.0 version: 7.1.0(express-session@1.17.3) drizzle-orm: - specifier: ^0.28.6 - version: 0.28.6(@types/pg@8.10.7)(pg@8.11.3) + specifier: ^0.29.1 + version: 0.29.1(@types/pg@8.10.9)(pg@8.11.3) fs-extra: specifier: ^11.1.1 version: 11.1.1 @@ -108,8 +111,8 @@ importers: specifier: ^2.4.1 version: 2.4.1(csstype@3.1.1)(react-dom@18.2.0)(react@18.2.0) react-markdown: - specifier: ^9.0.0 - version: 9.0.0(@types/react@18.2.39)(react@18.2.0) + specifier: ^9.0.1 + version: 9.0.1(@types/react@18.2.39)(react@18.2.0) react-select: specifier: ^5.8.0 version: 5.8.0(@types/react@18.2.39)(react-dom@18.2.0)(react@18.2.0) @@ -140,6 +143,9 @@ importers: sharp: specifier: 0.32.6 version: 0.32.6 + socket.io-client: + specifier: ^4.7.2 + version: 4.7.2 swr: specifier: ^2.2.4 version: 2.2.4(react@18.2.0) @@ -202,8 +208,8 @@ importers: specifier: 20.8.10 version: 20.8.10 '@types/pg': - specifier: ^8.10.7 - version: 8.10.7 + specifier: ^8.10.9 + version: 8.10.9 '@types/react': specifier: 18.2.39 version: 18.2.39 @@ -221,13 +227,13 @@ importers: version: 13.11.5 '@typescript-eslint/eslint-plugin': specifier: ^6.13.1 - version: 6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.52.0)(typescript@5.2.2) + version: 6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.55.0)(typescript@5.2.2) '@typescript-eslint/parser': specifier: ^6.10.0 - version: 6.10.0(eslint@8.52.0)(typescript@5.2.2) + version: 6.10.0(eslint@8.55.0)(typescript@5.2.2) '@vitejs/plugin-react': specifier: ^4.1.1 - version: 4.1.1(vite@4.5.0) + version: 4.1.1(vite@4.5.1) '@vitest/coverage-v8': specifier: ^0.34.6 version: 0.34.6(vitest@0.34.6) @@ -235,44 +241,44 @@ importers: specifier: ^7.3.0 version: 7.3.0 eslint: - specifier: 8.52.0 - version: 8.52.0 + specifier: 8.55.0 + version: 8.55.0 eslint-config-airbnb: specifier: ^19.0.4 - version: 19.0.4(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@8.52.0) + version: 19.0.4(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@8.55.0) eslint-config-airbnb-typescript: specifier: ^17.1.0 - version: 17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.52.0) + version: 17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.55.0) eslint-config-next: specifier: 14.0.3 - version: 14.0.3(eslint@8.52.0)(typescript@5.2.2) + version: 14.0.3(eslint@8.55.0)(typescript@5.2.2) eslint-config-prettier: specifier: ^9.0.0 - version: 9.0.0(eslint@8.52.0) + version: 9.0.0(eslint@8.55.0) eslint-import-resolver-typescript: specifier: ^3.6.1 - version: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.52.0) + version: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.55.0) eslint-plugin-import: specifier: ^2.29.0 - version: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + version: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) eslint-plugin-jest: specifier: ^27.6.0 - version: 27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.52.0)(jest@29.7.0)(typescript@5.2.2) + version: 27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.55.0)(jest@29.7.0)(typescript@5.2.2) eslint-plugin-jest-dom: specifier: ^5.1.0 - version: 5.1.0(@testing-library/dom@9.3.3)(eslint@8.52.0) + version: 5.1.0(@testing-library/dom@9.3.3)(eslint@8.55.0) eslint-plugin-jsx-a11y: specifier: ^6.8.0 - version: 6.8.0(eslint@8.52.0) + version: 6.8.0(eslint@8.55.0) eslint-plugin-react: specifier: ^7.33.2 - version: 7.33.2(eslint@8.52.0) + version: 7.33.2(eslint@8.55.0) eslint-plugin-react-hooks: specifier: ^4.6.0 - version: 4.6.0(eslint@8.52.0) + version: 4.6.0(eslint@8.55.0) eslint-plugin-testing-library: specifier: ^6.1.0 - version: 6.1.0(eslint@8.52.0)(typescript@5.2.2) + version: 6.1.0(eslint@8.55.0)(typescript@5.2.2) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.8.10)(ts-node@10.9.1) @@ -280,8 +286,8 @@ importers: specifier: ^29.7.0 version: 29.7.0 knip: - specifier: ^2.41.3 - version: 2.41.3 + specifier: ^3.6.1 + version: 3.6.1(@types/node@20.8.10)(typescript@5.2.2) memfs: specifier: ^4.6.0 version: 4.6.0(quill-delta@5.1.0)(rxjs@7.8.1)(tslib@2.6.2) @@ -301,14 +307,14 @@ importers: specifier: ^10.9.1 version: 10.9.1(@types/node@20.8.10)(typescript@5.2.2) tsx: - specifier: ^3.14.0 - version: 3.14.0 + specifier: ^4.6.2 + version: 4.6.2 typescript: specifier: 5.2.2 version: 5.2.2 vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.2.1(typescript@5.2.2)(vite@4.5.0) + version: 4.2.1(typescript@5.2.2)(vite@4.5.1) vitest: specifier: ^0.34.6 version: 0.34.6(sass@1.69.5) @@ -372,10 +378,10 @@ importers: version: 0.19.4 eslint-config-prettier: specifier: ^9.0.0 - version: 9.0.0(eslint@8.52.0) + version: 9.0.0(eslint@8.55.0) knip: - specifier: ^2.41.3 - version: 2.41.3 + specifier: ^3.6.1 + version: 3.6.1(@types/node@20.8.10)(typescript@5.2.2) memfs: specifier: ^4.6.0 version: 4.6.0(quill-delta@5.1.0)(rxjs@7.8.1)(tslib@2.6.2) @@ -386,11 +392,11 @@ importers: specifier: ^5.8.1 version: 5.8.1 vite: - specifier: ^4.5.0 - version: 4.5.0(@types/node@20.8.10)(sass@1.69.5) + specifier: ^4.5.1 + version: 4.5.1(@types/node@20.8.10)(sass@1.69.5) vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.2.1(typescript@5.2.2)(vite@4.5.0) + version: 4.2.1(typescript@5.2.2)(vite@4.5.1) vitest: specifier: ^0.34.6 version: 0.34.6(sass@1.69.5) @@ -412,6 +418,12 @@ importers: '@runtipi/shared': specifier: workspace:^ version: link:../shared + '@sentry/esbuild-plugin': + specifier: ^2.10.2 + version: 2.10.2 + '@sentry/node': + specifier: ^7.86.0 + version: 7.86.0 bullmq: specifier: ^4.13.0 version: 4.13.0 @@ -424,6 +436,9 @@ importers: pg: specifier: ^8.11.3 version: 8.11.3 + socket.io: + specifier: ^4.7.2 + version: 4.7.2 systeminformation: specifier: ^5.21.15 version: 5.21.15 @@ -447,8 +462,8 @@ importers: specifier: ^0.19.4 version: 0.19.4 knip: - specifier: ^2.41.3 - version: 2.41.3 + specifier: ^3.6.1 + version: 3.6.1(@types/node@20.8.10)(typescript@5.2.2) memfs: specifier: ^4.6.0 version: 4.6.0(quill-delta@5.1.0)(rxjs@7.8.1)(tslib@2.6.2) @@ -456,14 +471,14 @@ importers: specifier: ^3.0.1 version: 3.0.1 tsx: - specifier: ^3.14.0 - version: 3.14.0 + specifier: ^4.6.2 + version: 4.6.2 typescript: specifier: ^5.2.2 version: 5.2.2 vite-tsconfig-paths: specifier: ^4.2.1 - version: 4.2.1(typescript@5.2.2)(vite@4.5.0) + version: 4.2.1(typescript@5.2.2)(vite@4.5.1) vitest: specifier: ^0.34.6 version: 0.34.6(sass@1.69.5) @@ -475,8 +490,8 @@ packages: engines: {node: '>=0.10.0'} dev: true - /@adobe/css-tools@4.3.1: - resolution: {integrity: sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg==} + /@adobe/css-tools@4.3.2: + resolution: {integrity: sha512-DA5a1C0gD/pLOvhv33YMrbf2FK3oUzwNl9oOJqE4XVjuEtt6XIakRcsd7eLiOSPkp1kTRQGICTA8cKra/vFbjw==} dev: true /@ampproject/remapping@2.2.1: @@ -1409,13 +1424,13 @@ packages: dev: true optional: true - /@eslint-community/eslint-utils@4.4.0(eslint@8.52.0): + /@eslint-community/eslint-utils@4.4.0(eslint@8.55.0): resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 dependencies: - eslint: 8.52.0 + eslint: 8.55.0 eslint-visitor-keys: 3.4.3 dev: true @@ -1424,8 +1439,8 @@ packages: engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.1.2: - resolution: {integrity: sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==} + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 @@ -1441,8 +1456,8 @@ packages: - supports-color dev: true - /@eslint/js@8.52.0: - resolution: {integrity: sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA==} + /@eslint/js@8.55.0: + resolution: {integrity: sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true @@ -2128,8 +2143,8 @@ packages: validate-npm-package-name: 4.0.0 dev: true - /@pnpm/npm-resolver@17.0.0(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-XCeFga+Am3rsTO+8IIuIPb6VsZ+iCiv5QJW61YDl4XuiqoyCFzNyGgGfv05n45lIfK0Gg1jA2ewlo0LpGelCUw==} + /@pnpm/npm-resolver@18.0.0(@pnpm/logger@5.0.0): + resolution: {integrity: sha512-FGHmnRjSf7tQHagk6bMrUtHvZbz3ROUoGRDrpMyqJo///+S7SZt/hSDS77PhZ7T6PRXipkFyUtRkqtHmGKFCAg==} engines: {node: '>=16.14'} peerDependencies: '@pnpm/logger': ^5.0.0 @@ -2152,7 +2167,7 @@ packages: parse-npm-tarball-url: 3.0.0 path-temp: 2.1.0 ramda: /@pnpm/ramda@0.28.1 - rename-overwrite: 4.0.3 + rename-overwrite: 4.0.4 semver: 7.5.4 ssri: 10.0.5 version-selector-type: 3.0.0 @@ -2183,12 +2198,12 @@ packages: engines: {node: '>=16.14'} dev: true - /@pnpm/workspace.pkgs-graph@2.0.10(@pnpm/logger@5.0.0): - resolution: {integrity: sha512-iGZZ23li6Ya68kHx3oaWPCN4JMzJ0njmmmWDRxUcHkc+nxtxTwpEM/FRl7yG1nBo39YwX2XTtou22h2nKipHnw==} + /@pnpm/workspace.pkgs-graph@2.0.11(@pnpm/logger@5.0.0): + resolution: {integrity: sha512-VRX7E7pX92C0akCMYGzsTqJoOwQS7/8R40pAPK7smgaEpKeEgVThqnIXt+wPdseD5CzS7OzMaIWlT3WXr3O5rQ==} engines: {node: '>=16.14'} dependencies: '@pnpm/npm-package-arg': 1.0.0 - '@pnpm/npm-resolver': 17.0.0(@pnpm/logger@5.0.0) + '@pnpm/npm-resolver': 18.0.0(@pnpm/logger@5.0.0) '@pnpm/resolve-workspace-range': 5.0.1 ramda: /@pnpm/ramda@0.28.1 transitivePeerDependencies: @@ -2916,6 +2931,39 @@ packages: '@redis/client': 1.5.11 dev: false + /@rollup/plugin-commonjs@24.0.0(rollup@2.78.0): + resolution: {integrity: sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.68.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@2.78.0) + commondir: 1.0.1 + estree-walker: 2.0.2 + glob: 8.1.0 + is-reference: 1.2.1 + magic-string: 0.27.0 + rollup: 2.78.0 + dev: false + + /@rollup/pluginutils@5.1.0(rollup@2.78.0): + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 2.78.0 + dev: false + /@runtipi/postgres-migrations@5.3.0: resolution: {integrity: sha512-pTAx/8j843L4n9f4TOCRh6eGFQD827jY64EVy5luHZNOfaiX1KI6SaWpzMfNPdAwy1od0k5FZrDJjpyHXC0ppg==} engines: {node: '>10.17.0'} @@ -2931,10 +2979,294 @@ packages: resolution: {integrity: sha512-EF3948ckf3f5uPgYbQ6GhyA56Dmv8yg0+ir+BroRjwdxyZJsekhZzawOecC2rOTPCz173t7ZcR1HHZu0dZgOCw==} dev: true + /@sentry-internal/feedback@7.86.0: + resolution: {integrity: sha512-6rl0JYjmAKnhm4/fuFaROh4Ht8oi9f6ZeIcViCuGJcrGICZJJY0s+R77XJI78rNa82PYFrSCcnWXcGji4T8E7g==} + engines: {node: '>=12'} + dependencies: + '@sentry/core': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + dev: false + + /@sentry-internal/tracing@7.86.0: + resolution: {integrity: sha512-b4dUsNWlPWRwakGwR7bhOkqiFlqQszH1hhVFwrm/8s3kqEBZ+E4CeIfCvuHBHQ1cM/fx55xpXX/BU163cy+3iQ==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + dev: false + + /@sentry/browser@7.86.0: + resolution: {integrity: sha512-nfYWpVOmug+W7KJO7/xhA1JScMZcYHcoOVHLsUFm4znx51U4qZEk+zZDM11Q2Nw6MuDyEYg6bsH1QCwaoC6nLw==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/feedback': 7.86.0 + '@sentry-internal/tracing': 7.86.0 + '@sentry/core': 7.86.0 + '@sentry/replay': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + dev: false + + /@sentry/bundler-plugin-core@2.10.2: + resolution: {integrity: sha512-7IoekLtROlJZqTxtHQ3IhocBuf9dsEq+JjqlHMyZXoq+QKuvJFvMd/4T+r6KjZ15kMZOIkR+spK3V7duH201hw==} + engines: {node: '>= 14'} + dependencies: + '@sentry/cli': 2.23.0 + '@sentry/node': 7.86.0 + '@sentry/utils': 7.86.0 + dotenv: 16.3.1 + find-up: 5.0.0 + glob: 9.3.2 + magic-string: 0.27.0 + unplugin: 1.0.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@sentry/cli-darwin@2.23.0: + resolution: {integrity: sha512-tWuTxvb6P5pA0E+O1/7jKQ6AP45DOOW+BAd7mwBMHZ+5xG3nsvvrRS9hOIzBNPTeB2RyIEXgpQ2Mb6NdD21DBQ==} + engines: {node: '>=10'} + os: [darwin] + requiresBuild: true + dev: false + optional: true + + /@sentry/cli-linux-arm64@2.23.0: + resolution: {integrity: sha512-KsOckP+b0xAzrRuoP4eiqJ6ASD6SqIplL8BCHOAODQfvWn9rgNwsJWOgKlWwfrJnkJYkpWVYvYeyx0oeUx3N0g==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd] + requiresBuild: true + dev: false + optional: true + + /@sentry/cli-linux-arm@2.23.0: + resolution: {integrity: sha512-1R8ngBDKtPw++Km6VnVTx76ndrBL9BuBBNpF9TUCGftK3ArdaifqoIx8cZ8aKu8sWXLAKO7lHzxL4BNPZvlDiw==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd] + requiresBuild: true + dev: false + optional: true + + /@sentry/cli-linux-i686@2.23.0: + resolution: {integrity: sha512-KRqB98KstBkKh33ZqUq+q8O0U4c01aTWCNPpVrqAX7zikSk0AAJTG8eAtqwDSx949IkKUl8xa6PFLfz+Nb2EMQ==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd] + requiresBuild: true + dev: false + optional: true + + /@sentry/cli-linux-x64@2.23.0: + resolution: {integrity: sha512-USHZ0zzg9qujGYAyRjLeUfLDZOMgNjCr82m0BSBMmlFs4oKwHmO6bSvdi9UzNNcpmkOavNAdUM4jnZWk11i46Q==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd] + requiresBuild: true + dev: false + optional: true + + /@sentry/cli-win32-i686@2.23.0: + resolution: {integrity: sha512-lS/B3pONDl18IEu/I//3vcMnosThobyXpqfAm4WYUtFTiw/wwDHgwGgaIjZWm5wMRkPFzYoRFpZfPlUrJd/4cQ==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@sentry/cli-win32-x64@2.23.0: + resolution: {integrity: sha512-7LP6wA3w93ViYKQR8tMN2i/SfpQzaXqM2SAHI3yfJ3bdREHOV3+/N0mNiWVRvgL0TKNQJS42v2IILLhiDxufHQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: false + optional: true + + /@sentry/cli@1.77.1: + resolution: {integrity: sha512-OtJ7U9LeuPUAY/xow9wwcjM9w42IJIpDtClTKI/RliE685vd/OJUIpiAvebHNthDYpQynvwb/0iuF4fonh+CKw==} + engines: {node: '>= 8'} + hasBin: true + requiresBuild: true + dependencies: + https-proxy-agent: 5.0.1 + mkdirp: 0.5.6 + node-fetch: 2.6.9 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@sentry/cli@2.23.0: + resolution: {integrity: sha512-xFTv7YOaKWMCSPgN8A1jZpxJQhwdES89pqMTWjJOgjmkwFvziuaTM7O7kazps/cACDhJp2lK2j6AT6imhr4t9w==} + engines: {node: '>= 10'} + hasBin: true + dependencies: + https-proxy-agent: 5.0.1 + node-fetch: 2.6.9 + progress: 2.0.3 + proxy-from-env: 1.1.0 + which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.23.0 + '@sentry/cli-linux-arm': 2.23.0 + '@sentry/cli-linux-arm64': 2.23.0 + '@sentry/cli-linux-i686': 2.23.0 + '@sentry/cli-linux-x64': 2.23.0 + '@sentry/cli-win32-i686': 2.23.0 + '@sentry/cli-win32-x64': 2.23.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@sentry/core@7.86.0: + resolution: {integrity: sha512-SbLvqd1bRYzhDS42u7GMnmbDMfth/zRiLElQWbLK/shmuZzTcfQSwNNdF4Yj+VfjOkqPFgGmICHSHVUc9dh01g==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + dev: false + + /@sentry/esbuild-plugin@2.10.2: + resolution: {integrity: sha512-1n+ESSkW093zO2ZJtRpepF2SU/qvSOCHczNPfHsKuTpeSZgaGie9WMR39PXMOACFiUG3F0pK7TDTRUmcosBSAQ==} + engines: {node: '>= 14'} + dependencies: + '@sentry/bundler-plugin-core': 2.10.2 + unplugin: 1.0.1 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@sentry/integrations@7.86.0: + resolution: {integrity: sha512-BStRH1yBhhUsvmCXWx88/1+cY93l4B+3RW60RPeYcupvUQ1DJ8qxfN918+nA9XoZt9XELXvs8USCqqynG/aEkg==} + engines: {node: '>=8'} + dependencies: + '@sentry/core': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + localforage: 1.10.0 + dev: false + + /@sentry/nextjs@7.86.0(next@14.0.1)(react@18.2.0): + resolution: {integrity: sha512-pdRTt3ELLlpyKKtvumSiqFeTImdSAnoII1JSNwJvmWz9+3MRsvBW/Ee4r19WxK07Y/nxPxyPaIuUmbsXnjkt1A==} + engines: {node: '>=8'} + peerDependencies: + next: ^10.0.8 || ^11.0 || ^12.0 || ^13.0 || ^14.0 + react: 16.x || 17.x || 18.x + webpack: '>= 4.0.0' + peerDependenciesMeta: + webpack: + optional: true + dependencies: + '@rollup/plugin-commonjs': 24.0.0(rollup@2.78.0) + '@sentry/core': 7.86.0 + '@sentry/integrations': 7.86.0 + '@sentry/node': 7.86.0 + '@sentry/react': 7.86.0(react@18.2.0) + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + '@sentry/vercel-edge': 7.86.0 + '@sentry/webpack-plugin': 1.21.0 + chalk: 3.0.0 + next: 14.0.1(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)(sass@1.69.5) + react: 18.2.0 + resolve: 1.22.8 + rollup: 2.78.0 + stacktrace-parser: 0.1.10 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + + /@sentry/node@7.86.0: + resolution: {integrity: sha512-cB1bn/LMn2Km97Y3hv63xwWxT50/G5ixGuSxTZ3dCQM6VDhmZoCuC5NGT3itVvaRd6upQXRZa5W0Zgyh0HXKig==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/tracing': 7.86.0 + '@sentry/core': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + https-proxy-agent: 5.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /@sentry/react@7.86.0(react@18.2.0): + resolution: {integrity: sha512-2bHi+YcG4cT+4xHXXzv+AZpU3pdPUlDBorSgHOpa9At4yxr17UWW2f8bP9wPYRgj+NEIM3YhDgR46FlBu9GSKg==} + engines: {node: '>=8'} + peerDependencies: + react: 15.x || 16.x || 17.x || 18.x + dependencies: + '@sentry/browser': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + dev: false + + /@sentry/replay@7.86.0: + resolution: {integrity: sha512-YYZO8bfQSx1H87Te/zzyHPLHvExWiYwUfMWW68yGX+PPZIIzxaM81/iCQHkoucxlvuPCOtxCgf7RSMbsnqEa8g==} + engines: {node: '>=12'} + dependencies: + '@sentry-internal/tracing': 7.86.0 + '@sentry/core': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + dev: false + + /@sentry/types@7.86.0: + resolution: {integrity: sha512-pGAt0+bMfWgo0KG2epthfNV4Wae03tURpoxNjGo5Fr4cXxvLTSijSAQ6rmmO4bXBJ7+rErEjX30g30o/eEdP9g==} + engines: {node: '>=8'} + dev: false + + /@sentry/utils@7.86.0: + resolution: {integrity: sha512-6PejFtw9VTFFy5vu0ks+U7Ozkqz+eMt+HN8AZKBKErYzX5/xs0kpkOcSRpu3ETdTYcZf8VAmLVgFgE2BE+3WuQ==} + engines: {node: '>=8'} + dependencies: + '@sentry/types': 7.86.0 + dev: false + + /@sentry/vercel-edge@7.86.0: + resolution: {integrity: sha512-+MPb93DXIeYIoaFTT1YpC0myIkXW3xtxhQ7y7QwqS7k6x1zBb34OVCGitdE6+o85RV83sFMMiBxrfKNLt5Ht0A==} + engines: {node: '>=8'} + dependencies: + '@sentry-internal/tracing': 7.86.0 + '@sentry/core': 7.86.0 + '@sentry/types': 7.86.0 + '@sentry/utils': 7.86.0 + dev: false + + /@sentry/webpack-plugin@1.21.0: + resolution: {integrity: sha512-x0PYIMWcsTauqxgl7vWUY6sANl+XGKtx7DCVnnY7aOIIlIna0jChTAPANTfA2QrK+VK+4I/4JxatCEZBnXh3Og==} + engines: {node: '>= 8'} + dependencies: + '@sentry/cli': 1.77.1 + webpack-sources: 3.2.3 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@sindresorhus/merge-streams@1.0.0: + resolution: {integrity: sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==} + engines: {node: '>=18'} + dev: true + /@sinonjs/commons@2.0.0: resolution: {integrity: sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==} dependencies: @@ -2957,6 +3289,10 @@ packages: p-map: 4.0.0 dev: true + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: @@ -3074,7 +3410,7 @@ packages: vitest: optional: true dependencies: - '@adobe/css-tools': 4.3.1 + '@adobe/css-tools': 4.3.2 '@babel/runtime': 7.22.6 '@types/jest': 29.5.11 aria-query: 5.1.3 @@ -3200,13 +3536,22 @@ packages: /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} - dev: true + + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 20.8.10 + dev: false /@types/debug@4.1.7: resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==} dependencies: '@types/ms': 0.7.31 + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + dev: false + /@types/fs-extra@11.0.4: resolution: {integrity: sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==} dependencies: @@ -3223,7 +3568,7 @@ packages: /@types/hast@3.0.0: resolution: {integrity: sha512-SoytUJRuf68HXYqcXicQIhCrLQjqeYU2anikr4G3p3Iz+OZO5QDQpDj++gv+RenHsnUBwNZ2dumBArF8VLSk2Q==} dependencies: - '@types/unist': 2.0.6 + '@types/unist': 3.0.0 dev: false /@types/istanbul-lib-coverage@2.0.4: @@ -3303,8 +3648,8 @@ packages: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} dev: false - /@types/pg@8.10.7: - resolution: {integrity: sha512-ksJqHipwYaSEHz9e1fr6H6erjoEdNNaOxwyJgPx9bNeaqOW3iWBQgVHfpwiSAoqGzchfc+ZyRLwEfeCcyYD3uQ==} + /@types/pg@8.10.9: + resolution: {integrity: sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==} dependencies: '@types/node': 20.8.10 pg-protocol: 1.6.0 @@ -3356,10 +3701,6 @@ packages: resolution: {integrity: sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g==} dev: false - /@types/unist@2.0.6: - resolution: {integrity: sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==} - dev: false - /@types/unist@3.0.0: resolution: {integrity: sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==} dev: false @@ -3388,7 +3729,7 @@ packages: '@types/yargs-parser': 21.0.0 dev: true - /@typescript-eslint/eslint-plugin@6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/eslint-plugin@6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -3400,13 +3741,13 @@ packages: optional: true dependencies: '@eslint-community/regexpp': 4.9.0 - '@typescript-eslint/parser': 6.10.0(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.55.0)(typescript@5.2.2) '@typescript-eslint/scope-manager': 6.13.1 - '@typescript-eslint/type-utils': 6.13.1(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.13.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/type-utils': 6.13.1(eslint@8.55.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.13.1(eslint@8.55.0)(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.13.1 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.55.0 graphemer: 1.4.0 ignore: 5.2.4 natural-compare: 1.4.0 @@ -3417,7 +3758,7 @@ packages: - supports-color dev: true - /@typescript-eslint/parser@6.10.0(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/parser@6.10.0(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -3432,7 +3773,7 @@ packages: '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) '@typescript-eslint/visitor-keys': 6.10.0 debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.55.0 typescript: 5.2.2 transitivePeerDependencies: - supports-color @@ -3462,7 +3803,7 @@ packages: '@typescript-eslint/visitor-keys': 6.13.1 dev: true - /@typescript-eslint/type-utils@6.13.1(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/type-utils@6.13.1(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: @@ -3473,9 +3814,9 @@ packages: optional: true dependencies: '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) - '@typescript-eslint/utils': 6.13.1(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/utils': 6.13.1(eslint@8.55.0)(typescript@5.2.2) debug: 4.3.4 - eslint: 8.52.0 + eslint: 8.55.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -3560,19 +3901,19 @@ packages: - supports-color dev: true - /@typescript-eslint/utils@5.60.1(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/utils@5.60.1(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) '@types/json-schema': 7.0.13 '@types/semver': 7.5.4 '@typescript-eslint/scope-manager': 5.60.1 '@typescript-eslint/types': 5.60.1 '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.2.2) - eslint: 8.52.0 + eslint: 8.55.0 eslint-scope: 5.1.1 semver: 7.5.4 transitivePeerDependencies: @@ -3580,19 +3921,19 @@ packages: - typescript dev: true - /@typescript-eslint/utils@6.13.1(eslint@8.52.0)(typescript@5.2.2): + /@typescript-eslint/utils@6.13.1(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==} engines: {node: ^16.0.0 || >=18.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0 dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) '@types/json-schema': 7.0.13 '@types/semver': 7.5.4 '@typescript-eslint/scope-manager': 6.13.1 '@typescript-eslint/types': 6.13.1 '@typescript-eslint/typescript-estree': 6.13.1(typescript@5.2.2) - eslint: 8.52.0 + eslint: 8.55.0 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -3626,7 +3967,7 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - /@vitejs/plugin-react@4.1.1(vite@4.5.0): + /@vitejs/plugin-react@4.1.1(vite@4.5.1): resolution: {integrity: sha512-Jie2HERK+uh27e+ORXXwEP5h0Y2lS9T2PRGbfebiHGlwzDO0dEnd2aNtOR/qjBlPb1YgxwAONeblL1xqLikLag==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -3637,7 +3978,7 @@ packages: '@babel/plugin-transform-react-jsx-source': 7.22.5(@babel/core@7.23.2) '@types/babel__core': 7.20.3 react-refresh: 0.14.0 - vite: 4.5.0(@types/node@20.8.10)(sass@1.69.5) + vite: 4.5.1(@types/node@20.8.10)(sass@1.69.5) transitivePeerDependencies: - supports-color dev: true @@ -3731,6 +4072,14 @@ packages: /abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: @@ -3755,7 +4104,6 @@ packages: resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} hasBin: true - dev: true /acorn@8.8.2: resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} @@ -3836,7 +4184,6 @@ packages: engines: {node: '>=8'} dependencies: color-convert: 2.0.1 - dev: true /ansi-styles@5.2.0: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} @@ -4186,6 +4533,11 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -4382,7 +4734,6 @@ packages: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true /chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} @@ -4606,6 +4957,10 @@ packages: engines: {node: '>= 6'} dev: true + /commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + dev: false + /compose-function@3.0.3: resolution: {integrity: sha512-xzhzTJ5eC+gmIzvZq+C3kCJHsp9os6tJkrigDRZclyGtOKINbZtE8n1Tzmeh32jW+BUDPbvZpibwvJHBLGMVwg==} dependencies: @@ -4650,6 +5005,14 @@ packages: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} dev: true + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + /cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -4984,8 +5347,8 @@ packages: resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} engines: {node: '>=12'} - /drizzle-orm@0.28.6(@types/pg@8.10.7)(pg@8.11.3): - resolution: {integrity: sha512-yBe+F9htrlYER7uXgDJUQsTHFoIrI5yMm5A0bg0GiZ/kY5jNXTWoEy4KQtg35cE27sw1VbgzoMWHAgCckUUUww==} + /drizzle-orm@0.29.1(@types/pg@8.10.9)(pg@8.11.3): + resolution: {integrity: sha512-yItc4unfHnk8XkDD3/bdC63vdboTY7e7I03lCF1OJYABXSIfQYU9BFTQJXMMovVeb3T1/OJWwfW/70T1XPnuUA==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' '@cloudflare/workers-types': '>=3' @@ -5046,7 +5409,7 @@ packages: sqlite3: optional: true dependencies: - '@types/pg': 8.10.7 + '@types/pg': 8.10.9 pg: 8.11.3 dev: false @@ -5097,6 +5460,45 @@ packages: dependencies: once: 1.4.0 + /engine.io-client@6.5.3: + resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-parser: 5.2.1 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + engines: {node: '>=10.0.0'} + dev: false + + /engine.io@6.5.4: + resolution: {integrity: sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==} + engines: {node: '>=10.2.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.17 + '@types/node': 20.8.10 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.2.1 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /enhanced-resolve@5.12.0: resolution: {integrity: sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==} engines: {node: '>=10.13.0'} @@ -5311,7 +5713,7 @@ packages: source-map: 0.6.1 dev: true - /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.0)(eslint@8.52.0): + /eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.0)(eslint@8.55.0): resolution: {integrity: sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==} engines: {node: ^10.12.0 || >=12.0.0} peerDependencies: @@ -5319,14 +5721,14 @@ packages: eslint-plugin-import: ^2.25.2 dependencies: confusing-browser-globals: 1.0.11 - eslint: 8.52.0 - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + eslint: 8.55.0 + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) object.assign: 4.1.4 object.entries: 1.1.6 semver: 6.3.1 dev: true - /eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.52.0): + /eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.13.1)(@typescript-eslint/parser@6.10.0)(eslint-plugin-import@2.29.0)(eslint@8.55.0): resolution: {integrity: sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==} peerDependencies: '@typescript-eslint/eslint-plugin': ^5.13.0 || ^6.0.0 @@ -5334,14 +5736,14 @@ packages: eslint: ^7.32.0 || ^8.2.0 eslint-plugin-import: ^2.25.3 dependencies: - '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/parser': 6.10.0(eslint@8.52.0)(typescript@5.2.2) - eslint: 8.52.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.52.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.55.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.55.0)(typescript@5.2.2) + eslint: 8.55.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.55.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) dev: true - /eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@8.52.0): + /eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.0)(eslint-plugin-jsx-a11y@6.8.0)(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.33.2)(eslint@8.55.0): resolution: {integrity: sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==} engines: {node: ^10.12.0 || ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -5351,17 +5753,17 @@ packages: eslint-plugin-react: ^7.28.0 eslint-plugin-react-hooks: ^4.3.0 dependencies: - eslint: 8.52.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.52.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) - eslint-plugin-jsx-a11y: 6.8.0(eslint@8.52.0) - eslint-plugin-react: 7.33.2(eslint@8.52.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.52.0) + eslint: 8.55.0 + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.0)(eslint@8.55.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-plugin-jsx-a11y: 6.8.0(eslint@8.55.0) + eslint-plugin-react: 7.33.2(eslint@8.55.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.55.0) object.assign: 4.1.4 object.entries: 1.1.6 dev: true - /eslint-config-next@14.0.3(eslint@8.52.0)(typescript@5.2.2): + /eslint-config-next@14.0.3(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-IKPhpLdpSUyKofmsXUfrvBC49JMUTdeaD8ZIH4v9Vk0sC1X6URTuTJCLtA0Vwuj7V/CQh0oISuSTvNn5//Buew==} peerDependencies: eslint: ^7.23.0 || ^8.0.0 @@ -5372,27 +5774,27 @@ packages: dependencies: '@next/eslint-plugin-next': 14.0.3 '@rushstack/eslint-patch': 1.5.0 - '@typescript-eslint/parser': 6.10.0(eslint@8.52.0)(typescript@5.2.2) - eslint: 8.52.0 + '@typescript-eslint/parser': 6.10.0(eslint@8.55.0)(typescript@5.2.2) + eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.52.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) - eslint-plugin-jsx-a11y: 6.8.0(eslint@8.52.0) - eslint-plugin-react: 7.33.2(eslint@8.52.0) - eslint-plugin-react-hooks: 4.6.0(eslint@8.52.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.55.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-plugin-jsx-a11y: 6.8.0(eslint@8.55.0) + eslint-plugin-react: 7.33.2(eslint@8.55.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.55.0) typescript: 5.2.2 transitivePeerDependencies: - eslint-import-resolver-webpack - supports-color dev: true - /eslint-config-prettier@9.0.0(eslint@8.52.0): + /eslint-config-prettier@9.0.0(eslint@8.55.0): resolution: {integrity: sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==} hasBin: true peerDependencies: eslint: '>=7.0.0' dependencies: - eslint: 8.52.0 + eslint: 8.55.0 dev: true /eslint-import-resolver-node@0.3.9: @@ -5405,7 +5807,7 @@ packages: - supports-color dev: true - /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.52.0): + /eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.55.0): resolution: {integrity: sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg==} engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: @@ -5414,9 +5816,9 @@ packages: dependencies: debug: 4.3.4 enhanced-resolve: 5.12.0 - eslint: 8.52.0 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) - eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + eslint: 8.55.0 + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) + eslint-plugin-import: 2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) fast-glob: 3.3.1 get-tsconfig: 4.5.0 is-core-module: 2.13.0 @@ -5428,7 +5830,7 @@ packages: - supports-color dev: true - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): + /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} peerDependencies: @@ -5449,16 +5851,16 @@ packages: eslint-import-resolver-webpack: optional: true dependencies: - '@typescript-eslint/parser': 6.10.0(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.55.0)(typescript@5.2.2) debug: 3.2.7(supports-color@5.5.0) - eslint: 8.52.0 + eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.52.0) + eslint-import-resolver-typescript: 3.6.1(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.0)(eslint@8.55.0) transitivePeerDependencies: - supports-color dev: true - /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0): + /eslint-plugin-import@2.29.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0): resolution: {integrity: sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==} engines: {node: '>=4'} peerDependencies: @@ -5468,16 +5870,16 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 6.10.0(eslint@8.52.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.10.0(eslint@8.55.0)(typescript@5.2.2) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 - eslint: 8.52.0 + eslint: 8.55.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.52.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.10.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.55.0) hasown: 2.0.0 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -5493,7 +5895,7 @@ packages: - supports-color dev: true - /eslint-plugin-jest-dom@5.1.0(@testing-library/dom@9.3.3)(eslint@8.52.0): + /eslint-plugin-jest-dom@5.1.0(@testing-library/dom@9.3.3)(eslint@8.55.0): resolution: {integrity: sha512-JIXZp+E/h/aGlP/rQc4tuOejiHlZXg65qw8JAJMIJA5VsdjOkss/SYcRSqBrQuEOytEM8JvngUjcz31d1RrCrA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6', yarn: '>=1'} peerDependencies: @@ -5505,11 +5907,11 @@ packages: dependencies: '@babel/runtime': 7.22.6 '@testing-library/dom': 9.3.3 - eslint: 8.52.0 + eslint: 8.55.0 requireindex: 1.2.0 dev: true - /eslint-plugin-jest@27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.52.0)(jest@29.7.0)(typescript@5.2.2): + /eslint-plugin-jest@27.6.0(@typescript-eslint/eslint-plugin@6.13.1)(eslint@8.55.0)(jest@29.7.0)(typescript@5.2.2): resolution: {integrity: sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} peerDependencies: @@ -5522,16 +5924,16 @@ packages: jest: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.52.0)(typescript@5.2.2) - '@typescript-eslint/utils': 5.60.1(eslint@8.52.0)(typescript@5.2.2) - eslint: 8.52.0 + '@typescript-eslint/eslint-plugin': 6.13.1(@typescript-eslint/parser@6.10.0)(eslint@8.55.0)(typescript@5.2.2) + '@typescript-eslint/utils': 5.60.1(eslint@8.55.0)(typescript@5.2.2) + eslint: 8.55.0 jest: 29.7.0(@types/node@20.8.10)(ts-node@10.9.1) transitivePeerDependencies: - supports-color - typescript dev: true - /eslint-plugin-jsx-a11y@6.8.0(eslint@8.52.0): + /eslint-plugin-jsx-a11y@6.8.0(eslint@8.55.0): resolution: {integrity: sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==} engines: {node: '>=4.0'} peerDependencies: @@ -5547,7 +5949,7 @@ packages: damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 es-iterator-helpers: 1.0.15 - eslint: 8.52.0 + eslint: 8.55.0 hasown: 2.0.0 jsx-ast-utils: 3.3.5 language-tags: 1.0.9 @@ -5556,16 +5958,16 @@ packages: object.fromentries: 2.0.7 dev: true - /eslint-plugin-react-hooks@4.6.0(eslint@8.52.0): + /eslint-plugin-react-hooks@4.6.0(eslint@8.55.0): resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 dependencies: - eslint: 8.52.0 + eslint: 8.55.0 dev: true - /eslint-plugin-react@7.33.2(eslint@8.52.0): + /eslint-plugin-react@7.33.2(eslint@8.55.0): resolution: {integrity: sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==} engines: {node: '>=4'} peerDependencies: @@ -5576,7 +5978,7 @@ packages: array.prototype.tosorted: 1.1.1 doctrine: 2.1.0 es-iterator-helpers: 1.0.15 - eslint: 8.52.0 + eslint: 8.55.0 estraverse: 5.3.0 jsx-ast-utils: 3.3.3 minimatch: 3.1.2 @@ -5590,14 +5992,14 @@ packages: string.prototype.matchall: 4.0.8 dev: true - /eslint-plugin-testing-library@6.1.0(eslint@8.52.0)(typescript@5.2.2): + /eslint-plugin-testing-library@6.1.0(eslint@8.55.0)(typescript@5.2.2): resolution: {integrity: sha512-r7kE+az3tbp8vyRwfyAGZ6V/xw+XvdWFPicIo6jbOPZoossOFDeHizARqPGV6gEkyF8hyCFhhH3mlQOGS3N5Sg==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0, npm: '>=6'} peerDependencies: eslint: ^7.5.0 || ^8.0.0 dependencies: - '@typescript-eslint/utils': 5.60.1(eslint@8.52.0)(typescript@5.2.2) - eslint: 8.52.0 + '@typescript-eslint/utils': 5.60.1(eslint@8.55.0)(typescript@5.2.2) + eslint: 8.55.0 transitivePeerDependencies: - supports-color - typescript @@ -5624,15 +6026,15 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dev: true - /eslint@8.52.0: - resolution: {integrity: sha512-zh/JHnaixqHZsolRB/w9/02akBk9EPrOs9JwcTP2ek7yL5bVvXuRariiaAjjoJ5DvuwQ1WAE/HsMz+w17YgBCg==} + /eslint@8.55.0: + resolution: {integrity: sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} hasBin: true dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.52.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.55.0) '@eslint-community/regexpp': 4.9.0 - '@eslint/eslintrc': 2.1.2 - '@eslint/js': 8.52.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.55.0 '@humanwhocodes/config-array': 0.11.13 '@humanwhocodes/module-importer': 1.0.1 '@nodelib/fs.walk': 1.2.8 @@ -5710,6 +6112,10 @@ packages: engines: {node: '>=4.0'} dev: true + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: false + /esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -5807,6 +6213,17 @@ packages: micromatch: 4.0.5 dev: true + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + 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.5 + dev: true + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true @@ -5892,7 +6309,6 @@ packages: dependencies: locate-path: 6.0.0 path-exists: 4.0.0 - dev: true /flat-cache@3.0.4: resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} @@ -6168,6 +6584,16 @@ packages: once: 1.4.0 dev: false + /glob@9.3.2: + resolution: {integrity: sha512-BTv/JhKXFEHsErMte/AnfiSv8yYOLLiyH2lTg8vn02O21zWFgHPTfxtgn1QRe7NRgggUhC8hacR2Re94svHqeA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + fs.realpath: 1.0.0 + minimatch: 7.4.6 + minipass: 4.2.8 + path-scurry: 1.10.1 + dev: false + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -6198,15 +6624,16 @@ packages: slash: 3.0.0 dev: true - /globby@13.1.3: - resolution: {integrity: sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + /globby@14.0.0: + resolution: {integrity: sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==} + engines: {node: '>=18'} dependencies: - dir-glob: 3.0.1 - fast-glob: 3.3.1 + '@sindresorhus/merge-streams': 1.0.0 + fast-glob: 3.3.2 ignore: 5.2.4 - merge2: 1.4.1 - slash: 4.0.0 + path-type: 5.0.0 + slash: 5.1.0 + unicorn-magic: 0.1.0 dev: true /globrex@0.1.2: @@ -6254,7 +6681,6 @@ packages: /has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - dev: true /has-own-property@0.1.0: resolution: {integrity: sha512-14qdBKoonU99XDhWcFKZTShK+QV47qU97u8zzoVo9cL5TZ3BmBHXogItSt9qJjR0KUMFRhcCW8uGIGl8nkl7Aw==} @@ -6492,6 +6918,10 @@ packages: engines: {node: '>= 4'} dev: true + /immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + dev: false + /immutable@4.2.4: resolution: {integrity: sha512-WDxL3Hheb1JkRN3sQkyujNlL/xRjAo3rJtaU5xeufUauG66JdMr32bLj4gF+vWl84DIA3Zxw7tiAjneYzRRw+w==} @@ -6780,6 +7210,12 @@ packages: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} dev: true + /is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + dependencies: + '@types/estree': 1.0.5 + dev: false + /is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -6855,7 +7291,6 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} @@ -7550,21 +7985,25 @@ packages: engines: {node: '>=6'} dev: true - /knip@2.41.3: - resolution: {integrity: sha512-ooHaOfiieytMFSYnhhwk+TKFD3kGPNXIxpoLimEFf4nUpmthBOVKyawDjHvl23uJmPkqI6OOQqyQnK6dCUX+xQ==} - engines: {node: '>=16.17.0 <17 || >=18.6.0'} + /knip@3.6.1(@types/node@20.8.10)(typescript@5.2.2): + resolution: {integrity: sha512-ykcauRIaoSCz2YZ/Ymt8ymfQB+1wwaJV9CZKUCXjZ2/T9kMEv9Zj3Oy6a8zPBJQ6gnwkuxx5d5FOXd1TSpqX4Q==} + engines: {node: '>=18.6.0'} hasBin: true + peerDependencies: + '@types/node': '>=18' + typescript: '>=5.0.4' dependencies: '@ericcornelissen/bash-parser': 0.5.2 '@npmcli/map-workspaces': 3.0.4 '@pkgjs/parseargs': 0.11.0 '@pnpm/logger': 5.0.0 - '@pnpm/workspace.pkgs-graph': 2.0.10(@pnpm/logger@5.0.0) + '@pnpm/workspace.pkgs-graph': 2.0.11(@pnpm/logger@5.0.0) '@snyk/github-codeowners': 1.1.0 + '@types/node': 20.8.10 chalk: 5.3.0 easy-table: 1.2.0 - fast-glob: 3.3.1 - globby: 13.1.3 + fast-glob: 3.3.2 + globby: 14.0.0 jiti: 1.21.0 js-yaml: 4.1.0 micromatch: 4.0.5 @@ -7574,7 +8013,7 @@ packages: summary: 2.1.0 typescript: 5.2.2 zod: 3.22.4 - zod-validation-error: 1.5.0(zod@3.22.4) + zod-validation-error: 2.1.0(zod@3.22.4) transitivePeerDependencies: - domexception dev: true @@ -7619,6 +8058,12 @@ packages: type-check: 0.4.0 dev: true + /lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + dependencies: + immediate: 3.0.6 + dev: false + /lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -7637,6 +8082,12 @@ packages: engines: {node: '>=14'} dev: true + /localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + dependencies: + lie: 3.1.1 + dev: false + /locate-path@5.0.0: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} @@ -7649,7 +8100,6 @@ packages: engines: {node: '>=10'} dependencies: p-locate: 5.0.0 - dev: true /lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} @@ -7730,7 +8180,6 @@ packages: /lru-cache@10.0.1: resolution: {integrity: sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==} engines: {node: 14 || >=16.14} - dev: true /lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -7759,6 +8208,13 @@ packages: vlq: 0.2.3 dev: true + /magic-string@0.27.0: + resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: false + /magic-string@0.30.3: resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} engines: {node: '>=12'} @@ -8291,6 +8747,13 @@ packages: brace-expansion: 2.0.1 dev: false + /minimatch@7.4.6: + resolution: {integrity: sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + /minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -8313,10 +8776,14 @@ packages: engines: {node: '>=8'} dev: false + /minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + dev: false + /minipass@7.0.3: resolution: {integrity: sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==} engines: {node: '>=16 || 14 >=14.17'} - dev: true /minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} @@ -8329,6 +8796,13 @@ packages: /mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.8 + dev: false + /mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -8858,7 +9332,6 @@ packages: engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 - dev: true /p-limit@4.0.0: resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} @@ -8879,7 +9352,6 @@ packages: engines: {node: '>=10'} dependencies: p-limit: 3.1.0 - dev: true /p-map@4.0.0: resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==} @@ -8945,7 +9417,6 @@ packages: /path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - dev: true /path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} @@ -8965,7 +9436,6 @@ packages: dependencies: lru-cache: 10.0.1 minipass: 7.0.3 - dev: true /path-temp@2.1.0: resolution: {integrity: sha512-cMMJTAZlion/RWRRC48UbrDymEIt+/YSD/l8NqjneyDw2rDOBQcP5yRkMB4CYGn47KMhZvbblBP7Z79OsMw72w==} @@ -8982,6 +9452,11 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /path-type@5.0.0: + resolution: {integrity: sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==} + engines: {node: '>=12'} + dev: true + /pathe@1.1.1: resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} dev: true @@ -9273,7 +9748,6 @@ packages: /progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} - dev: true /prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} @@ -9407,8 +9881,8 @@ packages: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true - /react-markdown@9.0.0(@types/react@18.2.39)(react@18.2.0): - resolution: {integrity: sha512-v6yNf3AB8GfJ8lCpUvzxAXKxgsHpdmWPlcVRQ6Nocsezp255E/IDrF31kLQsPJeB/cKto/geUwjU36wH784FCA==} + /react-markdown@9.0.1(@types/react@18.2.39)(react@18.2.0): + resolution: {integrity: sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==} peerDependencies: '@types/react': '>=18' react: '>=18' @@ -9419,7 +9893,6 @@ packages: hast-util-to-jsx-runtime: 2.2.0 html-url-attributes: 3.0.0 mdast-util-to-hast: 13.0.2 - micromark-util-sanitize-uri: 2.0.0 react: 18.2.0 remark-parse: 11.0.0 remark-rehype: 11.0.0 @@ -9694,8 +10167,8 @@ packages: unified: 11.0.3 dev: false - /rename-overwrite@4.0.3: - resolution: {integrity: sha512-e1zOWZh4Lauz5DcLMC8j4eoOHPIrZkAVpiocE9SkDE1ZrGMW+W88LR1Y2YjD1DFgOYfJWqSsK6JKsRfuRH+tbQ==} + /rename-overwrite@4.0.4: + resolution: {integrity: sha512-5MC+p5npnyaJlFkwTHb0pqU2mkUkkW65ZWH8qwxcDlv+5nchtalcdzG+gaaianbWWwvwxi7vu7WSg6jdCweKug==} engines: {node: '>=12.10'} dependencies: '@zkochan/rimraf': 2.1.3 @@ -9798,6 +10271,14 @@ packages: dependencies: glob: 7.2.3 + /rollup@2.78.0: + resolution: {integrity: sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: false + /rollup@3.29.4: resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} engines: {node: '>=14.18.0', npm: '>=8.0.0'} @@ -9988,9 +10469,9 @@ packages: engines: {node: '>=8'} dev: true - /slash@4.0.0: - resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==} - engines: {node: '>=12'} + /slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} dev: true /slice-ansi@5.0.0: @@ -10001,6 +10482,56 @@ packages: is-fullwidth-code-point: 4.0.0 dev: false + /socket.io-adapter@2.5.2: + resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + dependencies: + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /socket.io-client@4.7.2: + resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-client: 6.5.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /socket.io@4.7.2: + resolution: {integrity: sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==} + engines: {node: '>=10.2.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.4 + socket.io-adapter: 2.5.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -10012,13 +10543,6 @@ packages: source-map: 0.6.1 dev: true - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: true - /source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -10075,6 +10599,13 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true + /stacktrace-parser@0.1.10: + resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} + engines: {node: '>=6'} + dependencies: + type-fest: 0.7.1 + dev: false + /standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} dev: false @@ -10290,7 +10821,6 @@ packages: engines: {node: '>=8'} dependencies: has-flag: 4.0.0 - dev: true /supports-color@8.1.1: resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} @@ -10619,13 +11149,13 @@ packages: typescript: 5.2.2 dev: true - /tsx@3.14.0: - resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} + /tsx@4.6.2: + resolution: {integrity: sha512-QPpBdJo+ZDtqZgAnq86iY/PD2KYCUPSUGIunHdGwyII99GKH+f3z3FZ8XNFLSGQIA4I365ui8wnQpl8OKLqcsg==} + engines: {node: '>=18.0.0'} hasBin: true dependencies: esbuild: 0.18.20 get-tsconfig: 4.7.2 - source-map-support: 0.5.21 optionalDependencies: fsevents: 2.3.3 dev: true @@ -10669,6 +11199,11 @@ packages: engines: {node: '>=8'} dev: true + /type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + dev: false + /type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} @@ -10755,6 +11290,11 @@ packages: string.fromcodepoint: 0.2.1 dev: true + /unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + dev: true + /unified@11.0.3: resolution: {integrity: sha512-jlCV402P+YDcFcB2VcN/n8JasOddqIiaxv118wNBoZXEhOn+lYG7BR4Bfg2BwxvlK58dwbuH2w7GX2esAjL6Mg==} dependencies: @@ -10816,6 +11356,15 @@ packages: resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} engines: {node: '>= 10.0.0'} + /unplugin@1.0.1: + resolution: {integrity: sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA==} + dependencies: + acorn: 8.10.0 + chokidar: 3.5.3 + webpack-sources: 3.2.3 + webpack-virtual-modules: 0.5.0 + dev: false + /update-browserslist-db@1.0.13(browserslist@4.22.1): resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} hasBin: true @@ -10949,6 +11498,11 @@ packages: engines: {node: '>= 0.10'} dev: false + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false + /version-selector-type@3.0.0: resolution: {integrity: sha512-PSvMIZS7C1MuVNBXl/CDG2pZq8EXy/NW2dHIdm3bVP5N0PC8utDK8ttXLXj44Gn3J0lQE3U7Mpm1estAOd+eiA==} engines: {node: '>=10.13'} @@ -10988,7 +11542,7 @@ packages: mlly: 1.4.2 pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.5.0(@types/node@20.8.10)(sass@1.69.5) + vite: 4.5.1(@types/node@20.8.10)(sass@1.69.5) transitivePeerDependencies: - '@types/node' - less @@ -11000,7 +11554,7 @@ packages: - terser dev: true - /vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.5.0): + /vite-tsconfig-paths@4.2.1(typescript@5.2.2)(vite@4.5.1): resolution: {integrity: sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==} peerDependencies: vite: '*' @@ -11011,14 +11565,14 @@ packages: debug: 4.3.4 globrex: 0.1.2 tsconfck: 2.1.1(typescript@5.2.2) - vite: 4.5.0(@types/node@20.8.10)(sass@1.69.5) + vite: 4.5.1(@types/node@20.8.10)(sass@1.69.5) transitivePeerDependencies: - supports-color - typescript dev: true - /vite@4.5.0(@types/node@20.8.10)(sass@1.69.5): - resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} + /vite@4.5.1(@types/node@20.8.10)(sass@1.69.5): + resolution: {integrity: sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -11106,7 +11660,7 @@ packages: strip-literal: 1.0.1 tinybench: 2.5.0 tinypool: 0.7.0 - vite: 4.5.0(@types/node@20.8.10)(sass@1.69.5) + vite: 4.5.1(@types/node@20.8.10)(sass@1.69.5) vite-node: 0.34.6(@types/node@20.8.10)(sass@1.69.5) why-is-node-running: 2.2.2 transitivePeerDependencies: @@ -11187,6 +11741,15 @@ packages: engines: {node: '>=12'} dev: true + /webpack-sources@3.2.3: + resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} + engines: {node: '>=10.13.0'} + dev: false + + /webpack-virtual-modules@0.5.0: + resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} + dev: false + /whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} @@ -11267,7 +11830,6 @@ packages: hasBin: true dependencies: isexe: 2.0.0 - dev: true /why-is-node-running@2.2.2: resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} @@ -11350,6 +11912,19 @@ packages: signal-exit: 3.0.7 dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /ws@8.12.1: resolution: {integrity: sha512-1qo+M9Ba+xNhPB+YTWUlK6M17brTut5EXbcBaMRN5pH5dFrXz7lzz1ChFSUq3bOUl8yEvSenhHmYUNJxFzdJew==} engines: {node: '>=10.0.0'} @@ -11372,6 +11947,11 @@ packages: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} dev: true + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -11437,16 +12017,15 @@ packages: /yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - dev: true /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} dev: true - /zod-validation-error@1.5.0(zod@3.22.4): - resolution: {integrity: sha512-/7eFkAI4qV0tcxMBB/3+d2c1P6jzzZYdYSlBuAklzMuCrJu5bzJfHS0yVAS87dRHVlhftd6RFJDIvv03JgkSbw==} - engines: {node: '>=16.0.0'} + /zod-validation-error@2.1.0(zod@3.22.4): + resolution: {integrity: sha512-VJh93e2wb4c3tWtGgTa0OF/dTt/zoPCPzXq4V11ZjxmEAFaPi/Zss1xIZdEB5RD8GD00U0/iVXgqkF77RV7pdQ==} + engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.18.0 dependencies: diff --git a/sentry.client.config.ts b/sentry.client.config.ts new file mode 100644 index 0000000000..e90d1b07df --- /dev/null +++ b/sentry.client.config.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/nextjs'; +import { settingsSchema } from '@runtipi/shared/src/schemas/env-schemas'; + +const inputElement = document.getElementById('client-settings') as HTMLInputElement | null; + +if (inputElement) { + try { + // Parse the input value + const parsedSettings = settingsSchema.parse(JSON.parse(inputElement.value)); + + if (parsedSettings.allowErrorMonitoring) { + Sentry.init({ + environment: process.env.NODE_ENV, + dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584', + debug: process.env.NODE_ENV === 'development', + }); + } + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error parsing client settings:', error); + } +} diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts new file mode 100644 index 0000000000..634d97676a --- /dev/null +++ b/sentry.edge.config.ts @@ -0,0 +1,16 @@ +// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). +// The config you add here will be used whenever one of the edge features is loaded. +// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; +import { getConfig } from '@/server/core/TipiConfig'; + +if (getConfig().allowErrorMonitoring) { + Sentry.init({ + environment: getConfig().NODE_ENV, + dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584', + tracesSampleRate: 1, + debug: getConfig().NODE_ENV === 'development', + }); +} diff --git a/sentry.server.config.ts b/sentry.server.config.ts new file mode 100644 index 0000000000..65d48aead0 --- /dev/null +++ b/sentry.server.config.ts @@ -0,0 +1,15 @@ +// This file configures the initialization of Sentry on the server. +// The config you add here will be used whenever the server handles a request. +// https://docs.sentry.io/platforms/javascript/guides/nextjs/ + +import * as Sentry from '@sentry/nextjs'; +import { getConfig } from '@/server/core/TipiConfig'; + +if (getConfig().allowErrorMonitoring) { + Sentry.init({ + environment: getConfig().NODE_ENV, + dsn: 'https://7a73d72f886948478b55621e7b92c3c7@o4504242900238336.ingest.sentry.io/4504826587971584', + tracesSampleRate: 1, + debug: getConfig().NODE_ENV === 'development', + }); +} diff --git a/src/app/(auth)/login/components/LoginContainer/LoginContainer.tsx b/src/app/(auth)/login/components/LoginContainer/LoginContainer.tsx index 067f874eea..1c38956396 100644 --- a/src/app/(auth)/login/components/LoginContainer/LoginContainer.tsx +++ b/src/app/(auth)/login/components/LoginContainer/LoginContainer.tsx @@ -14,10 +14,11 @@ export function LoginContainer() { const router = useRouter(); const loginMutation = useAction(loginAction, { + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } else if (data.success && data.totpSessionId) { + if (data.success && data.totpSessionId) { setTotpSessionId(data.totpSessionId); } else { router.push('/dashboard'); @@ -26,18 +27,27 @@ export function LoginContainer() { }); const verifyTotpMutation = useAction(verifyTotpAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } else { - router.push('/dashboard'); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onSuccess: () => { + router.push('/dashboard'); }, }); if (totpSessionId) { - return verifyTotpMutation.execute({ totpCode, totpSessionId })} />; + return ( + verifyTotpMutation.execute({ totpCode, totpSessionId })} + /> + ); } - return loginMutation.execute({ username: email, password })} />; + return ( + loginMutation.execute({ username: email, password })} + /> + ); } diff --git a/src/app/(auth)/register/components/RegisterContainer/RegisterContainer.tsx b/src/app/(auth)/register/components/RegisterContainer/RegisterContainer.tsx index 5941105a16..2df0a287cc 100644 --- a/src/app/(auth)/register/components/RegisterContainer/RegisterContainer.tsx +++ b/src/app/(auth)/register/components/RegisterContainer/RegisterContainer.tsx @@ -11,14 +11,18 @@ export const RegisterContainer: React.FC = () => { const router = useRouter(); const registerMutation = useAction(registerAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } else { - router.push('/dashboard'); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onSuccess: () => { + router.push('/dashboard'); }, }); - return registerMutation.execute({ username: email, password })} loading={registerMutation.status === 'executing'} />; + return ( + registerMutation.execute({ username: email, password })} + loading={registerMutation.status === 'executing'} + /> + ); }; diff --git a/src/app/(auth)/reset-password/components/ResetPasswordContainer/ResetPasswordContainer.tsx b/src/app/(auth)/reset-password/components/ResetPasswordContainer/ResetPasswordContainer.tsx index a0bb6fa5bd..422bd19978 100644 --- a/src/app/(auth)/reset-password/components/ResetPasswordContainer/ResetPasswordContainer.tsx +++ b/src/app/(auth)/reset-password/components/ResetPasswordContainer/ResetPasswordContainer.tsx @@ -15,18 +15,14 @@ export const ResetPasswordContainer: React.FC = () => { const router = useRouter(); const resetPasswordMutation = useAction(resetPasswordAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } + onError: (error) => { + if (error.serverError) toast.error(error.serverError); }, }); const cancelRequestMutation = useAction(cancelResetPasswordAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } + onError: (error) => { + if (error.serverError) toast.error(error.serverError); }, }); diff --git a/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/AppDetailsContainer.tsx b/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/AppDetailsContainer.tsx index ba6526c935..e4ccb5898c 100644 --- a/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/AppDetailsContainer.tsx +++ b/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/AppDetailsContainer.tsx @@ -1,5 +1,3 @@ -'use client'; - import React from 'react'; import { toast } from 'react-hot-toast'; import { useTranslations } from 'next-intl'; @@ -13,10 +11,10 @@ import { updateAppAction } from '@/actions/app-actions/update-app-action'; import { updateAppConfigAction } from '@/actions/app-actions/update-app-config-action'; import { AppLogo } from '@/components/AppLogo'; import { AppStatus } from '@/components/AppStatus'; -import { AppStatus as AppStatusEnum } from '@/server/db/schema'; import { castAppConfig } from '@/lib/helpers/castAppConfig'; import { AppService } from '@/server/services/apps/apps.service'; import { resetAppAction } from '@/actions/app-actions/reset-app-action'; +import { AppStatus as AppStatusEnum } from '@/server/db/schema'; import { InstallModal } from '../InstallModal'; import { StopModal } from '../StopModal'; import { UninstallModal } from '../UninstallModal'; @@ -24,19 +22,20 @@ import { UpdateModal } from '../UpdateModal'; import { UpdateSettingsModal } from '../UpdateSettingsModal/UpdateSettingsModal'; import { AppActions } from '../AppActions'; import { AppDetailsTabs } from '../AppDetailsTabs'; -import { FormValues } from '../InstallForm'; import { ResetAppModal } from '../ResetAppModal'; -interface IProps { - app: Awaited>; - localDomain?: string; -} type OpenType = 'local' | 'domain' | 'local_domain'; -export const AppDetailsContainer: React.FC = ({ app, localDomain }) => { - const [customStatus, setCustomStatus] = React.useState(app.status); +type AppDetailsContainerProps = { + app: Awaited>; + localDomain?: string; + optimisticStatus: AppStatusEnum; + setOptimisticStatus: (status: AppStatusEnum) => void; +}; +export const AppDetailsContainer: React.FC = ({ app, localDomain, optimisticStatus, setOptimisticStatus }) => { const t = useTranslations(); + const installDisclosure = useDisclosure(); const uninstallDisclosure = useDisclosure(); const stopDisclosure = useDisclosure(); @@ -45,130 +44,78 @@ export const AppDetailsContainer: React.FC = ({ app, localDomain }) => { const resetAppDisclosure = useDisclosure(); const installMutation = useAction(installAppAction, { - onSuccess: (data) => { - if (!data.success) { - setCustomStatus(app.status); - toast.error(data.failure.reason); - } else { - setCustomStatus('running'); - toast.success(t('apps.app-details.install-success')); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onExecute: () => { + setOptimisticStatus('installing'); + installDisclosure.close(); }, }); const uninstallMutation = useAction(uninstallAppAction, { - onSuccess: (data) => { - if (!data.success) { - setCustomStatus(app.status); - toast.error(data.failure.reason); - } else { - setCustomStatus('missing'); - toast.success(t('apps.app-details.uninstall-success')); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onExecute: () => { + uninstallDisclosure.close(); + setOptimisticStatus('uninstalling'); }, }); const stopMutation = useAction(stopAppAction, { - onSuccess: (data) => { - if (!data.success) { - setCustomStatus(app.status); - toast.error(data.failure.reason); - } else { - setCustomStatus('stopped'); - toast.success(t('apps.app-details.stop-success')); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onExecute: () => { + stopDisclosure.close(); + setOptimisticStatus('stopping'); }, }); const startMutation = useAction(startAppAction, { - onSuccess: (data) => { - if (!data.success) { - setCustomStatus(app.status); - toast.error(data.failure.reason); - } else { - setCustomStatus('running'); - toast.success(t('apps.app-details.start-success')); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onExecute: () => { + setOptimisticStatus('starting'); }, }); const updateMutation = useAction(updateAppAction, { - onSuccess: (data) => { - setCustomStatus(app.status); - - if (!data.success) { - toast.error(data.failure.reason); - } else { - toast.success(t('apps.app-details.update-success')); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onExecute: () => { + updateDisclosure.close(); + setOptimisticStatus('updating'); }, }); const updateConfigMutation = useAction(updateAppConfigAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } else { - toast.success(t('apps.app-details.update-config-success')); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onExecute: () => { + updateSettingsDisclosure.close(); + }, + onSuccess: () => { + toast.success(t('apps.app-details.update-config-success')); }, }); const resetMutation = useAction(resetAppAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - resetAppDisclosure.close(); - } else { - resetAppDisclosure.close(); - toast.success(t('apps.app-details.app-reset-success')); - setCustomStatus('running'); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onExecute: () => { + resetAppDisclosure.open(); + setOptimisticStatus('stopping'); }, }); const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0); - const handleInstallSubmit = async (values: FormValues) => { - setCustomStatus('installing'); - installDisclosure.close(); - installMutation.execute({ id: app.id, form: values }); - }; - - const handleUnistallSubmit = () => { - setCustomStatus('uninstalling'); - uninstallDisclosure.close(); - uninstallMutation.execute({ id: app.id }); - }; - - const handleStopSubmit = () => { - setCustomStatus('stopping'); - stopDisclosure.close(); - stopMutation.execute({ id: app.id }); - }; - - const handleStartSubmit = async () => { - setCustomStatus('starting'); - startMutation.execute({ id: app.id }); - }; - - const handleUpdateSettingsSubmit = async (values: FormValues) => { - updateSettingsDisclosure.close(); - updateConfigMutation.execute({ id: app.id, form: values }); - }; - - const handleUpdateSubmit = async () => { - setCustomStatus('updating'); - updateDisclosure.close(); - updateMutation.execute({ id: app.id }); - }; - - const handleResetSubmit = () => { - setCustomStatus('stopping'); - resetMutation.execute({ id: app.id }); - resetAppDisclosure.open(); - }; - const openResetAppModal = () => { updateSettingsDisclosure.close(); resetAppDisclosure.open(); @@ -200,19 +147,46 @@ export const AppDetailsContainer: React.FC = ({ app, localDomain }) => { return (
- - - - - + installMutation.execute({ id: app.id, form: values })} + isOpen={installDisclosure.isOpen} + onClose={installDisclosure.close} + info={app.info} + /> + stopMutation.execute({ id: app.id })} + isOpen={stopDisclosure.isOpen} + onClose={stopDisclosure.close} + info={app.info} + /> + uninstallMutation.execute({ id: app.id })} + isOpen={uninstallDisclosure.isOpen} + onClose={uninstallDisclosure.close} + info={app.info} + /> + updateMutation.execute({ id: app.id })} + isOpen={updateDisclosure.isOpen} + onClose={updateDisclosure.close} + info={app.info} + newVersion={newVersion} + /> + resetMutation.execute({ id: app.id })} + isOpen={resetAppDisclosure.isOpen} + onClose={resetAppDisclosure.close} + info={app.info} + isLoading={resetMutation.status === 'executing'} + /> updateConfigMutation.execute({ id: app.id, form: values })} isOpen={updateSettingsDisclosure.isOpen} onClose={updateSettingsDisclosure.close} info={app.info} config={castAppConfig(app?.config)} onReset={openResetAppModal} - status={customStatus} + status={optimisticStatus} />
@@ -222,7 +196,7 @@ export const AppDetailsContainer: React.FC = ({ app, localDomain }) => { {app.info.version}
{app.info.short_desc} -
{customStatus !== 'missing' && }
+
{optimisticStatus !== 'missing' && }
= ({ app, localDomain }) => { onUninstall={uninstallDisclosure.open} onInstall={installDisclosure.open} onOpen={handleOpen} - onStart={handleStartSubmit} + onStart={() => startMutation.execute({ id: app.id })} app={app} - status={customStatus} + status={optimisticStatus} />
diff --git a/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/AppDetailsWrapper.tsx b/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/AppDetailsWrapper.tsx new file mode 100644 index 0000000000..791bce8761 --- /dev/null +++ b/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/AppDetailsWrapper.tsx @@ -0,0 +1,88 @@ +'use client'; + +import React, { startTransition, useOptimistic } from 'react'; +import { useSocket } from '@/lib/socket/useSocket'; +import { AppStatus } from '@/server/db/schema'; +import { AppService } from '@/server/services/apps/apps.service'; +import { useTranslations } from 'next-intl'; +import toast from 'react-hot-toast'; +import { useAction } from 'next-safe-action/hook'; +import { revalidateAppAction } from '@/actions/app-actions/revalidate-app'; +import { AppDetailsContainer } from './AppDetailsContainer'; + +interface IProps { + app: Awaited>; + localDomain?: string; +} + +export const AppDetailsWrapper = (props: IProps) => { + const { app, localDomain } = props; + const t = useTranslations(); + const [optimisticStatus, setOptimisticStatus] = useOptimistic(app.status); + const revalidateAppMutation = useAction(revalidateAppAction); + + const changeStatus = (status: AppStatus) => { + startTransition(() => { + setOptimisticStatus(status); + }); + }; + + useSocket({ + onEvent: (event, data) => { + if (data.error) { + // eslint-disable-next-line no-console + console.error(data.error); + } + + switch (event) { + case 'install_success': + toast.success(t('apps.app-details.install-success')); + changeStatus('running'); + break; + case 'install_error': + toast.error(t('server-messages.errors.app-failed-to-install', { id: app.id })); + changeStatus('missing'); + break; + case 'start_success': + toast.success(t('apps.app-details.start-success')); + changeStatus('running'); + break; + case 'start_error': + toast.error(t('server-messages.errors.app-failed-to-start', { id: app.id })); + changeStatus('stopped'); + break; + case 'stop_success': + toast.success(t('apps.app-details.stop-success')); + changeStatus('stopped'); + break; + case 'stop_error': + toast.error(t('server-messages.errors.app-failed-to-stop', { id: app.id })); + changeStatus('running'); + break; + case 'uninstall_success': + toast.success(t('apps.app-details.uninstall-success')); + changeStatus('missing'); + break; + case 'uninstall_error': + toast.error(t('server-messages.errors.app-failed-to-uninstall', { id: app.id })); + changeStatus('stopped'); + break; + case 'update_success': + toast.success(t('apps.app-details.update-success')); + changeStatus('running'); + break; + case 'update_error': + toast.error(t('server-messages.errors.app-failed-to-update', { id: app.id })); + changeStatus('stopped'); + break; + default: + break; + } + + revalidateAppMutation.execute({ id: app.id }); + }, + selector: { type: 'app', data: { property: 'appId', value: app.id } }, + }); + + return ; +}; diff --git a/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/index.ts b/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/index.ts index e69de29bb2..ff74643bfc 100644 --- a/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/index.ts +++ b/src/app/(dashboard)/app-store/[id]/components/AppDetailsContainer/index.ts @@ -0,0 +1 @@ +export { AppDetailsWrapper } from './AppDetailsWrapper'; diff --git a/src/app/(dashboard)/app-store/[id]/components/InstallForm/InstallForm.tsx b/src/app/(dashboard)/app-store/[id]/components/InstallForm/InstallForm.tsx index cebe2eb61a..3aaeddc413 100644 --- a/src/app/(dashboard)/app-store/[id]/components/InstallForm/InstallForm.tsx +++ b/src/app/(dashboard)/app-store/[id]/components/InstallForm/InstallForm.tsx @@ -71,7 +71,9 @@ export const InstallForm: React.FC = ({ formFields, info, onSubmit, init {field.required && *} {Boolean(field.hint) && ( <> - {field.hint} + + {field.hint} + ? )} @@ -84,7 +86,9 @@ export const InstallForm: React.FC = ({ formFields, info, onSubmit, init control={control} name={field.env_variable} defaultValue={field.default} - render={({ field: { onChange, value, ref, ...props } }) => } + render={({ field: { onChange, value, ref, ...props } }) => ( + + )} /> ); } @@ -133,7 +137,15 @@ export const InstallForm: React.FC = ({ formFields, info, onSubmit, init name="exposed" defaultValue={false} render={({ field: { onChange, value, ref, ...props } }) => ( - + )} /> {watchExposed && ( @@ -167,7 +179,15 @@ export const InstallForm: React.FC = ({ formFields, info, onSubmit, init name="isVisibleOnGuestDashboard" defaultValue={false} render={({ field: { onChange, value, ref, ...props } }) => ( - + )} /> {info.exposable && renderExposeForm()} diff --git a/src/app/(dashboard)/app-store/[id]/page.tsx b/src/app/(dashboard)/app-store/[id]/page.tsx index 1332b51ca6..8f6e791245 100644 --- a/src/app/(dashboard)/app-store/[id]/page.tsx +++ b/src/app/(dashboard)/app-store/[id]/page.tsx @@ -2,15 +2,13 @@ import { AppServiceClass } from '@/server/services/apps/apps.service'; import React from 'react'; import { Metadata } from 'next'; import { db } from '@/server/db'; -import { getTranslatorFromCookie } from '@/lib/get-translator'; import { getSettings } from '@/server/core/TipiConfig'; -import { AppDetailsContainer } from './components/AppDetailsContainer/AppDetailsContainer'; +import { AppDetailsWrapper } from './components/AppDetailsContainer'; -export async function generateMetadata(): Promise { - const translator = await getTranslatorFromCookie(); +export async function generateMetadata({ params }: { params: { id: string } }): Promise { return { - title: `${translator('apps.app-store.title')} - Tipi`, + title: `${params.id} - Tipi`, }; } @@ -19,5 +17,5 @@ export default async function AppDetailsPage({ params }: { params: { id: string const app = await appsService.getApp(params.id); const settings = getSettings(); - return ; -} + return ; +} \ No newline at end of file diff --git a/src/app/(dashboard)/components/Header/Header.tsx b/src/app/(dashboard)/components/Header/Header.tsx index 8f7713b491..3d94952041 100644 --- a/src/app/(dashboard)/components/Header/Header.tsx +++ b/src/app/(dashboard)/components/Header/Header.tsx @@ -14,17 +14,18 @@ import { logoutAction } from '@/actions/logout/logout-action'; import Script from 'next/script'; import { useRouter } from 'next/navigation'; import { getLogo } from '@/lib/themes'; +import { useClientSettings } from '@/hooks/use-client-settings'; import { NavBar } from '../NavBar'; interface IProps { isUpdateAvailable?: boolean; authenticated?: boolean; - autoTheme: boolean; } -export const Header: React.FC = ({ isUpdateAvailable, authenticated = true, autoTheme }) => { +export const Header: React.FC = ({ isUpdateAvailable, authenticated = true }) => { const { setDarkMode } = useUIStore(); const t = useTranslations('header'); + const { allowAutoThemes = true } = useClientSettings(); const router = useRouter(); @@ -57,7 +58,7 @@ export const Header: React.FC = ({ isUpdateAvailable, authenticated = tr className={clsx('navbar-brand-image me-3')} width={100} height={100} - src={getLogo(autoTheme)} + src={getLogo(allowAutoThemes)} style={{ width: '30px', maxWidth: '30px', @@ -81,16 +82,40 @@ export const Header: React.FC = ({ isUpdateAvailable, authenticated = tr
- {t('dark-mode')} -
setDarkMode(true)} role="button" aria-hidden="true" className="darkMode nav-link px-0 hide-theme-dark cursor-pointer" data-testid="dark-mode-toggle"> + + {t('dark-mode')} + +
setDarkMode(true)} + role="button" + aria-hidden="true" + className="darkMode nav-link px-0 hide-theme-dark cursor-pointer" + data-testid="dark-mode-toggle" + >
- {t('light-mode')} -
setDarkMode(false)} aria-hidden="true" className="lightMode nav-link px-0 hide-theme-light cursor-pointer" data-testid="light-mode-toggle"> + + {t('light-mode')} + +
setDarkMode(false)} + aria-hidden="true" + className="lightMode nav-link px-0 hide-theme-light cursor-pointer" + data-testid="light-mode-toggle" + >
- {authenticated ? t('logout') : t('login')} -
logHandler()} tabIndex={0} onKeyPress={() => logHandler()} role="button" className="logOut nav-link px-0 cursor-pointer" data-testid="logout-button"> + + {authenticated ? t('logout') : t('login')} + +
logHandler()} + tabIndex={0} + onKeyPress={() => logHandler()} + role="button" + className="logOut nav-link px-0 cursor-pointer" + data-testid="logout-button" + > {authenticated ? : }
diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index b8ea25add9..24f08a37e5 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -5,7 +5,6 @@ import { SystemServiceClass } from '@/server/services/system'; import semver from 'semver'; import clsx from 'clsx'; import { AppServiceClass } from '@/server/services/apps/apps.service'; -import { getConfig } from '@/server/core/TipiConfig'; import { Header } from './components/Header'; import { PageTitle } from './components/PageTitle'; import styles from './layout.module.scss'; @@ -13,7 +12,6 @@ import { LayoutActions } from './components/LayoutActions/LayoutActions'; export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const user = await getUserFromCookie(); - const { allowAutoThemes } = getConfig(); const { apps } = await AppServiceClass.listApps(); @@ -27,7 +25,7 @@ export default async function DashboardLayout({ children }: { children: React.Re return (
-
+
diff --git a/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.tsx b/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.tsx index 85bb9eb776..05fffe9824 100644 --- a/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.tsx +++ b/src/app/(dashboard)/settings/components/ChangePasswordForm/ChangePasswordForm.tsx @@ -34,13 +34,12 @@ export const ChangePasswordForm = () => { const router = useRouter(); const changePasswordMutation = useAction(changePasswordAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } else { - toast.success(t('password-change-success')); - router.push('/'); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onSuccess: () => { + toast.success(t('password-change-success')); + router.push('/'); }, }); diff --git a/src/app/(dashboard)/settings/components/ChangeUsernameForm/ChangeUsernameForm.tsx b/src/app/(dashboard)/settings/components/ChangeUsernameForm/ChangeUsernameForm.tsx index 924226c8ae..9de9d4f596 100644 --- a/src/app/(dashboard)/settings/components/ChangeUsernameForm/ChangeUsernameForm.tsx +++ b/src/app/(dashboard)/settings/components/ChangeUsernameForm/ChangeUsernameForm.tsx @@ -27,13 +27,12 @@ export const ChangeUsernameForm = ({ username }: Props) => { type FormValues = z.infer; const changeUsernameMutation = useAction(changeUsernameAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } else { - toast.success(t('change-username.success')); - router.push('/'); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onSuccess: () => { + toast.success(t('change-username.success')); + router.push('/'); }, }); diff --git a/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx b/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx index 5591a06bff..27dc100b1c 100644 --- a/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx +++ b/src/app/(dashboard)/settings/components/OtpForm/OtpForm.tsx @@ -29,28 +29,26 @@ export const OtpForm = (props: { totpEnabled: boolean }) => { onExecute: () => { setupOtpDisclosure.close(); }, + onError: (e) => { + setPassword(''); + if (e.serverError) toast.error(e.serverError); + }, onSuccess: (data) => { - if (!data.success) { - setPassword(''); - toast.error(data.failure.reason); - } else { - setKey(data.key); - setUri(data.uri); - } + setKey(data.key); + setUri(data.uri); }, }); const setupTotpMutation = useAction(setupTotpAction, { - onSuccess: (data) => { - if (!data.success) { - setTotpCode(''); - toast.error(data.failure.reason); - } else { - setTotpCode(''); - setKey(''); - setUri(''); - toast.success(t('2fa-enable-success')); - } + onError: (e) => { + setTotpCode(''); + if (e.serverError) toast.error(e.serverError); + }, + onSuccess: () => { + setTotpCode(''); + setKey(''); + setUri(''); + toast.success(t('2fa-enable-success')); }, }); @@ -58,13 +56,12 @@ export const OtpForm = (props: { totpEnabled: boolean }) => { onExecute: () => { disableOtpDisclosure.close(); }, - onSuccess: (data) => { - if (!data.success) { - setPassword(''); - toast.error(data.failure.reason); - } else { - toast.success(t('2fa-disable-success')); - } + onError: (e) => { + setPassword(''); + if (e.serverError) toast.error(e.serverError); + }, + onSuccess: () => { + toast.success(t('2fa-disable-success')); }, }); diff --git a/src/app/(dashboard)/settings/components/SettingsContainer/SettingsContainer.tsx b/src/app/(dashboard)/settings/components/SettingsContainer/SettingsContainer.tsx index b7bc96b786..84d71c0ee0 100644 --- a/src/app/(dashboard)/settings/components/SettingsContainer/SettingsContainer.tsx +++ b/src/app/(dashboard)/settings/components/SettingsContainer/SettingsContainer.tsx @@ -20,13 +20,12 @@ export const SettingsContainer = ({ initialValues, currentLocale }: Props) => { const router = useRouter(); const updateSettingsMutation = useAction(updateSettingsAction, { - onSuccess: (data) => { - if (!data.success) { - toast.error(data.failure.reason); - } else { - toast.success(t('settings.settings.settings-updated')); - router.refresh(); - } + onError: (e) => { + if (e.serverError) toast.error(e.serverError); + }, + onSuccess: () => { + toast.success(t('settings.settings.settings-updated')); + router.refresh(); }, }); @@ -36,7 +35,12 @@ export const SettingsContainer = ({ initialValues, currentLocale }: Props) => { return (
- +
); }; diff --git a/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx b/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx index b9adefe203..4bf25041d4 100644 --- a/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx +++ b/src/app/(dashboard)/settings/components/SettingsForm/SettingsForm.tsx @@ -20,6 +20,7 @@ export type SettingsFormValues = { localDomain?: string; guestDashboard?: boolean; allowAutoThemes?: boolean; + allowErrorMonitoring?: boolean; }; interface IProps { @@ -132,7 +133,9 @@ export const SettingsForm = (props: IProps) => { label={ <> {t('guest-dashboard')} - {t('guest-dashboard-hint')} + + {t('guest-dashboard-hint')} + ? } @@ -140,6 +143,31 @@ export const SettingsForm = (props: IProps) => { )} />
+
+ ( + + {t('allow-error-monitoring')} + + {t('allow-error-monitoring-hint')} + + ? + + } + /> + )} + /> +
{ label={ <> {t('allow-auto-themes')} - {t('allow-auto-themes-hint')} + + {t('allow-auto-themes-hint')} + ? } @@ -169,7 +199,9 @@ export const SettingsForm = (props: IProps) => { label={ <> {t('domain-name')} - {t('domain-name-hint')} + + {t('domain-name-hint')} + ? } @@ -186,7 +218,9 @@ export const SettingsForm = (props: IProps) => { label={ <> {t('internal-ip')} - {t('internal-ip-hint')} + + {t('internal-ip-hint')} + ? } @@ -200,7 +234,9 @@ export const SettingsForm = (props: IProps) => { label={ <> {t('apps-repo')} - {t('apps-repo-hint')} + + {t('apps-repo-hint')} + ? } @@ -214,7 +250,9 @@ export const SettingsForm = (props: IProps) => { label={ <> {t('storage-path')} - {t('storage-path-hint')} + + {t('storage-path-hint')} + ? } @@ -228,7 +266,9 @@ export const SettingsForm = (props: IProps) => { label={ <> {t('local-domain')} - {t('local-domain-hint')} + + {t('local-domain-hint')} + ? } diff --git a/src/app/actions/app-actions/install-app-action.ts b/src/app/actions/app-actions/install-app-action.ts index 37a851f181..320473160f 100644 --- a/src/app/actions/app-actions/install-app-action.ts +++ b/src/app/actions/app-actions/install-app-action.ts @@ -6,6 +6,7 @@ import { action } from '@/lib/safe-action'; import { revalidatePath } from 'next/cache'; import { AppServiceClass } from '@/server/services/apps/apps.service'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const formSchema = z.object({}).catchall(z.any()); @@ -19,8 +20,9 @@ const input = z.object({ */ export const installAppAction = action(input, async ({ id, form }) => { try { - const appsService = new AppServiceClass(db); + await ensureUser(); + const appsService = new AppServiceClass(db); await appsService.installApp(id, form); revalidatePath('/apps'); diff --git a/src/app/actions/app-actions/reset-app-action.ts b/src/app/actions/app-actions/reset-app-action.ts index 0c20d3e799..c1712f58e9 100644 --- a/src/app/actions/app-actions/reset-app-action.ts +++ b/src/app/actions/app-actions/reset-app-action.ts @@ -6,6 +6,7 @@ import { AppServiceClass } from '@/server/services/apps/apps.service'; import { revalidatePath } from 'next/cache'; import { z } from 'zod'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ id: z.string(), @@ -13,8 +14,9 @@ const input = z.object({ export const resetAppAction = action(input, async ({ id }) => { try { - const appsService = new AppServiceClass(db); + await ensureUser(); + const appsService = new AppServiceClass(db); await appsService.resetApp(id); revalidatePath('/apps'); diff --git a/src/app/actions/app-actions/revalidate-app.ts b/src/app/actions/app-actions/revalidate-app.ts new file mode 100644 index 0000000000..637e71f41e --- /dev/null +++ b/src/app/actions/app-actions/revalidate-app.ts @@ -0,0 +1,23 @@ +'use server'; + +import { z } from 'zod'; +import { action } from '@/lib/safe-action'; +import { revalidatePath } from 'next/cache'; +import { handleActionError } from '../utils/handle-action-error'; + +const input = z.object({ id: z.string() }); + +/** + * Given an app id, revalidates the app and app store pages on demand. + */ +export const revalidateAppAction = action(input, async ({ id }) => { + try { + revalidatePath('/apps'); + revalidatePath(`/app/${id}`); + revalidatePath(`/app-store/${id}`); + + return { success: true }; + } catch (e) { + return handleActionError(e); + } +}); diff --git a/src/app/actions/app-actions/start-app-action.ts b/src/app/actions/app-actions/start-app-action.ts index 28f4071e0b..0070079529 100644 --- a/src/app/actions/app-actions/start-app-action.ts +++ b/src/app/actions/app-actions/start-app-action.ts @@ -6,6 +6,7 @@ import { action } from '@/lib/safe-action'; import { revalidatePath } from 'next/cache'; import { AppServiceClass } from '@/server/services/apps/apps.service'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ id: z.string() }); @@ -14,8 +15,9 @@ const input = z.object({ id: z.string() }); */ export const startAppAction = action(input, async ({ id }) => { try { - const appsService = new AppServiceClass(db); + ensureUser(); + const appsService = new AppServiceClass(db); await appsService.startApp(id); revalidatePath('/apps'); diff --git a/src/app/actions/app-actions/stop-app-action.ts b/src/app/actions/app-actions/stop-app-action.ts index a4fe953ff5..9579f1501d 100644 --- a/src/app/actions/app-actions/stop-app-action.ts +++ b/src/app/actions/app-actions/stop-app-action.ts @@ -6,6 +6,7 @@ import { action } from '@/lib/safe-action'; import { revalidatePath } from 'next/cache'; import { AppServiceClass } from '@/server/services/apps/apps.service'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ id: z.string() }); @@ -14,8 +15,9 @@ const input = z.object({ id: z.string() }); */ export const stopAppAction = action(input, async ({ id }) => { try { - const appsService = new AppServiceClass(db); + await ensureUser(); + const appsService = new AppServiceClass(db); await appsService.stopApp(id); revalidatePath('/apps'); diff --git a/src/app/actions/app-actions/uninstall-app-action.ts b/src/app/actions/app-actions/uninstall-app-action.ts index 5871f952d0..9e6fa3bc6f 100644 --- a/src/app/actions/app-actions/uninstall-app-action.ts +++ b/src/app/actions/app-actions/uninstall-app-action.ts @@ -6,6 +6,7 @@ import { action } from '@/lib/safe-action'; import { revalidatePath } from 'next/cache'; import { AppServiceClass } from '@/server/services/apps/apps.service'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ id: z.string() }); @@ -14,8 +15,9 @@ const input = z.object({ id: z.string() }); */ export const uninstallAppAction = action(input, async ({ id }) => { try { - const appsService = new AppServiceClass(db); + await ensureUser(); + const appsService = new AppServiceClass(db); await appsService.uninstallApp(id); revalidatePath('/apps'); diff --git a/src/app/actions/app-actions/update-app-action.ts b/src/app/actions/app-actions/update-app-action.ts index b45dde404d..c80bd1b7bf 100644 --- a/src/app/actions/app-actions/update-app-action.ts +++ b/src/app/actions/app-actions/update-app-action.ts @@ -6,6 +6,7 @@ import { action } from '@/lib/safe-action'; import { revalidatePath } from 'next/cache'; import { AppServiceClass } from '@/server/services/apps/apps.service'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ id: z.string() }); @@ -14,8 +15,9 @@ const input = z.object({ id: z.string() }); */ export const updateAppAction = action(input, async ({ id }) => { try { - const appsService = new AppServiceClass(db); + await ensureUser(); + const appsService = new AppServiceClass(db); await appsService.updateApp(id); revalidatePath('/apps'); diff --git a/src/app/actions/app-actions/update-app-config-action.ts b/src/app/actions/app-actions/update-app-config-action.ts index dfeee66a8f..5cfa7730e6 100644 --- a/src/app/actions/app-actions/update-app-config-action.ts +++ b/src/app/actions/app-actions/update-app-config-action.ts @@ -6,6 +6,7 @@ import { action } from '@/lib/safe-action'; import { revalidatePath } from 'next/cache'; import { AppServiceClass } from '@/server/services/apps/apps.service'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const formSchema = z.object({}).catchall(z.any()); @@ -21,8 +22,9 @@ const input = z.object({ */ export const updateAppConfigAction = action(input, async ({ id, form }) => { try { - const appsService = new AppServiceClass(db); + await ensureUser(); + const appsService = new AppServiceClass(db); await appsService.updateAppConfig(id, form); revalidatePath('/apps'); diff --git a/src/app/actions/cancel-reset-password/cancel-reset-password-action.ts b/src/app/actions/cancel-reset-password/cancel-reset-password-action.ts index 51459b609f..4e2186cb8b 100644 --- a/src/app/actions/cancel-reset-password/cancel-reset-password-action.ts +++ b/src/app/actions/cancel-reset-password/cancel-reset-password-action.ts @@ -5,6 +5,7 @@ import { AuthServiceClass } from '@/server/services/auth/auth.service'; import { action } from '@/lib/safe-action'; import { revalidatePath } from 'next/cache'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.void(); @@ -13,6 +14,8 @@ const input = z.void(); */ export const cancelResetPasswordAction = action(input, async () => { try { + await ensureUser(); + await AuthServiceClass.cancelPasswordChangeRequest(); revalidatePath('/reset-password'); diff --git a/src/app/actions/reset-password/reset-password-action.ts b/src/app/actions/reset-password/reset-password-action.ts index c4c97f4b98..3c2b1c9885 100644 --- a/src/app/actions/reset-password/reset-password-action.ts +++ b/src/app/actions/reset-password/reset-password-action.ts @@ -5,6 +5,7 @@ import { db } from '@/server/db'; import { AuthServiceClass } from '@/server/services/auth/auth.service'; import { action } from '@/lib/safe-action'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ newPassword: z.string(), @@ -15,8 +16,9 @@ const input = z.object({ */ export const resetPasswordAction = action(input, async ({ newPassword }) => { try { - const authService = new AuthServiceClass(db); + await ensureUser(); + const authService = new AuthServiceClass(db); const { email } = await authService.changeOperatorPassword({ newPassword }); return { success: true, email }; diff --git a/src/app/actions/settings/change-password.ts b/src/app/actions/settings/change-password.ts index 0f80a3f77f..244ea670f8 100644 --- a/src/app/actions/settings/change-password.ts +++ b/src/app/actions/settings/change-password.ts @@ -2,10 +2,10 @@ import { z } from 'zod'; import { action } from '@/lib/safe-action'; -import { getUserFromCookie } from '@/server/common/session.helpers'; import { AuthServiceClass } from '@/server/services/auth/auth.service'; import { db } from '@/server/db'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ currentPassword: z.string(), newPassword: z.string() }); @@ -14,15 +14,11 @@ const input = z.object({ currentPassword: z.string(), newPassword: z.string() }) */ export const changePasswordAction = action(input, async ({ currentPassword, newPassword }) => { try { - const user = await getUserFromCookie(); - - if (!user) { - throw new Error('User not found'); - } + const { id } = await ensureUser(); const authService = new AuthServiceClass(db); - await authService.changePassword({ userId: user.id, currentPassword, newPassword }); + await authService.changePassword({ userId: id, currentPassword, newPassword }); return { success: true }; } catch (e) { diff --git a/src/app/actions/settings/change-username.ts b/src/app/actions/settings/change-username.ts index 2715940364..9862e37e58 100644 --- a/src/app/actions/settings/change-username.ts +++ b/src/app/actions/settings/change-username.ts @@ -2,10 +2,10 @@ import { z } from 'zod'; import { action } from '@/lib/safe-action'; -import { getUserFromCookie } from '@/server/common/session.helpers'; import { AuthServiceClass } from '@/server/services/auth/auth.service'; import { db } from '@/server/db'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ newUsername: z.string().email(), password: z.string() }); @@ -14,15 +14,11 @@ const input = z.object({ newUsername: z.string().email(), password: z.string() } */ export const changeUsernameAction = action(input, async ({ newUsername, password }) => { try { - const user = await getUserFromCookie(); - - if (!user) { - throw new Error('User not found'); - } + const { id } = await ensureUser(); const authService = new AuthServiceClass(db); - await authService.changeUsername({ userId: user.id, newUsername, password }); + await authService.changeUsername({ userId: id, newUsername, password }); return { success: true }; } catch (e) { diff --git a/src/app/actions/settings/disable-totp.ts b/src/app/actions/settings/disable-totp.ts index 08c86b0553..7c5baaaea9 100644 --- a/src/app/actions/settings/disable-totp.ts +++ b/src/app/actions/settings/disable-totp.ts @@ -2,11 +2,11 @@ import { z } from 'zod'; import { action } from '@/lib/safe-action'; -import { getUserFromCookie } from '@/server/common/session.helpers'; import { AuthServiceClass } from '@/server/services/auth/auth.service'; import { db } from '@/server/db'; import { revalidatePath } from 'next/cache'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ password: z.string() }); @@ -15,14 +15,10 @@ const input = z.object({ password: z.string() }); */ export const disableTotpAction = action(input, async ({ password }) => { try { - const user = await getUserFromCookie(); - - if (!user) { - throw new Error('User not found'); - } + const { id } = await ensureUser(); const authService = new AuthServiceClass(db); - await authService.disableTotp({ userId: user.id, password }); + await authService.disableTotp({ userId: id, password }); revalidatePath('/settings'); diff --git a/src/app/actions/settings/get-totp-uri.ts b/src/app/actions/settings/get-totp-uri.ts index b283375915..8228a51184 100644 --- a/src/app/actions/settings/get-totp-uri.ts +++ b/src/app/actions/settings/get-totp-uri.ts @@ -2,10 +2,10 @@ import { z } from 'zod'; import { action } from '@/lib/safe-action'; -import { getUserFromCookie } from '@/server/common/session.helpers'; import { AuthServiceClass } from '@/server/services/auth/auth.service'; import { db } from '@/server/db'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ password: z.string() }); @@ -14,14 +14,10 @@ const input = z.object({ password: z.string() }); */ export const getTotpUriAction = action(input, async ({ password }) => { try { - const user = await getUserFromCookie(); - - if (!user) { - throw new Error('User not found'); - } + const { id } = await ensureUser(); const authService = new AuthServiceClass(db); - const { key, uri } = await authService.getTotpUri({ userId: user.id, password }); + const { key, uri } = await authService.getTotpUri({ userId: id, password }); return { success: true, key, uri }; } catch (e) { diff --git a/src/app/actions/settings/setup-totp-action.ts b/src/app/actions/settings/setup-totp-action.ts index 57360d7cae..bfef289cce 100644 --- a/src/app/actions/settings/setup-totp-action.ts +++ b/src/app/actions/settings/setup-totp-action.ts @@ -2,11 +2,11 @@ import { z } from 'zod'; import { action } from '@/lib/safe-action'; -import { getUserFromCookie } from '@/server/common/session.helpers'; import { AuthServiceClass } from '@/server/services/auth/auth.service'; import { db } from '@/server/db'; import { revalidatePath } from 'next/cache'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; const input = z.object({ totpCode: z.string() }); @@ -15,14 +15,10 @@ const input = z.object({ totpCode: z.string() }); */ export const setupTotpAction = action(input, async ({ totpCode }) => { try { - const user = await getUserFromCookie(); - - if (!user) { - throw new Error('User not found'); - } + const { id } = await ensureUser(); const authService = new AuthServiceClass(db); - await authService.setupTotp({ userId: user.id, totpCode }); + await authService.setupTotp({ userId: id, totpCode }); revalidatePath('/settings'); diff --git a/src/app/actions/settings/update-settings.ts b/src/app/actions/settings/update-settings.ts index edb3d5f291..82fd79aa4e 100644 --- a/src/app/actions/settings/update-settings.ts +++ b/src/app/actions/settings/update-settings.ts @@ -1,20 +1,20 @@ 'use server'; import { action } from '@/lib/safe-action'; -import { getUserFromCookie } from '@/server/common/session.helpers'; import { settingsSchema } from '@runtipi/shared'; import { setSettings } from '@/server/core/TipiConfig'; import { revalidatePath } from 'next/cache'; import { handleActionError } from '../utils/handle-action-error'; +import { ensureUser } from '../utils/ensure-user'; /** * Given a settings object, update the settings.json file */ export const updateSettingsAction = action(settingsSchema, async (settings) => { try { - const user = await getUserFromCookie(); + const { operator } = await ensureUser(); - if (!user?.operator) { + if (!operator) { throw new Error('Not authorized'); } diff --git a/src/app/actions/utils/ensure-user.ts b/src/app/actions/utils/ensure-user.ts new file mode 100644 index 0000000000..59e2dae71c --- /dev/null +++ b/src/app/actions/utils/ensure-user.ts @@ -0,0 +1,11 @@ +import { getUserFromCookie } from '@/server/common/session.helpers'; + +export const ensureUser = async () => { + const user = await getUserFromCookie(); + + if (!user) { + throw new Error('You must be logged in to perform this action'); + } + + return user; +}; diff --git a/src/app/actions/utils/handle-action-error.ts b/src/app/actions/utils/handle-action-error.ts index 78c66a99b9..b190227f5d 100644 --- a/src/app/actions/utils/handle-action-error.ts +++ b/src/app/actions/utils/handle-action-error.ts @@ -9,12 +9,7 @@ export const handleActionError = async (e: unknown) => { const errorVariables = e instanceof TranslatedError ? e.variableValues : {}; const translator = await getTranslatorFromCookie(); - const messageTranslated = translator(message as MessageKey, errorVariables); + const messageTranslated = e instanceof TranslatedError ? translator(message as MessageKey, errorVariables) : message; - return { - success: false as const, - failure: { - reason: messageTranslated, - }, - }; + throw new Error(messageTranslated as string); }; diff --git a/src/app/components/ClientProviders/ClientProviders.tsx b/src/app/components/ClientProviders/ClientProviders.tsx index 7c7b7768f0..8cec881be0 100644 --- a/src/app/components/ClientProviders/ClientProviders.tsx +++ b/src/app/components/ClientProviders/ClientProviders.tsx @@ -8,15 +8,12 @@ type Props = { children: React.ReactNode; cookies: ComponentProps['value']; initialTheme?: string; - allowAutoThemes: boolean; }; -export const ClientProviders = ({ children, initialTheme, cookies, allowAutoThemes }: Props) => { +export const ClientProviders = ({ children, initialTheme, cookies }: Props) => { return ( - - {children} - + {children} ); }; diff --git a/src/app/components/ClientProviders/ThemeProvider/ThemeProvider.tsx b/src/app/components/ClientProviders/ThemeProvider/ThemeProvider.tsx index 98f3d90922..1f68a16b15 100644 --- a/src/app/components/ClientProviders/ThemeProvider/ThemeProvider.tsx +++ b/src/app/components/ClientProviders/ThemeProvider/ThemeProvider.tsx @@ -4,10 +4,10 @@ import { useUIStore } from '@/client/state/uiStore'; import React, { useEffect } from 'react'; import { useCookies } from 'next-client-cookies'; import { getAutoTheme } from '@/lib/themes'; +import { useClientSettings } from '@/hooks/use-client-settings'; type Props = { children: React.ReactNode; - allowAutoThemes: boolean; initialTheme?: string; }; @@ -18,9 +18,10 @@ const loadChristmasTheme = async () => { }; export const ThemeProvider = (props: Props) => { - const { children, initialTheme, allowAutoThemes } = props; + const { children, initialTheme } = props; const cookies = useCookies(); const { theme, setDarkMode } = useUIStore(); + const { allowAutoThemes = true } = useClientSettings(); useEffect(() => { if (theme) { diff --git a/src/app/global-error.jsx b/src/app/global-error.jsx new file mode 100644 index 0000000000..bfe3d39b3d --- /dev/null +++ b/src/app/global-error.jsx @@ -0,0 +1,19 @@ +'use client'; + +import React, { useEffect } from 'react'; +import * as Sentry from '@sentry/nextjs'; +import Error from 'next/error'; + +export default function GlobalError({ error }) { + useEffect(() => { + Sentry.captureException(error); + }, [error]); + + return ( + + + + + + ); +} diff --git a/src/app/global.css b/src/app/global.css index b8fb84b092..00d1fade0a 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -3,3 +3,8 @@ body { overflow-y: scroll; } + +.tooltip { + max-width: 300px; + text-align: center; +} diff --git a/src/app/hooks/use-client-settings.ts b/src/app/hooks/use-client-settings.ts new file mode 100644 index 0000000000..7c48f8f995 --- /dev/null +++ b/src/app/hooks/use-client-settings.ts @@ -0,0 +1,25 @@ +import { settingsSchema } from '@runtipi/shared/src/schemas/env-schemas'; +import { useState, useEffect } from 'react'; +import { z } from 'zod'; + +// A react hook to grab the content of hidden input "client-settings", JSON.parse it and return the value +export const useClientSettings = () => { + const [settings, setSettings] = useState>({}); + + useEffect(() => { + // Get the hidden input element + const inputElement = document.getElementById('client-settings') as HTMLInputElement | null; + if (inputElement) { + try { + // Parse the input value + const parsedSettings = settingsSchema.parse(JSON.parse(inputElement.value)); + setSettings(parsedSettings); + } catch (error) { + // eslint-disable-next-line no-console + console.error('Error parsing client settings:', error); + } + } + }, []); + + return settings; +}; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 053de24f42..b1f4cba7e7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -5,7 +5,7 @@ import { cookies } from 'next/headers'; import { GeistSans } from 'geist/font/sans'; import merge from 'lodash.merge'; import { NextIntlClientProvider } from 'next-intl'; -import { getConfig } from '@/server/core/TipiConfig'; +import { getSettings } from '@/server/core/TipiConfig'; import './global.css'; import clsx from 'clsx'; @@ -21,19 +21,20 @@ export const metadata: Metadata = { export default async function RootLayout({ children }: { children: React.ReactNode }) { const locale = getCurrentLocale(); + const clientSettings = getSettings(); + const englishMessages = (await import(`../client/messages/en.json`)).default; const messages = (await import(`../client/messages/${locale}.json`)).default; const mergedMessages = merge(englishMessages, messages); const theme = cookies().get('theme'); - const { allowAutoThemes } = getConfig(); - return ( - + + {children} diff --git a/src/app/page.tsx b/src/app/page.tsx index be54275dfd..eb70ef3acc 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -14,7 +14,7 @@ export const dynamic = 'force-dynamic'; export default async function RootPage() { const appService = new AppServiceClass(db); - const { guestDashboard, allowAutoThemes } = getConfig(); + const { guestDashboard } = getConfig(); const headersList = headers(); const host = headersList.get('host'); @@ -24,7 +24,7 @@ export default async function RootPage() { const apps = await appService.getGuestDashboardApps(); return ( - + {apps.length === 0 ? ( ) : ( diff --git a/src/client/components/AppStatus/AppStatus.tsx b/src/client/components/AppStatus/AppStatus.tsx index e4aa145fd5..2ea12a46cc 100644 --- a/src/client/components/AppStatus/AppStatus.tsx +++ b/src/client/components/AppStatus/AppStatus.tsx @@ -16,7 +16,7 @@ export const AppStatus: React.FC<{ status: AppStatusEnum; lite?: boolean }> = ({ return ( <> - {lite && } + {lite && }
{!lite && {formattedStatus}} diff --git a/src/client/components/AppTile/AppTile.tsx b/src/client/components/AppTile/AppTile.tsx index 27df801c9f..4edc709b88 100644 --- a/src/client/components/AppTile/AppTile.tsx +++ b/src/client/components/AppTile/AppTile.tsx @@ -37,7 +37,9 @@ export const AppTile: React.FC<{ app: AppTileInfo; status: AppStatusEnum; update
{updateAvailable && ( <> - {t('update-available')} + + {t('update-available')} +
diff --git a/src/client/components/UnauthenticatedPage/UnauthenticatedPage.tsx b/src/client/components/UnauthenticatedPage/UnauthenticatedPage.tsx index 950f4ddc4c..2830b6e503 100644 --- a/src/client/components/UnauthenticatedPage/UnauthenticatedPage.tsx +++ b/src/client/components/UnauthenticatedPage/UnauthenticatedPage.tsx @@ -10,16 +10,15 @@ type Props = { children: React.ReactNode; title: MessageKey; subtitle?: MessageKey; - autoTheme: boolean; }; export const UnauthenticatedPage = (props: Props) => { - const { children, title, subtitle, autoTheme } = props; + const { children, title, subtitle } = props; const t = useTranslations(); return (
-
+
diff --git a/src/client/messages/en.json b/src/client/messages/en.json index 786e8f4043..55588fd9a2 100644 --- a/src/client/messages/en.json +++ b/src/client/messages/en.json @@ -251,6 +251,8 @@ "invalid-domain": "Invalid domain", "guest-dashboard": "Enable guest dashboard", "guest-dashboard-hint": "This will allow non-authenticated users to see a limited dashboard and easily access the running apps on your instance.", + "allow-error-monitoring": "Allow anonymous error monitoring", + "allow-error-monitoring-hint": "Error monitoring is used to track errors and improve Tipi. Keep this option enabled to help us improve Tipi.", "allow-auto-themes": "Allow auto themes", "allow-auto-themes-hint": "Be surprised by themes that change automatically based on the time of the year.", "domain-name": "Domain name", diff --git a/src/lib/safe-action.ts b/src/lib/safe-action.ts index 2f9142094a..cfe837a3d5 100644 --- a/src/lib/safe-action.ts +++ b/src/lib/safe-action.ts @@ -1,3 +1,12 @@ import { createSafeActionClient } from 'next-safe-action'; -export const action = createSafeActionClient(); +export const action = createSafeActionClient({ + handleReturnedServerError: (e) => { + // eslint-disable-next-line no-console + console.error('Error from server', e); + + return { + serverError: e.message || 'An unexpected error occurred', + }; + }, +}); diff --git a/src/lib/socket/useSocket.ts b/src/lib/socket/useSocket.ts new file mode 100644 index 0000000000..e3ffbde947 --- /dev/null +++ b/src/lib/socket/useSocket.ts @@ -0,0 +1,71 @@ +import { SocketEvent, socketEventSchema } from '@runtipi/shared/src/schemas/socket'; +import { useEffect } from 'react'; +import io from 'socket.io-client'; + +// Data selector is used to select a specific property/value from the data object if it exists +type DataSelector = { + property: keyof Extract['data']; + value: unknown; +}; + +type Selector = { + type: T; + event?: U; + data?: DataSelector; +}; + +type Props = { + onEvent: (event: Extract['event'], U>, data: Extract['data']) => void; + onError?: (error: string) => void; + selector: Selector; +}; + +export const useSocket = (props: Props) => { + const { onEvent, onError, selector } = props; + + useEffect(() => { + const socket = io('http://localhost:3935'); + + const handleEvent = (type: SocketEvent['type'], rawData: unknown) => { + const parsedEvent = socketEventSchema.safeParse(rawData); + + if (!parsedEvent.success) { + return; + } + + const { event, data } = parsedEvent.data; + + if (selector) { + if (selector.type !== type) { + return; + } + + if (selector.event && selector.event !== event) { + return; + } + + const property = selector.data?.property as keyof SocketEvent['data']; + if (selector.data && selector.data.value !== data[property]) { + return; + } + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - This is fine + onEvent(event, data); + }; + + socket.on(selector.type as string, (data) => { + handleEvent(selector.type, data); + }); + + socket.on('error', (error: string) => { + onError?.(String(error)); + }); + + return () => { + socket?.off(selector.type as string); + socket.disconnect(); + }; + }, [onError, onEvent, selector, selector.type]); +}; diff --git a/src/server/queries/apps/apps.queries.ts b/src/server/queries/apps/apps.queries.ts index 1a75360f91..7545043a00 100644 --- a/src/server/queries/apps/apps.queries.ts +++ b/src/server/queries/apps/apps.queries.ts @@ -25,7 +25,7 @@ export class AppQueries { * @param {Partial} data - The data to update the app with */ public async updateApp(appId: string, data: Partial) { - const updatedApps = await this.db.update(appTable).set(data).where(eq(appTable.id, appId)).returning(); + const updatedApps = await this.db.update(appTable).set(data).where(eq(appTable.id, appId)).returning().execute(); return updatedApps[0]; } @@ -35,7 +35,7 @@ export class AppQueries { * @param {string} appId - The id of the app to delete */ public async deleteApp(appId: string) { - await this.db.delete(appTable).where(eq(appTable.id, appId)); + await this.db.delete(appTable).where(eq(appTable.id, appId)).execute(); } /** @@ -44,7 +44,7 @@ export class AppQueries { * @param {NewApp} data - The data to create the app with */ public async createApp(data: NewApp) { - const newApps = await this.db.insert(appTable).values(data).returning(); + const newApps = await this.db.insert(appTable).values(data).returning().execute(); return newApps[0]; } @@ -68,7 +68,10 @@ export class AppQueries { * Returns all apps that are running and visible on guest dashboard sorted by id ascending */ public async getGuestDashboardApps() { - return this.db.query.appTable.findMany({ where: and(eq(appTable.status, 'running'), eq(appTable.isVisibleOnGuestDashboard, true)), orderBy: asc(appTable.id) }); + return this.db.query.appTable.findMany({ + where: and(eq(appTable.status, 'running'), eq(appTable.isVisibleOnGuestDashboard, true)), + orderBy: asc(appTable.id), + }); } /** @@ -88,6 +91,6 @@ export class AppQueries { * @param {Partial} data - The data to update the apps with */ public async updateAppsByStatusNotIn(statuses: AppStatus[], data: Partial) { - return this.db.update(appTable).set(data).where(notInArray(appTable.status, statuses)).returning(); + return this.db.update(appTable).set(data).where(notInArray(appTable.status, statuses)).returning().execute(); } } diff --git a/src/server/run-migrations-dev.ts b/src/server/run-migrations-dev.ts index 6567221ae4..3632673a8b 100644 --- a/src/server/run-migrations-dev.ts +++ b/src/server/run-migrations-dev.ts @@ -65,6 +65,7 @@ const main = async () => { }; main().catch((e) => { + // eslint-disable-next-line no-console console.error(e); process.exit(1); }); diff --git a/src/server/services/apps/apps.service.test.ts b/src/server/services/apps/apps.service.test.ts index 87d4da4a8b..63b017e87f 100644 --- a/src/server/services/apps/apps.service.test.ts +++ b/src/server/services/apps/apps.service.test.ts @@ -2,7 +2,6 @@ import fs from 'fs-extra'; import waitForExpect from 'wait-for-expect'; import { TestDatabase, clearDatabase, closeDatabase, createDatabase } from '@/server/tests/test-utils'; import { faker } from '@faker-js/faker'; -import { waitUntilFinishedMock } from '@/tests/server/jest.setup'; import { castAppConfig } from '@/lib/helpers/castAppConfig'; import { AppServiceClass } from './apps.service'; import { EventDispatcher } from '../../core/EventDispatcher'; @@ -42,7 +41,7 @@ describe('Install app', () => { expect(dbApp).toBeDefined(); expect(dbApp?.id).toBe(appConfig.id); expect(dbApp?.config).toStrictEqual({ TEST_FIELD: 'test' }); - expect(dbApp?.status).toBe('running'); + expect(dbApp?.status).toBe('installing'); }); it('Should start app if already installed', async () => { @@ -58,25 +57,14 @@ describe('Install app', () => { expect(app?.status).toBe('running'); }); - it('Should delete app if install script fails', async () => { - // arrange - const appConfig = createAppConfig(); - - // act - waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' }); - await expect(AppsService.installApp(appConfig.id, {})).rejects.toThrow('server-messages.errors.app-failed-to-install'); - const app = await getAppById(appConfig.id, db); - - // assert - expect(app).toBeNull(); - }); - it('Should throw if app is exposed and domain is not provided', async () => { // arrange const appConfig = createAppConfig({ exposable: true }); // act & assert - await expect(AppsService.installApp(appConfig.id, { exposed: true })).rejects.toThrowError('server-messages.errors.domain-required-if-expose-app'); + await expect(AppsService.installApp(appConfig.id, { exposed: true })).rejects.toThrowError( + 'server-messages.errors.domain-required-if-expose-app', + ); }); it('Should throw if app is exposed and config does not allow it', async () => { @@ -84,7 +72,9 @@ describe('Install app', () => { const appConfig = createAppConfig({ exposable: false }); // act & assert - await expect(AppsService.installApp(appConfig.id, { exposed: true, domain: 'test.com' })).rejects.toThrowError('server-messages.errors.app-not-exposable'); + await expect(AppsService.installApp(appConfig.id, { exposed: true, domain: 'test.com' })).rejects.toThrowError( + 'server-messages.errors.app-not-exposable', + ); }); it('Should throw if app is exposed and domain is not valid', async () => { @@ -92,7 +82,9 @@ describe('Install app', () => { const appConfig = createAppConfig({ exposable: true }); // act & assert - await expect(AppsService.installApp(appConfig.id, { exposed: true, domain: 'test' })).rejects.toThrowError('server-messages.errors.domain-not-valid'); + await expect(AppsService.installApp(appConfig.id, { exposed: true, domain: 'test' })).rejects.toThrowError( + 'server-messages.errors.domain-not-valid', + ); }); it('Should throw if app is exposed and domain is already used by another exposed app', async () => { @@ -103,7 +95,9 @@ describe('Install app', () => { await insertApp({ domain, exposed: true }, appConfig2, db); // act & assert - await expect(AppsService.installApp(appConfig.id, { exposed: true, domain })).rejects.toThrowError('server-messages.errors.domain-already-in-use'); + await expect(AppsService.installApp(appConfig.id, { exposed: true, domain })).rejects.toThrowError( + 'server-messages.errors.domain-already-in-use', + ); }); it('Should throw if architecure is not supported', async () => { @@ -159,51 +153,10 @@ describe('Install app', () => { }); describe('Uninstall app', () => { - it('Should correctly remove app from database', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({}, appConfig, db); - - // act - await AppsService.uninstallApp(appConfig.id); - const app = await getAppById(appConfig.id, db); - - // assert - expect(app).toBeNull(); - }); - - it('Should stop app if it is running', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({ status: 'running' }, appConfig, db); - - // act - waitUntilFinishedMock.mockResolvedValueOnce({ success: true, stdout: 'test' }); - waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' }); - await expect(AppsService.uninstallApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-uninstall'); - const app = await getAppById(appConfig.id, db); - - // assert - expect(app?.status).toBe('stopped'); - }); - it('Should throw if app is not installed', async () => { // act & assert await expect(AppsService.uninstallApp('any')).rejects.toThrowError('server-messages.errors.app-not-found'); }); - - it('Should throw if uninstall script fails', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({ status: 'running' }, appConfig, db); - waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' }); - await updateApp(appConfig.id, { status: 'updating' }, db); - - // act & assert - await expect(AppsService.uninstallApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-uninstall'); - const app = await getAppById(appConfig.id, db); - expect(app?.status).toBe('stopped'); - }); }); describe('Start app', () => { @@ -237,49 +190,12 @@ describe('Start app', () => { // assert expect(app?.status).toBe('running'); }); - - it('Should throw if start script fails', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({ status: 'stopped' }, appConfig, db); - waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' }); - - // act & assert - await expect(AppsService.startApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-start'); - const app = await getAppById(appConfig.id, db); - expect(app?.status).toBe('stopped'); - }); }); describe('Stop app', () => { - it('Should correctly stop app', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({ status: 'running' }, appConfig, db); - - // act - await AppsService.stopApp(appConfig.id); - const app = await getAppById(appConfig.id, db); - - // assert - expect(app?.status).toBe('stopped'); - }); - it('Should throw if app is not installed', async () => { await expect(AppsService.stopApp('any')).rejects.toThrowError('server-messages.errors.app-not-found'); }); - - it('Should throw if stop script fails', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({ status: 'running' }, appConfig, db); - waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'test' }); - - // act & assert - await expect(AppsService.stopApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-stop'); - const app = await getAppById(appConfig.id, db); - expect(app?.status).toBe('running'); - }); }); describe('Update app config', () => { @@ -317,7 +233,9 @@ describe('Update app config', () => { await insertApp({}, appConfig, db); // act & assert - expect(AppsService.updateAppConfig(appConfig.id, { exposed: true, domain: 'test' })).rejects.toThrowError('server-messages.errors.domain-not-valid'); + expect(AppsService.updateAppConfig(appConfig.id, { exposed: true, domain: 'test' })).rejects.toThrowError( + 'server-messages.errors.domain-not-valid', + ); }); it('Should throw if app is exposed and domain is already used', async () => { @@ -329,7 +247,9 @@ describe('Update app config', () => { await insertApp({}, appConfig2, db); // act & assert - await expect(AppsService.updateAppConfig(appConfig2.id, { exposed: true, domain })).rejects.toThrowError('server-messages.errors.domain-already-in-use'); + await expect(AppsService.updateAppConfig(appConfig2.id, { exposed: true, domain })).rejects.toThrowError( + 'server-messages.errors.domain-already-in-use', + ); }); it('should throw if app is not exposed and config has force_expose set to true', async () => { @@ -347,22 +267,9 @@ describe('Update app config', () => { await insertApp({}, appConfig, db); // act & assert - await expect(AppsService.updateAppConfig(appConfig.id, { exposed: true, domain: 'test.com' })).rejects.toThrowError('server-messages.errors.app-not-exposable'); - }); -}); - -describe('Reset app', () => { - it('Should correctly reset app', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({ status: 'running' }, appConfig, db); - - // act - await AppsService.resetApp(appConfig.id); - const app = await getAppById(appConfig.id, db); - - // assert - expect(app?.status).toBe('running'); + await expect(AppsService.updateAppConfig(appConfig.id, { exposed: true, domain: 'test.com' })).rejects.toThrowError( + 'server-messages.errors.app-not-exposable', + ); }); }); @@ -494,17 +401,6 @@ describe('Update app', () => { await expect(AppsService.updateApp('test-app2')).rejects.toThrow('server-messages.errors.app-not-found'); }); - it('Should throw if update script fails', async () => { - // arrange - const appConfig = createAppConfig({}); - await insertApp({}, appConfig, db); - waitUntilFinishedMock.mockResolvedValueOnce({ success: false, stdout: 'error' }); - - // act & assert - await expect(AppsService.updateApp(appConfig.id)).rejects.toThrow('server-messages.errors.app-failed-to-update'); - const app = await getAppById(appConfig.id, db); - expect(app?.status).toBe('stopped'); - }); it('Should comme back to the previous status before the update of the app', async () => { // arrange const appConfig = createAppConfig({}); diff --git a/src/server/services/apps/apps.service.ts b/src/server/services/apps/apps.service.ts index be0da68ed7..5002f2bb6f 100644 --- a/src/server/services/apps/apps.service.ts +++ b/src/server/services/apps/apps.service.ts @@ -2,7 +2,7 @@ import validator from 'validator'; import { App } from '@/server/db/schema'; import { AppQueries } from '@/server/queries/apps/apps.queries'; import { TranslatedError } from '@/server/utils/errors'; -import { Database } from '@/server/db'; +import { Database, db } from '@/server/db'; import { AppInfo } from '@runtipi/shared'; import { EventDispatcher } from '@/server/core/EventDispatcher/EventDispatcher'; import { castAppConfig } from '@/lib/helpers/castAppConfig'; @@ -32,7 +32,7 @@ const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(fi export class AppServiceClass { private queries; - constructor(p: Database) { + constructor(p: Database = db) { this.queries = new AppQueries(p); } @@ -55,13 +55,15 @@ export class AppServiceClass { try { await this.queries.updateApp(app.id, { status: 'starting' }); - eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: app.id, form: castAppConfig(app.config) }).then(({ success }) => { - if (success) { - this.queries.updateApp(app.id, { status: 'running' }); - } else { - this.queries.updateApp(app.id, { status: 'stopped' }); - } - }); + eventDispatcher + .dispatchEventAsync({ type: 'app', command: 'start', appid: app.id, form: castAppConfig(app.config) }) + .then(({ success }) => { + if (success) { + this.queries.updateApp(app.id, { status: 'running' }); + } else { + this.queries.updateApp(app.id, { status: 'stopped' }); + } + }); } catch (e) { await this.queries.updateApp(app.id, { status: 'stopped' }); Logger.error(e); @@ -87,7 +89,12 @@ export class AppServiceClass { await this.queries.updateApp(appName, { status: 'starting' }); const eventDispatcher = new EventDispatcher('startApp'); - const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'start', appid: appName, form: castAppConfig(app.config) }); + const { success, stdout } = await eventDispatcher.dispatchEventAsync({ + type: 'app', + command: 'start', + appid: appName, + form: castAppConfig(app.config), + }); await eventDispatcher.close(); if (success) { @@ -166,18 +173,17 @@ export class AppServiceClass { // Run script const eventDispatcher = new EventDispatcher('installApp'); - const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form }); - await eventDispatcher.close(); + eventDispatcher.dispatchEventAsync({ type: 'app', command: 'install', appid: id, form }).then(({ success, stdout }) => { + if (success) { + this.queries.updateApp(id, { status: 'running' }); + } else { + this.queries.deleteApp(id); + Logger.error(`Failed to install app ${id}: ${stdout}`); + } - if (!success) { - await this.queries.deleteApp(id); - Logger.error(`Failed to install app ${id}: ${stdout}`); - throw new TranslatedError('server-messages.errors.app-failed-to-install', { id }); - } + eventDispatcher.close(); + }); } - - const updatedApp = await this.queries.updateApp(id, { status: 'running' }); - return updatedApp; }; /** @@ -240,7 +246,12 @@ export class AppServiceClass { await eventDispatcher.close(); if (success) { - const updatedApp = await this.queries.updateApp(id, { exposed: exposed || false, domain: domain || null, config: form, isVisibleOnGuestDashboard: form.isVisibleOnGuestDashboard }); + const updatedApp = await this.queries.updateApp(id, { + exposed: exposed || false, + domain: domain || null, + config: form, + isVisibleOnGuestDashboard: form.isVisibleOnGuestDashboard, + }); return updatedApp; } @@ -264,16 +275,16 @@ export class AppServiceClass { await this.queries.updateApp(id, { status: 'stopping' }); const eventDispatcher = new EventDispatcher('stopApp'); - const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) }); - await eventDispatcher.close(); + eventDispatcher.dispatchEventAsync({ type: 'app', command: 'stop', appid: id, form: castAppConfig(app.config) }).then(({ success, stdout }) => { + if (success) { + this.queries.updateApp(id, { status: 'stopped' }); + } else { + Logger.error(`Failed to stop app ${id}: ${stdout}`); + this.queries.updateApp(id, { status: 'running' }); + } - if (success) { - await this.queries.updateApp(id, { status: 'stopped' }); - } else { - await this.queries.updateApp(id, { status: 'running' }); - Logger.error(`Failed to stop app ${id}: ${stdout}`); - throw new TranslatedError('server-messages.errors.app-failed-to-stop', { id }); - } + eventDispatcher.close(); + }); const updatedApp = await this.queries.getApp(id); return updatedApp; @@ -298,16 +309,17 @@ export class AppServiceClass { await this.queries.updateApp(id, { status: 'uninstalling' }); const eventDispatcher = new EventDispatcher('uninstallApp'); - const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) }); - await eventDispatcher.close(); - - if (!success) { - await this.queries.updateApp(id, { status: 'stopped' }); - Logger.error(`Failed to uninstall app ${id}: ${stdout}`); - throw new TranslatedError('server-messages.errors.app-failed-to-uninstall', { id }); - } - - await this.queries.deleteApp(id); + eventDispatcher + .dispatchEventAsync({ type: 'app', command: 'uninstall', appid: id, form: castAppConfig(app.config) }) + .then(({ stdout, success }) => { + if (success) { + this.queries.deleteApp(id); + } else { + this.queries.updateApp(id, { status: 'stopped' }); + Logger.error(`Failed to uninstall app ${id}: ${stdout}`); + } + eventDispatcher.close(); + }); return { id, status: 'missing', config: {} }; }; @@ -350,7 +362,12 @@ export class AppServiceClass { await this.queries.updateApp(id, { status: 'updating' }); const eventDispatcher = new EventDispatcher('updateApp'); - const { success, stdout } = await eventDispatcher.dispatchEventAsync({ type: 'app', command: 'update', appid: id, form: castAppConfig(app.config) }); + const { success, stdout } = await eventDispatcher.dispatchEventAsync({ + type: 'app', + command: 'update', + appid: id, + form: castAppConfig(app.config), + }); await eventDispatcher.close(); if (success) { diff --git a/tsconfig.json b/tsconfig.json index db94332da0..d10ebb08bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,9 @@ "@/components/*": [ "./src/client/components/*" ], + "@/hooks/*": [ + "./src/app/hooks/*" + ], "@/utils/*": [ "./src/client/utils/*" ],