From 014669f4a4cfb4c9dda0e62f5a02b2bd087c22e7 Mon Sep 17 00:00:00 2001 From: izzy Date: Wed, 5 Nov 2025 13:44:43 +0000 Subject: [PATCH] feat: jwt auth for maintenance --- pnpm-lock.yaml | 383 +++++++++++++++++- server/package.json | 1 + server/src/app.module.ts | 8 + .../src/controllers/maintenance.controller.ts | 33 +- server/src/dtos/maintenance.dto.ts | 4 +- .../src/middleware/maintenance-auth.guard.ts | 34 +- server/src/services/cli.service.ts | 4 +- .../services/maintenance-worker.service.ts | 19 +- web/src/routes/maintenance/+page.svelte | 44 +- 9 files changed, 482 insertions(+), 48 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6faccf6ab6..e9108b5358 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -310,6 +310,9 @@ importers: '@nestjs/core': specifier: ^11.0.4 version: 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.1.7)(@nestjs/websockets@11.1.7)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.1 + version: 11.0.1(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2)) '@nestjs/platform-express': specifier: ^11.0.4 version: 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.7) @@ -613,19 +616,19 @@ importers: version: 13.15.3 '@vitest/coverage-v8': specifier: ^3.0.0 - version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) eslint: specifier: ^9.14.0 - version: 9.38.0(jiti@2.6.1) + version: 9.38.0(jiti@1.21.7) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@9.38.0(jiti@2.6.1)) + version: 10.1.8(eslint@9.38.0(jiti@1.21.7)) eslint-plugin-prettier: specifier: ^5.1.3 - version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))(prettier@3.6.2) + version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@1.21.7)))(eslint@9.38.0(jiti@1.21.7))(prettier@3.6.2) eslint-plugin-unicorn: specifier: ^60.0.0 - version: 60.0.0(eslint@9.38.0(jiti@2.6.1)) + version: 60.0.0(eslint@9.38.0(jiti@1.21.7)) globals: specifier: ^16.0.0 version: 16.4.0 @@ -661,16 +664,16 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.28.0 - version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) + version: 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) unplugin-swc: specifier: ^1.4.5 version: 1.5.8(@swc/core@1.13.5(@swc/helpers@0.5.17))(rollup@4.52.5) vite-tsconfig-paths: specifier: ^5.0.0 - version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + version: 5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) vitest: specifier: ^3.0.0 - version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) web: dependencies: @@ -3209,6 +3212,11 @@ packages: '@nestjs/websockets': optional: true + '@nestjs/jwt@11.0.1': + resolution: {integrity: sha512-HXSsc7SAnCnjA98TsZqrE7trGtHDnYXWp4Ffy6LwSmck1QvbGYdMzBquXofX5l6tIRpeY4Qidl2Ti2CVG77Pdw==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/mapped-types@2.1.0': resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} peerDependencies: @@ -4590,6 +4598,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/justified-layout@4.1.4': resolution: {integrity: sha512-q2ybP0u0NVj87oMnGZOGxY2iUN8ddr48zPOBHBdbOLpsMTA/keGj+93ou+OMCnJk0xewzlNIaVEkxM6VBD3E2w==} @@ -5361,6 +5372,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -6291,6 +6305,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -7714,12 +7731,22 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + just-compare@2.3.0: resolution: {integrity: sha512-6shoR7HDT+fzfL3gBahx1jZG3hWLrhPAf+l7nCwahDdT9XDtosB9kIF0ZrzUp5QY8dJWfQVr5rnsPqsbvflDzg==} justified-layout@4.1.0: resolution: {integrity: sha512-M5FimNMXgiOYerVRGsXZ2YK9YNCaTtwtYp7Hb2308U1Q9TXXHx5G0p08mcVR5O53qf8bWY4NJcPBxE6zuayXSg==} + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + kdbush@3.0.0: resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} @@ -7919,15 +7946,36 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.uniq@4.5.0: resolution: {integrity: sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==} @@ -14112,6 +14160,11 @@ snapshots: '@esbuild/win32-x64@0.25.11': optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.38.0(jiti@1.21.7))': + dependencies: + eslint: 9.38.0(jiti@1.21.7) + eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.0(eslint@9.38.0(jiti@2.6.1))': dependencies: eslint: 9.38.0(jiti@2.6.1) @@ -14863,6 +14916,12 @@ snapshots: '@nestjs/platform-express': 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.7) '@nestjs/websockets': 11.1.7(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.7)(@nestjs/platform-socket.io@11.1.7)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt@11.0.1(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.10 + jsonwebtoken: 9.0.2 + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)': dependencies: '@nestjs/common': 11.1.7(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -16411,6 +16470,11 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 22.18.13 + '@types/justified-layout@4.1.4': {} '@types/keygrip@1.0.6': {} @@ -16643,6 +16707,23 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 + '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.2 + eslint: 9.38.0(jiti@1.21.7) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -16660,6 +16741,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.46.2 + debug: 4.4.3 + eslint: 9.38.0(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.46.2 @@ -16690,6 +16783,18 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/type-utils@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.38.0(jiti@1.21.7) + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.46.2 @@ -16720,6 +16825,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@1.21.7)) + '@typescript-eslint/scope-manager': 8.46.2 + '@typescript-eslint/types': 8.46.2 + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + eslint: 9.38.0(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1)) @@ -16740,6 +16856,25 @@ snapshots: '@vercel/oidc@3.0.3': {} + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@ampproject/remapping': 2.3.0 @@ -16786,6 +16921,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 + '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -17377,6 +17520,8 @@ snapshots: buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -18329,6 +18474,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.243: {} @@ -18539,6 +18688,10 @@ snapshots: source-map: 0.6.1 optional: true + eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@1.21.7)): + dependencies: + eslint: 9.38.0(jiti@1.21.7) + eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)): dependencies: eslint: 9.38.0(jiti@2.6.1) @@ -18555,6 +18708,16 @@ snapshots: lodash.memoize: 4.1.2 semver: 7.7.3 + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@1.21.7)))(eslint@9.38.0(jiti@1.21.7))(prettier@3.6.2): + dependencies: + eslint: 9.38.0(jiti@1.21.7) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.11 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 10.1.8(eslint@9.38.0(jiti@1.21.7)) + eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint@9.38.0(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.38.0(jiti@2.6.1) @@ -18583,6 +18746,28 @@ snapshots: transitivePeerDependencies: - ts-node + eslint-plugin-unicorn@60.0.0(eslint@9.38.0(jiti@1.21.7)): + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@1.21.7)) + '@eslint/plugin-kit': 0.3.5 + change-case: 5.4.4 + ci-info: 4.3.0 + clean-regexp: 1.0.0 + core-js-compat: 3.46.0 + eslint: 9.38.0(jiti@1.21.7) + esquery: 1.6.0 + find-up-simple: 1.0.1 + globals: 16.4.0 + indent-string: 5.0.0 + is-builtin-module: 5.0.0 + jsesc: 3.1.0 + pluralize: 8.0.0 + regexp-tree: 0.1.27 + regjsparser: 0.12.0 + semver: 7.7.3 + strip-indent: 4.0.0 + eslint-plugin-unicorn@60.0.0(eslint@9.38.0(jiti@2.6.1)): dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -18641,6 +18826,47 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint@9.38.0(jiti@1.21.7): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@1.21.7)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.1 + '@eslint/core': 0.16.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.38.0 + '@eslint/plugin-kit': 0.4.0 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 1.21.7 + transitivePeerDependencies: + - supports-color + eslint@9.38.0(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1)) @@ -20143,10 +20369,34 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + just-compare@2.3.0: {} justified-layout@4.1.0: {} + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + kdbush@3.0.0: {} kdbush@4.0.2: {} @@ -20311,12 +20561,26 @@ snapshots: lodash.defaults@4.2.0: {} + lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.uniq@4.5.0: {} lodash@4.17.21: {} @@ -23927,6 +24191,17 @@ snapshots: typedarray@0.0.6: {} + typescript-eslint@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3))(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.46.2(typescript@5.9.3) + '@typescript-eslint/utils': 8.46.2(eslint@9.38.0(jiti@1.21.7))(typescript@5.9.3) + eslint: 9.38.0(jiti@1.21.7) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + typescript-eslint@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3): dependencies: '@typescript-eslint/eslint-plugin': 8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) @@ -24205,6 +24480,27 @@ snapshots: - rollup - supports-color + vite-node@3.2.4(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -24247,6 +24543,17 @@ snapshots: - tsx - yaml + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)): + dependencies: + debug: 4.4.3 + globrex: 0.1.2 + tsconfck: 3.1.6(typescript@5.9.3) + optionalDependencies: + vite: 7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + - typescript + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)): dependencies: debug: 4.4.3 @@ -24258,6 +24565,22 @@ snapshots: - supports-color - typescript + vite@7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.5 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.18.13 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.30.2 + terser: 5.44.0 + yaml: 2.8.1 + vite@7.1.12(@types/node@22.18.13)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1): dependencies: esbuild: 0.25.11 @@ -24298,6 +24621,50 @@ snapshots: dependencies: vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@1.21.7)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.2.0 + debug: 4.4.3 + expect-type: 1.2.1 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.12(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@22.18.13)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 22.18.13 + happy-dom: 20.0.8 + jsdom: 26.1.0(canvas@2.11.2) + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.18.13)(happy-dom@20.0.8)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 diff --git a/server/package.json b/server/package.json index f853880218..62501b96b0 100644 --- a/server/package.json +++ b/server/package.json @@ -37,6 +37,7 @@ "@nestjs/bullmq": "^11.0.1", "@nestjs/common": "^11.0.4", "@nestjs/core": "^11.0.4", + "@nestjs/jwt": "^11.0.1", "@nestjs/platform-express": "^11.0.4", "@nestjs/platform-socket.io": "^11.0.4", "@nestjs/schedule": "^6.0.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 2b02628db1..bd89c7ad59 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,6 +1,7 @@ import { BullModule } from '@nestjs/bullmq'; import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; +import { JwtModule } from '@nestjs/jwt'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; @@ -49,6 +50,13 @@ const commonImports = [ ClsModule.forRoot(cls.config), KyselyModule.forRoot(getKyselyConfig(database.config)), OpenTelemetryModule.forRoot(otel), + JwtModule.register({ + // used for maintenance mode JWTs + global: true, + signOptions: { + expiresIn: '1h', + }, + }), ]; const bullImports = [BullModule.forRoot(bull.config), BullModule.registerQueue(...bull.queues)]; diff --git a/server/src/controllers/maintenance.controller.ts b/server/src/controllers/maintenance.controller.ts index dc7a8a9b97..5b07025716 100644 --- a/server/src/controllers/maintenance.controller.ts +++ b/server/src/controllers/maintenance.controller.ts @@ -1,21 +1,42 @@ import { BadRequestException, Controller, Post, Res } from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; import { ApiTags } from '@nestjs/swagger'; import { Response } from 'express'; -import { MaintenanceModeResponseDto } from 'src/dtos/maintenance.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { MaintenanceAuthDto, MaintenanceModeResponseDto } from 'src/dtos/maintenance.dto'; import { ImmichCookie, Permission } from 'src/enum'; -import { Authenticated } from 'src/middleware/auth.guard'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MaintenanceService } from 'src/services/maintenance.service'; @ApiTags('Maintenance (admin)') @Controller('admin/maintenance') export class MaintenanceController { - constructor(private service: MaintenanceService) {} + constructor( + private service: MaintenanceService, + private jwtService: JwtService, + ) {} @Post('start') @Authenticated({ permission: Permission.Maintenance, admin: true }) - async startMaintenance(@Res({ passthrough: true }) response: Response): Promise { - await this.service.startMaintenance(); - response.cookie(ImmichCookie.MaintenanceToken, 'my-token'); + async startMaintenance( + @Auth() auth: AuthDto, + @Res({ passthrough: true }) response: Response, + ): Promise { + const { token } = await this.service.startMaintenance(); + + const jwt = await this.jwtService.signAsync( + { + data: { + username: auth.user.name, + } as MaintenanceAuthDto, + }, + { + secret: token, + }, + ); + + response.cookie(ImmichCookie.MaintenanceToken, jwt); + return { isMaintenanceMode: true }; } diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index 1a1911e130..0d9a1a8d22 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -5,4 +5,6 @@ export class MaintenanceModeResponseDto { isMaintenanceMode!: boolean; } -export class MaintenanceAuthDto {} +export class MaintenanceAuthDto { + username!: string; +} diff --git a/server/src/middleware/maintenance-auth.guard.ts b/server/src/middleware/maintenance-auth.guard.ts index 16cf79150a..4357bb17f1 100644 --- a/server/src/middleware/maintenance-auth.guard.ts +++ b/server/src/middleware/maintenance-auth.guard.ts @@ -3,15 +3,18 @@ import { ExecutionContext, Injectable, SetMetadata, + UnauthorizedException, applyDecorators, createParamDecorator, } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; +import { JwtService } from '@nestjs/jwt'; +import { parse } from 'cookie'; import { Request } from 'express'; import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; -import { MetadataKey } from 'src/enum'; +import { ImmichCookie, MetadataKey } from 'src/enum'; import { LoggingRepository } from 'src/repositories/logging.repository'; -import { MaintenanceWorkerService } from 'src/services/maintenance-worker.service'; +import { MaintenanceWorkerRepository } from 'src/repositories/maintenance-worker.repository'; export const MaintenanceRoute = (options = {}): MethodDecorator => { const decorators: MethodDecorator[] = [SetMetadata(MetadataKey.AuthRoute, options)]; @@ -19,15 +22,15 @@ export const MaintenanceRoute = (options = {}): MethodDecorator => { }; export interface MaintenanceAuthRequest extends Request { - user?: MaintenanceAuthDto; + maintenanceAuth?: MaintenanceAuthDto; } export interface MaintenanceAuthenticatedRequest extends Request { - user: MaintenanceAuthDto; + maintenanceAuth: MaintenanceAuthDto; } export const MaintenanceAuth = createParamDecorator((data, context: ExecutionContext): MaintenanceAuthDto => { - return context.switchToHttp().getRequest().user; + return context.switchToHttp().getRequest().maintenanceAuth; }); @Injectable() @@ -35,7 +38,8 @@ export class MaintenanceAuthGuard implements CanActivate { constructor( private logger: LoggingRepository, private reflector: Reflector, - private maintenanceWorkerService: MaintenanceWorkerService, + private maintenanceWorkerRepository: MaintenanceWorkerRepository, + private jwtService: JwtService, ) { this.logger.setContext(MaintenanceAuthGuard.name); } @@ -48,7 +52,23 @@ export class MaintenanceAuthGuard implements CanActivate { } const request = context.switchToHttp().getRequest(); - request.user = await this.maintenanceWorkerService.authenticate(request.headers); + const token = parse(request.headers.cookie || '')[ImmichCookie.MaintenanceToken]; + + if (!token) { + throw new UnauthorizedException('Missing JWT Token'); + } + + try { + const payload = await this.jwtService.verifyAsync(token, { + secret: await this.maintenanceWorkerRepository.maintenanceToken(), + }); + + request['maintenanceAuth'] = payload; + this.logger.debug(payload); + } catch { + throw new UnauthorizedException('Invalid JWT Token'); + } + return true; } } diff --git a/server/src/services/cli.service.ts b/server/src/services/cli.service.ts index dff70d64af..60074901da 100644 --- a/server/src/services/cli.service.ts +++ b/server/src/services/cli.service.ts @@ -53,12 +53,12 @@ export class CliService extends BaseService { async disableMaintenanceMode(): Promise { const state = { isMaintenanceMode: false as const }; - this.maintenanceRepository.setMaintenanceMode(state); + await this.maintenanceRepository.setMaintenanceMode(state); this.oneShotServerSend('AppRestart', state); } async enableMaintenanceMode(): Promise { - this.maintenanceRepository.enterMaintenanceMode(); + await this.maintenanceRepository.enterMaintenanceMode(); this.oneShotServerSend('AppRestart', { isMaintenanceMode: true, }); diff --git a/server/src/services/maintenance-worker.service.ts b/server/src/services/maintenance-worker.service.ts index 947d40edd5..b3f3350ead 100644 --- a/server/src/services/maintenance-worker.service.ts +++ b/server/src/services/maintenance-worker.service.ts @@ -1,8 +1,4 @@ -import { Injectable, UnauthorizedException } from '@nestjs/common'; -import { parse } from 'cookie'; -import { IncomingHttpHeaders } from 'node:http'; -import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto'; -import { ImmichCookie } from 'src/enum'; +import { Injectable } from '@nestjs/common'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { MaintenanceWorkerRepository } from 'src/repositories/maintenance-worker.repository'; @@ -56,17 +52,4 @@ export class MaintenanceWorkerService { async endMaintenance(): Promise { await this.maintenanceRepository.exitMaintenanceMode(); } - - private getCookieToken(headers: IncomingHttpHeaders): string | null { - const cookies = parse(headers.cookie || ''); - return cookies[ImmichCookie.MaintenanceToken] || null; - } - - async authenticate(headers: IncomingHttpHeaders): Promise { - if (this.getCookieToken(headers) !== 'my-token') { - throw new UnauthorizedException('Invalid maintenance key'); - } - - return {}; - } } diff --git a/web/src/routes/maintenance/+page.svelte b/web/src/routes/maintenance/+page.svelte index 29a184aebd..6a3952da6a 100644 --- a/web/src/routes/maintenance/+page.svelte +++ b/web/src/routes/maintenance/+page.svelte @@ -1,23 +1,55 @@ -
{$t('maintenance_title')}

{$t('maintenance_description')}

- - - +

+ + {/if}