feat: jwt auth for maintenance

This commit is contained in:
izzy 2025-11-05 13:44:43 +00:00
parent 5f4a72735c
commit 014669f4a4
No known key found for this signature in database
GPG key ID: 5059F398521BB0F6
9 changed files with 482 additions and 48 deletions

383
pnpm-lock.yaml generated
View file

@ -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

View file

@ -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",

View file

@ -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)];

View file

@ -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<MaintenanceModeResponseDto> {
await this.service.startMaintenance();
response.cookie(ImmichCookie.MaintenanceToken, 'my-token');
async startMaintenance(
@Auth() auth: AuthDto,
@Res({ passthrough: true }) response: Response,
): Promise<MaintenanceModeResponseDto> {
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 };
}

View file

@ -5,4 +5,6 @@ export class MaintenanceModeResponseDto {
isMaintenanceMode!: boolean;
}
export class MaintenanceAuthDto {}
export class MaintenanceAuthDto {
username!: string;
}

View file

@ -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<MaintenanceAuthenticatedRequest>().user;
return context.switchToHttp().getRequest<MaintenanceAuthenticatedRequest>().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<MaintenanceAuthRequest>();
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;
}
}

View file

@ -53,12 +53,12 @@ export class CliService extends BaseService {
async disableMaintenanceMode(): Promise<void> {
const state = { isMaintenanceMode: false as const };
this.maintenanceRepository.setMaintenanceMode(state);
await this.maintenanceRepository.setMaintenanceMode(state);
this.oneShotServerSend('AppRestart', state);
}
async enableMaintenanceMode(): Promise<void> {
this.maintenanceRepository.enterMaintenanceMode();
await this.maintenanceRepository.enterMaintenanceMode();
this.oneShotServerSend('AppRestart', {
isMaintenanceMode: true,
});

View file

@ -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<void> {
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<MaintenanceAuthDto> {
if (this.getCookieToken(headers) !== 'my-token') {
throw new UnauthorizedException('Invalid maintenance key');
}
return {};
}
}

View file

@ -1,23 +1,55 @@
<script>
<script lang="ts">
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { endMaintenance } from '@immich/sdk';
import { Button, Heading } from '@immich/ui';
import { t } from 'svelte-i18n';
function checkAuth() {
try {
const jwtCookie = document.cookie
.split(';')
.map((cookie) => cookie.split('=', 2).map((value) => value.trim()))
.find(([name]) => name === 'immich_maintenance_token');
if (jwtCookie) {
const [, jwt] = jwtCookie;
// decode the JWT
// https://stackoverflow.com/a/38552302
const encodedPayload = decodeURIComponent(
// eslint-disable-next-line unicorn/prefer-string-replace-all
atob(jwt.split('.')[1].replace(/-/g, '+').replace(/_/g, '/'))
.split('')
// eslint-disable-next-line unicorn/prefer-code-point
.map((c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join(''),
);
return JSON.parse(encodedPayload).data as {
username: string;
};
}
} catch (error) {
void error;
}
}
const user = checkAuth();
</script>
<AuthPageLayout>
<div class="flex flex-col place-items-center text-center gap-4">
<Heading size="large" color="primary" tag="h1">{$t('maintenance_title')}</Heading>
<p>{$t('maintenance_description')}</p>
<!-- {#if $user && $user.isAdmin}
{#if user}
<p>
{$t('maintenance_logged_in_as', {
values: {
user: $user.name,
user: user.username,
},
})}
</p> -->
<Button onclick={() => endMaintenance()}>{$t('maintenance_exit')}</Button>
<!-- {/if} -->
</p>
<Button onclick={() => endMaintenance()}>{$t('maintenance_exit')}</Button>
{/if}
</div>
</AuthPageLayout>