refactor: format
This commit is contained in:
parent
aeb25ac816
commit
a98e122fbc
|
@ -0,0 +1,7 @@
|
||||||
|
node_modules
|
||||||
|
dist/
|
||||||
|
config
|
||||||
|
build/
|
||||||
|
.eslintrc.js
|
||||||
|
package.json
|
||||||
|
tsconfig**.json
|
|
@ -0,0 +1,22 @@
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin', 'prettier'],
|
||||||
|
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-var-requires': 0,
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,10 @@
|
||||||
|
/dist/*
|
||||||
|
.local
|
||||||
|
.output.js
|
||||||
|
/node_modules/**
|
||||||
|
|
||||||
|
**/*.svg
|
||||||
|
**/*.sh
|
||||||
|
|
||||||
|
/public/*
|
||||||
|
test/**/*
|
|
@ -0,0 +1,11 @@
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 100, // 每行代码长度(默认80)
|
||||||
|
tabWidth: 2, // 每个tab相当于多少个空格(默认2)
|
||||||
|
useTabs: false, // 是否使用tab进行缩进(默认false)
|
||||||
|
singleQuote: true, // 使用单引号(默认false)
|
||||||
|
semi: true, // 声明结尾使用分号(默认true)
|
||||||
|
trailingComma: 'es5', // 多行使用拖尾逗号(默认none)
|
||||||
|
bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true)
|
||||||
|
arrowParens: 'avoid', // 只有一个参数的箭头函数的参数是否带圆括号(默认avoid)
|
||||||
|
endOfLine: 'auto', // 文件换行格式 LF/CRLF
|
||||||
|
};
|
|
@ -1,7 +1,8 @@
|
||||||
CREATE TABLE contracts (
|
CREATE TABLE contracts (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '合同编号',
|
id INT AUTO_INCREMENT PRIMARY KEY ,
|
||||||
contract_title VARCHAR(255) COMMENT '合同标题',
|
contract_number VARCHAR(255) COMMENT '合同编号',
|
||||||
contract_type VARCHAR(255) COMMENT '合同类型',
|
title VARCHAR(255) COMMENT '合同标题',
|
||||||
|
type VARCHAR(255) COMMENT '合同类型',
|
||||||
party_a VARCHAR(255) COMMENT '甲方',
|
party_a VARCHAR(255) COMMENT '甲方',
|
||||||
party_b VARCHAR(255) COMMENT '乙方',
|
party_b VARCHAR(255) COMMENT '乙方',
|
||||||
signing_date DATE COMMENT '签订日期',
|
signing_date DATE COMMENT '签订日期',
|
||||||
|
|
16
package.json
16
package.json
|
@ -43,7 +43,7 @@
|
||||||
"c": "git add . && git cz && git push",
|
"c": "git add . && git cz && git push",
|
||||||
"release": "standard-version",
|
"release": "standard-version",
|
||||||
"commitlint": "commitlint --config commitlint.config.cjs -e -V",
|
"commitlint": "commitlint --config commitlint.config.cjs -e -V",
|
||||||
"lint:prettier": "prettier --write \"**/*.{js,cjs,ts,json,md}\""
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/cookie": "^9.3.1",
|
"@fastify/cookie": "^9.3.1",
|
||||||
|
@ -108,7 +108,6 @@
|
||||||
"winston-daily-rotate-file": "^5.0.0"
|
"winston-daily-rotate-file": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@antfu/eslint-config": "^2.6.4",
|
|
||||||
"@compodoc/compodoc": "^1.1.23",
|
"@compodoc/compodoc": "^1.1.23",
|
||||||
"@nestjs/cli": "^10.3.2",
|
"@nestjs/cli": "^10.3.2",
|
||||||
"@nestjs/schematics": "^10.1.1",
|
"@nestjs/schematics": "^10.1.1",
|
||||||
|
@ -119,10 +118,14 @@
|
||||||
"@types/node": "^20.11.16",
|
"@types/node": "^20.11.16",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||||
|
"@typescript-eslint/parser": "^5.0.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.0.1",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
|
"prettier": "~3.2.5",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"lint-staged": "^15.2.2",
|
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
|
@ -136,11 +139,6 @@
|
||||||
"standard-version": "^9.5.0",
|
"standard-version": "^9.5.0",
|
||||||
"husky": "^8.0.0"
|
"husky": "^8.0.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
|
||||||
"overrides": {
|
|
||||||
"@liaoliaots/nestjs-redis": "npm:@songkeys/nestjs-redis"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"config": {
|
"config": {
|
||||||
"commitizen": {
|
"commitizen": {
|
||||||
"path": "cz-customizable"
|
"path": "cz-customizable"
|
||||||
|
|
587
pnpm-lock.yaml
587
pnpm-lock.yaml
|
@ -238,6 +238,18 @@ devDependencies:
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^8.56.0
|
specifier: ^8.56.0
|
||||||
version: 8.56.0
|
version: 8.56.0
|
||||||
|
eslint-config-prettier:
|
||||||
|
specifier: ~9.1.0
|
||||||
|
version: 9.1.0(eslint@8.56.0)
|
||||||
|
eslint-define-config:
|
||||||
|
specifier: ~2.1.0
|
||||||
|
version: 2.1.0
|
||||||
|
eslint-plugin-import:
|
||||||
|
specifier: ~2.29.1
|
||||||
|
version: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)
|
||||||
|
eslint-plugin-prettier:
|
||||||
|
specifier: ~5.1.3
|
||||||
|
version: 5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.2.5)
|
||||||
husky:
|
husky:
|
||||||
specifier: ^8.0.0
|
specifier: ^8.0.0
|
||||||
version: 8.0.3
|
version: 8.0.3
|
||||||
|
@ -247,6 +259,9 @@ devDependencies:
|
||||||
lint-staged:
|
lint-staged:
|
||||||
specifier: ^15.2.2
|
specifier: ^15.2.2
|
||||||
version: 15.2.2
|
version: 15.2.2
|
||||||
|
prettier:
|
||||||
|
specifier: ~3.2.5
|
||||||
|
version: 3.2.5
|
||||||
source-map-support:
|
source-map-support:
|
||||||
specifier: ^0.5.21
|
specifier: ^0.5.21
|
||||||
version: 0.5.21
|
version: 0.5.21
|
||||||
|
@ -2921,6 +2936,11 @@ packages:
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
/@pkgr/core@0.1.1:
|
||||||
|
resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==}
|
||||||
|
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@selderee/plugin-htmlparser2@0.11.0:
|
/@selderee/plugin-htmlparser2@0.11.0:
|
||||||
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3228,6 +3248,10 @@ packages:
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/json5@0.0.29:
|
||||||
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/jsonwebtoken@9.0.5:
|
/@types/jsonwebtoken@9.0.5:
|
||||||
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
|
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -3920,6 +3944,14 @@ packages:
|
||||||
/argparse@2.0.1:
|
/argparse@2.0.1:
|
||||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||||
|
|
||||||
|
/array-buffer-byte-length@1.0.1:
|
||||||
|
resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
is-array-buffer: 3.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/array-from@2.1.1:
|
/array-from@2.1.1:
|
||||||
resolution: {integrity: sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==}
|
resolution: {integrity: sha512-GQTc6Uupx1FCavi5mPzBvVT7nEOeWMmUA9P95wpfpW1XwMSKs+KaymD5C2Up7KAUKg/mYwbsUYzdZWcoajlNZg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3928,6 +3960,17 @@ packages:
|
||||||
resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
|
resolution: {integrity: sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/array-includes@3.1.7:
|
||||||
|
resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
get-intrinsic: 1.2.4
|
||||||
|
is-string: 1.0.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
/array-timsort@1.0.3:
|
/array-timsort@1.0.3:
|
||||||
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -3937,6 +3980,62 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/array.prototype.filter@1.0.3:
|
||||||
|
resolution: {integrity: sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
es-array-method-boxes-properly: 1.0.0
|
||||||
|
is-string: 1.0.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/array.prototype.findlastindex@1.2.4:
|
||||||
|
resolution: {integrity: sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
es-shim-unscopables: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/array.prototype.flat@1.3.2:
|
||||||
|
resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
es-shim-unscopables: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/array.prototype.flatmap@1.3.2:
|
||||||
|
resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
es-shim-unscopables: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/arraybuffer.prototype.slice@1.0.3:
|
||||||
|
resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
array-buffer-byte-length: 1.0.1
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.2.4
|
||||||
|
is-array-buffer: 3.0.4
|
||||||
|
is-shared-array-buffer: 1.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/arrify@1.0.1:
|
/arrify@1.0.1:
|
||||||
resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
|
resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -3980,6 +4079,13 @@ packages:
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/available-typed-arrays@1.0.7:
|
||||||
|
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
possible-typed-array-names: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/avvio@8.3.0:
|
/avvio@8.3.0:
|
||||||
resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==}
|
resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5452,6 +5558,13 @@ packages:
|
||||||
run-applescript: 3.2.0
|
run-applescript: 3.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/doctrine@2.1.0:
|
||||||
|
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
dependencies:
|
||||||
|
esutils: 2.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/doctrine@3.0.0:
|
/doctrine@3.0.0:
|
||||||
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
@ -5689,6 +5802,57 @@ packages:
|
||||||
stackframe: 1.3.4
|
stackframe: 1.3.4
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/es-abstract@1.22.4:
|
||||||
|
resolution: {integrity: sha512-vZYJlk2u6qHYxBOTjAeg7qUxHdNfih64Uu2J8QqWgXZ2cri0ZpJAkzDUK/q593+mvKwlxyaxr6F1Q+3LKoQRgg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
array-buffer-byte-length: 1.0.1
|
||||||
|
arraybuffer.prototype.slice: 1.0.3
|
||||||
|
available-typed-arrays: 1.0.7
|
||||||
|
call-bind: 1.0.7
|
||||||
|
es-define-property: 1.0.0
|
||||||
|
es-errors: 1.3.0
|
||||||
|
es-set-tostringtag: 2.0.3
|
||||||
|
es-to-primitive: 1.2.1
|
||||||
|
function.prototype.name: 1.1.6
|
||||||
|
get-intrinsic: 1.2.4
|
||||||
|
get-symbol-description: 1.0.2
|
||||||
|
globalthis: 1.0.3
|
||||||
|
gopd: 1.0.1
|
||||||
|
has-property-descriptors: 1.0.2
|
||||||
|
has-proto: 1.0.1
|
||||||
|
has-symbols: 1.0.3
|
||||||
|
hasown: 2.0.1
|
||||||
|
internal-slot: 1.0.7
|
||||||
|
is-array-buffer: 3.0.4
|
||||||
|
is-callable: 1.2.7
|
||||||
|
is-negative-zero: 2.0.3
|
||||||
|
is-regex: 1.1.4
|
||||||
|
is-shared-array-buffer: 1.0.3
|
||||||
|
is-string: 1.0.7
|
||||||
|
is-typed-array: 1.1.13
|
||||||
|
is-weakref: 1.0.2
|
||||||
|
object-inspect: 1.13.1
|
||||||
|
object-keys: 1.1.1
|
||||||
|
object.assign: 4.1.5
|
||||||
|
regexp.prototype.flags: 1.5.2
|
||||||
|
safe-array-concat: 1.1.0
|
||||||
|
safe-regex-test: 1.0.3
|
||||||
|
string.prototype.trim: 1.2.8
|
||||||
|
string.prototype.trimend: 1.0.7
|
||||||
|
string.prototype.trimstart: 1.0.7
|
||||||
|
typed-array-buffer: 1.0.2
|
||||||
|
typed-array-byte-length: 1.0.1
|
||||||
|
typed-array-byte-offset: 1.0.2
|
||||||
|
typed-array-length: 1.0.5
|
||||||
|
unbox-primitive: 1.0.2
|
||||||
|
which-typed-array: 1.1.14
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/es-array-method-boxes-properly@1.0.0:
|
||||||
|
resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/es-define-property@1.0.0:
|
/es-define-property@1.0.0:
|
||||||
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
|
resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -5703,6 +5867,30 @@ packages:
|
||||||
resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==}
|
resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/es-set-tostringtag@2.0.3:
|
||||||
|
resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
get-intrinsic: 1.2.4
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
hasown: 2.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/es-shim-unscopables@1.0.2:
|
||||||
|
resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==}
|
||||||
|
dependencies:
|
||||||
|
hasown: 2.0.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/es-to-primitive@1.2.1:
|
||||||
|
resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
is-callable: 1.2.7
|
||||||
|
is-date-object: 1.0.5
|
||||||
|
is-symbol: 1.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/es5-ext@0.10.62:
|
/es5-ext@0.10.62:
|
||||||
resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==}
|
resolution: {integrity: sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==}
|
||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
|
@ -5855,6 +6043,20 @@ packages:
|
||||||
parse-gitignore: 2.0.0
|
parse-gitignore: 2.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-config-prettier@9.1.0(eslint@8.56.0):
|
||||||
|
resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
eslint: '>=7.0.0'
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.56.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/eslint-define-config@2.1.0:
|
||||||
|
resolution: {integrity: sha512-QUp6pM9pjKEVannNAbSJNeRuYwW3LshejfyBBpjeMGaJjaDUpVps4C6KVR8R7dWZnD3i0synmrE36znjTkJvdQ==}
|
||||||
|
engines: {node: '>=18.0.0', npm: '>=9.0.0', pnpm: '>=8.6.0'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-import-resolver-node@0.3.9:
|
/eslint-import-resolver-node@0.3.9:
|
||||||
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
|
resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5955,6 +6157,41 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.56.0):
|
||||||
|
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
peerDependencies:
|
||||||
|
'@typescript-eslint/parser': '*'
|
||||||
|
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@typescript-eslint/parser':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||||
|
array-includes: 3.1.7
|
||||||
|
array.prototype.findlastindex: 1.2.4
|
||||||
|
array.prototype.flat: 1.3.2
|
||||||
|
array.prototype.flatmap: 1.3.2
|
||||||
|
debug: 3.2.7
|
||||||
|
doctrine: 2.1.0
|
||||||
|
eslint: 8.56.0
|
||||||
|
eslint-import-resolver-node: 0.3.9
|
||||||
|
eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint@8.56.0)
|
||||||
|
hasown: 2.0.1
|
||||||
|
is-core-module: 2.13.1
|
||||||
|
is-glob: 4.0.3
|
||||||
|
minimatch: 3.1.2
|
||||||
|
object.fromentries: 2.0.7
|
||||||
|
object.groupby: 1.0.2
|
||||||
|
object.values: 1.1.7
|
||||||
|
semver: 6.3.1
|
||||||
|
tsconfig-paths: 3.15.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- eslint-import-resolver-typescript
|
||||||
|
- eslint-import-resolver-webpack
|
||||||
|
- supports-color
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-jsdoc@48.1.0(eslint@8.56.0):
|
/eslint-plugin-jsdoc@48.1.0(eslint@8.56.0):
|
||||||
resolution: {integrity: sha512-g9S8ukmTd1DVcV/xeBYPPXOZ6rc8WJ4yi0+MVxJ1jBOrz5kmxV9gJJQ64ltCqIWFnBChLIhLVx3tbTSarqVyFA==}
|
resolution: {integrity: sha512-g9S8ukmTd1DVcV/xeBYPPXOZ6rc8WJ4yi0+MVxJ1jBOrz5kmxV9gJJQ64ltCqIWFnBChLIhLVx3tbTSarqVyFA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
@ -6056,6 +6293,27 @@ packages:
|
||||||
- typescript
|
- typescript
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/eslint-plugin-prettier@5.1.3(eslint-config-prettier@9.1.0)(eslint@8.56.0)(prettier@3.2.5):
|
||||||
|
resolution: {integrity: sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/eslint': '>=8.0.0'
|
||||||
|
eslint: '>=8.0.0'
|
||||||
|
eslint-config-prettier: '*'
|
||||||
|
prettier: '>=3.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/eslint':
|
||||||
|
optional: true
|
||||||
|
eslint-config-prettier:
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
eslint: 8.56.0
|
||||||
|
eslint-config-prettier: 9.1.0(eslint@8.56.0)
|
||||||
|
prettier: 3.2.5
|
||||||
|
prettier-linter-helpers: 1.0.0
|
||||||
|
synckit: 0.8.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
/eslint-plugin-toml@0.9.2(eslint@8.56.0):
|
/eslint-plugin-toml@0.9.2(eslint@8.56.0):
|
||||||
resolution: {integrity: sha512-ri0xf63PYf3pIq/WY9BIwrqxZmGTIwSkAO0bHddI0ajUwN4KGz6W8vOvdXFHOpRdRfzxlmXze/vfsY/aTEXESg==}
|
resolution: {integrity: sha512-ri0xf63PYf3pIq/WY9BIwrqxZmGTIwSkAO0bHddI0ajUwN4KGz6W8vOvdXFHOpRdRfzxlmXze/vfsY/aTEXESg==}
|
||||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||||
|
@ -6496,6 +6754,10 @@ packages:
|
||||||
/fast-deep-equal@3.1.3:
|
/fast-deep-equal@3.1.3:
|
||||||
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
|
||||||
|
|
||||||
|
/fast-diff@1.3.0:
|
||||||
|
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/fast-glob@3.3.2:
|
/fast-glob@3.3.2:
|
||||||
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==}
|
||||||
engines: {node: '>=8.6.0'}
|
engines: {node: '>=8.6.0'}
|
||||||
|
@ -6773,6 +7035,12 @@ packages:
|
||||||
optional: true
|
optional: true
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/for-each@0.3.3:
|
||||||
|
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
|
||||||
|
dependencies:
|
||||||
|
is-callable: 1.2.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
/foreground-child@3.1.1:
|
/foreground-child@3.1.1:
|
||||||
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
|
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
@ -6887,6 +7155,16 @@ packages:
|
||||||
/function-bind@1.1.2:
|
/function-bind@1.1.2:
|
||||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||||
|
|
||||||
|
/function.prototype.name@1.1.6:
|
||||||
|
resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
functions-have-names: 1.2.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/functions-have-names@1.2.3:
|
/functions-have-names@1.2.3:
|
||||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -6968,6 +7246,15 @@ packages:
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/get-symbol-description@1.0.2:
|
||||||
|
resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
es-errors: 1.3.0
|
||||||
|
get-intrinsic: 1.2.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/get-tsconfig@4.7.2:
|
/get-tsconfig@4.7.2:
|
||||||
resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==}
|
resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7109,6 +7396,13 @@ packages:
|
||||||
type-fest: 0.20.2
|
type-fest: 0.20.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/globalthis@1.0.3:
|
||||||
|
resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
define-properties: 1.2.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
/globby@11.1.0:
|
/globby@11.1.0:
|
||||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -7156,6 +7450,10 @@ packages:
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/has-bigints@1.0.2:
|
||||||
|
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/has-flag@3.0.0:
|
/has-flag@3.0.0:
|
||||||
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -7179,6 +7477,11 @@ packages:
|
||||||
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
|
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
/has-proto@1.0.3:
|
||||||
|
resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/has-symbols@1.0.3:
|
/has-symbols@1.0.3:
|
||||||
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
@ -7504,6 +7807,15 @@ packages:
|
||||||
wrap-ansi: 6.2.0
|
wrap-ansi: 6.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/internal-slot@1.0.7:
|
||||||
|
resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
es-errors: 1.3.0
|
||||||
|
hasown: 2.0.1
|
||||||
|
side-channel: 1.0.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
/interpret@1.4.0:
|
/interpret@1.4.0:
|
||||||
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
|
resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
@ -7554,6 +7866,14 @@ packages:
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-array-buffer@3.0.4:
|
||||||
|
resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
get-intrinsic: 1.2.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-arrayish@0.2.1:
|
/is-arrayish@0.2.1:
|
||||||
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -7562,12 +7882,26 @@ packages:
|
||||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/is-bigint@1.0.4:
|
||||||
|
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
|
||||||
|
dependencies:
|
||||||
|
has-bigints: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-binary-path@2.1.0:
|
/is-binary-path@2.1.0:
|
||||||
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dependencies:
|
dependencies:
|
||||||
binary-extensions: 2.2.0
|
binary-extensions: 2.2.0
|
||||||
|
|
||||||
|
/is-boolean-object@1.1.2:
|
||||||
|
resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-builtin-module@3.2.1:
|
/is-builtin-module@3.2.1:
|
||||||
resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
|
resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -7575,6 +7909,11 @@ packages:
|
||||||
builtin-modules: 3.3.0
|
builtin-modules: 3.3.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-callable@1.2.7:
|
||||||
|
resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-core-module@2.13.1:
|
/is-core-module@2.13.1:
|
||||||
resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
|
resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -7654,6 +7993,18 @@ packages:
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-negative-zero@2.0.3:
|
||||||
|
resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/is-number-object@1.0.7:
|
||||||
|
resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-number@7.0.0:
|
/is-number@7.0.0:
|
||||||
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
@ -7689,6 +8040,13 @@ packages:
|
||||||
call-bind: 1.0.7
|
call-bind: 1.0.7
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
|
/is-shared-array-buffer@1.0.3:
|
||||||
|
resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-stream@1.1.0:
|
/is-stream@1.1.0:
|
||||||
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
resolution: {integrity: sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -7703,6 +8061,20 @@ packages:
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-string@1.0.7:
|
||||||
|
resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/is-symbol@1.0.4:
|
||||||
|
resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
has-symbols: 1.0.3
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-text-path@1.0.1:
|
/is-text-path@1.0.1:
|
||||||
resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==}
|
resolution: {integrity: sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -7710,6 +8082,13 @@ packages:
|
||||||
text-extensions: 1.9.0
|
text-extensions: 1.9.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-typed-array@1.1.13:
|
||||||
|
resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
which-typed-array: 1.1.14
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-unicode-supported@0.1.0:
|
/is-unicode-supported@0.1.0:
|
||||||
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
@ -7724,6 +8103,12 @@ packages:
|
||||||
resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
|
resolution: {integrity: sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/is-weakref@1.0.2:
|
||||||
|
resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
dev: true
|
||||||
|
|
||||||
/is-windows@1.0.2:
|
/is-windows@1.0.2:
|
||||||
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
|
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -7739,6 +8124,10 @@ packages:
|
||||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/isarray@2.0.5:
|
||||||
|
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/isexe@2.0.0:
|
/isexe@2.0.0:
|
||||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||||
|
|
||||||
|
@ -8342,6 +8731,13 @@ packages:
|
||||||
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/json5@1.0.2:
|
||||||
|
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||||
|
hasBin: true
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.8
|
||||||
|
dev: true
|
||||||
|
|
||||||
/json5@2.2.3:
|
/json5@2.2.3:
|
||||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -9728,6 +10124,44 @@ packages:
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/object.assign@4.1.5:
|
||||||
|
resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
has-symbols: 1.0.3
|
||||||
|
object-keys: 1.1.1
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/object.fromentries@2.0.7:
|
||||||
|
resolution: {integrity: sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/object.groupby@1.0.2:
|
||||||
|
resolution: {integrity: sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==}
|
||||||
|
dependencies:
|
||||||
|
array.prototype.filter: 1.0.3
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
es-errors: 1.3.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/object.values@1.1.7:
|
||||||
|
resolution: {integrity: sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/obliterator@2.0.4:
|
/obliterator@2.0.4:
|
||||||
resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==}
|
resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
@ -10322,6 +10756,11 @@ packages:
|
||||||
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
|
resolution: {integrity: sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/possible-typed-array-names@1.0.0:
|
||||||
|
resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/postcss-selector-parser@6.0.15:
|
/postcss-selector-parser@6.0.15:
|
||||||
resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==}
|
resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
@ -10349,6 +10788,19 @@ packages:
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/prettier-linter-helpers@1.0.0:
|
||||||
|
resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
dependencies:
|
||||||
|
fast-diff: 1.3.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/prettier@3.2.5:
|
||||||
|
resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
hasBin: true
|
||||||
|
dev: true
|
||||||
|
|
||||||
/pretty-format@29.7.0:
|
/pretty-format@29.7.0:
|
||||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||||
|
@ -10985,6 +11437,16 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
|
|
||||||
|
/safe-array-concat@1.1.0:
|
||||||
|
resolution: {integrity: sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
get-intrinsic: 1.2.4
|
||||||
|
has-symbols: 1.0.3
|
||||||
|
isarray: 2.0.5
|
||||||
|
dev: true
|
||||||
|
|
||||||
/safe-buffer@5.1.2:
|
/safe-buffer@5.1.2:
|
||||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -10992,6 +11454,15 @@ packages:
|
||||||
/safe-buffer@5.2.1:
|
/safe-buffer@5.2.1:
|
||||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||||
|
|
||||||
|
/safe-regex-test@1.0.3:
|
||||||
|
resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
es-errors: 1.3.0
|
||||||
|
is-regex: 1.1.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/safe-regex2@2.0.0:
|
/safe-regex2@2.0.0:
|
||||||
resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==}
|
resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -11527,6 +11998,31 @@ packages:
|
||||||
strip-ansi: 7.1.0
|
strip-ansi: 7.1.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/string.prototype.trim@1.2.8:
|
||||||
|
resolution: {integrity: sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/string.prototype.trimend@1.0.7:
|
||||||
|
resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/string.prototype.trimstart@1.0.7:
|
||||||
|
resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
define-properties: 1.2.1
|
||||||
|
es-abstract: 1.22.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
/string_decoder@1.1.1:
|
/string_decoder@1.1.1:
|
||||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -11690,6 +12186,14 @@ packages:
|
||||||
tslib: 2.6.2
|
tslib: 2.6.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/synckit@0.8.8:
|
||||||
|
resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==}
|
||||||
|
engines: {node: ^14.18.0 || >=16.0.0}
|
||||||
|
dependencies:
|
||||||
|
'@pkgr/core': 0.1.1
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/systeminformation@5.22.0:
|
/systeminformation@5.22.0:
|
||||||
resolution: {integrity: sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==}
|
resolution: {integrity: sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
|
@ -11988,6 +12492,15 @@ packages:
|
||||||
tsconfig-paths: 4.2.0
|
tsconfig-paths: 4.2.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/tsconfig-paths@3.15.0:
|
||||||
|
resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
|
||||||
|
dependencies:
|
||||||
|
'@types/json5': 0.0.29
|
||||||
|
json5: 1.0.2
|
||||||
|
minimist: 1.2.8
|
||||||
|
strip-bom: 3.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tsconfig-paths@4.2.0:
|
/tsconfig-paths@4.2.0:
|
||||||
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -12070,6 +12583,50 @@ packages:
|
||||||
resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==}
|
resolution: {integrity: sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/typed-array-buffer@1.0.2:
|
||||||
|
resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
es-errors: 1.3.0
|
||||||
|
is-typed-array: 1.1.13
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/typed-array-byte-length@1.0.1:
|
||||||
|
resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
for-each: 0.3.3
|
||||||
|
gopd: 1.0.1
|
||||||
|
has-proto: 1.0.3
|
||||||
|
is-typed-array: 1.1.13
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/typed-array-byte-offset@1.0.2:
|
||||||
|
resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
available-typed-arrays: 1.0.7
|
||||||
|
call-bind: 1.0.7
|
||||||
|
for-each: 0.3.3
|
||||||
|
gopd: 1.0.1
|
||||||
|
has-proto: 1.0.3
|
||||||
|
is-typed-array: 1.1.13
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/typed-array-length@1.0.5:
|
||||||
|
resolution: {integrity: sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
for-each: 0.3.3
|
||||||
|
gopd: 1.0.1
|
||||||
|
has-proto: 1.0.3
|
||||||
|
is-typed-array: 1.1.13
|
||||||
|
possible-typed-array-names: 1.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/typedarray@0.0.6:
|
/typedarray@0.0.6:
|
||||||
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
@ -12191,6 +12748,15 @@ packages:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@lukeed/csprng': 1.1.0
|
'@lukeed/csprng': 1.1.0
|
||||||
|
|
||||||
|
/unbox-primitive@1.0.2:
|
||||||
|
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
|
||||||
|
dependencies:
|
||||||
|
call-bind: 1.0.7
|
||||||
|
has-bigints: 1.0.2
|
||||||
|
has-symbols: 1.0.3
|
||||||
|
which-boxed-primitive: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/undici-types@5.26.5:
|
/undici-types@5.26.5:
|
||||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||||
|
|
||||||
|
@ -12517,6 +13083,27 @@ packages:
|
||||||
tr46: 0.0.3
|
tr46: 0.0.3
|
||||||
webidl-conversions: 3.0.1
|
webidl-conversions: 3.0.1
|
||||||
|
|
||||||
|
/which-boxed-primitive@1.0.2:
|
||||||
|
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
|
||||||
|
dependencies:
|
||||||
|
is-bigint: 1.0.4
|
||||||
|
is-boolean-object: 1.1.2
|
||||||
|
is-number-object: 1.0.7
|
||||||
|
is-string: 1.0.7
|
||||||
|
is-symbol: 1.0.4
|
||||||
|
dev: true
|
||||||
|
|
||||||
|
/which-typed-array@1.1.14:
|
||||||
|
resolution: {integrity: sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==}
|
||||||
|
engines: {node: '>= 0.4'}
|
||||||
|
dependencies:
|
||||||
|
available-typed-arrays: 1.0.7
|
||||||
|
call-bind: 1.0.7
|
||||||
|
for-each: 0.3.3
|
||||||
|
gopd: 1.0.1
|
||||||
|
has-tostringtag: 1.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/which@1.3.1:
|
/which@1.3.1:
|
||||||
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
import { ClassSerializerInterceptor, Module } from '@nestjs/common'
|
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ConfigModule } from '@nestjs/config'
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'
|
import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
|
||||||
import config from '~/config'
|
import config from '~/config';
|
||||||
import { SharedModule } from '~/shared/shared.module'
|
import { SharedModule } from '~/shared/shared.module';
|
||||||
|
|
||||||
import { AllExceptionsFilter } from './common/filters/any-exception.filter'
|
import { AllExceptionsFilter } from './common/filters/any-exception.filter';
|
||||||
|
|
||||||
import { IdempotenceInterceptor } from './common/interceptors/idempotence.interceptor'
|
import { IdempotenceInterceptor } from './common/interceptors/idempotence.interceptor';
|
||||||
import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor'
|
import { TimeoutInterceptor } from './common/interceptors/timeout.interceptor';
|
||||||
import { TransformInterceptor } from './common/interceptors/transform.interceptor'
|
import { TransformInterceptor } from './common/interceptors/transform.interceptor';
|
||||||
import { AuthModule } from './modules/auth/auth.module'
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard'
|
import { JwtAuthGuard } from './modules/auth/guards/jwt-auth.guard';
|
||||||
import { RbacGuard } from './modules/auth/guards/rbac.guard'
|
import { RbacGuard } from './modules/auth/guards/rbac.guard';
|
||||||
import { HealthModule } from './modules/health/health.module'
|
import { HealthModule } from './modules/health/health.module';
|
||||||
import { NetdiskModule } from './modules/netdisk/netdisk.module'
|
import { NetdiskModule } from './modules/netdisk/netdisk.module';
|
||||||
import { SseModule } from './modules/sse/sse.module'
|
import { SseModule } from './modules/sse/sse.module';
|
||||||
import { SystemModule } from './modules/system/system.module'
|
import { SystemModule } from './modules/system/system.module';
|
||||||
import { TasksModule } from './modules/tasks/tasks.module'
|
import { TasksModule } from './modules/tasks/tasks.module';
|
||||||
import { TodoModule } from './modules/todo/todo.module'
|
import { TodoModule } from './modules/todo/todo.module';
|
||||||
import { ToolsModule } from './modules/tools/tools.module'
|
import { ToolsModule } from './modules/tools/tools.module';
|
||||||
import { DatabaseModule } from './shared/database/database.module'
|
import { DatabaseModule } from './shared/database/database.module';
|
||||||
|
|
||||||
import { SocketModule } from './socket/socket.module'
|
import { SocketModule } from './socket/socket.module';
|
||||||
|
import { ContractModule } from './modules/contract/contract.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -51,6 +52,8 @@ import { SocketModule } from './socket/socket.module'
|
||||||
// end biz
|
// end biz
|
||||||
|
|
||||||
TodoModule,
|
TodoModule,
|
||||||
|
|
||||||
|
ContractModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
|
{ provide: APP_FILTER, useClass: AllExceptionsFilter },
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import FastifyCookie from '@fastify/cookie'
|
import FastifyCookie from '@fastify/cookie';
|
||||||
import FastifyMultipart from '@fastify/multipart'
|
import FastifyMultipart from '@fastify/multipart';
|
||||||
import { FastifyAdapter } from '@nestjs/platform-fastify'
|
import { FastifyAdapter } from '@nestjs/platform-fastify';
|
||||||
|
|
||||||
const app: FastifyAdapter = new FastifyAdapter({
|
const app: FastifyAdapter = new FastifyAdapter({
|
||||||
trustProxy: true,
|
trustProxy: true,
|
||||||
logger: false,
|
logger: false,
|
||||||
// forceCloseConnections: true,
|
// forceCloseConnections: true,
|
||||||
})
|
});
|
||||||
export { app as fastifyApp }
|
export { app as fastifyApp };
|
||||||
|
|
||||||
app.register(FastifyMultipart, {
|
app.register(FastifyMultipart, {
|
||||||
limits: {
|
limits: {
|
||||||
|
@ -15,32 +15,30 @@ app.register(FastifyMultipart, {
|
||||||
fileSize: 1024 * 1024 * 6, // limit size 6M
|
fileSize: 1024 * 1024 * 6, // limit size 6M
|
||||||
files: 5, // Max number of file fields
|
files: 5, // Max number of file fields
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
app.register(FastifyCookie, {
|
app.register(FastifyCookie, {
|
||||||
secret: 'cookie-secret', // 这个 secret 不太重要,不存鉴权相关,无关紧要
|
secret: 'cookie-secret', // 这个 secret 不太重要,不存鉴权相关,无关紧要
|
||||||
})
|
});
|
||||||
|
|
||||||
app.getInstance().addHook('onRequest', (request, reply, done) => {
|
app.getInstance().addHook('onRequest', (request, reply, done) => {
|
||||||
// set undefined origin
|
// set undefined origin
|
||||||
const { origin } = request.headers
|
const { origin } = request.headers;
|
||||||
if (!origin)
|
if (!origin) request.headers.origin = request.headers.host;
|
||||||
request.headers.origin = request.headers.host
|
|
||||||
|
|
||||||
// forbidden php
|
// forbidden php
|
||||||
|
|
||||||
const { url } = request
|
const { url } = request;
|
||||||
|
|
||||||
if (url.endsWith('.php')) {
|
if (url.endsWith('.php')) {
|
||||||
reply.raw.statusMessage
|
reply.raw.statusMessage =
|
||||||
= 'Eh. PHP is not support on this machine. Yep, I also think PHP is bestest programming language. But for me it is beyond my reach.'
|
'Eh. PHP is not support on this machine. Yep, I also think PHP is bestest programming language. But for me it is beyond my reach.';
|
||||||
|
|
||||||
return reply.code(418).send()
|
return reply.code(418).send();
|
||||||
}
|
}
|
||||||
|
|
||||||
// skip favicon request
|
// skip favicon request
|
||||||
if (url.match(/favicon.ico$/) || url.match(/manifest.json$/))
|
if (url.match(/favicon.ico$/) || url.match(/manifest.json$/)) return reply.code(204).send();
|
||||||
return reply.code(204).send()
|
|
||||||
|
|
||||||
done()
|
done();
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,26 +1,26 @@
|
||||||
import { INestApplication } from '@nestjs/common'
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { IoAdapter } from '@nestjs/platform-socket.io'
|
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||||
import { createAdapter } from '@socket.io/redis-adapter'
|
import { createAdapter } from '@socket.io/redis-adapter';
|
||||||
|
|
||||||
import { REDIS_PUBSUB } from '~/shared/redis/redis.constant'
|
import { REDIS_PUBSUB } from '~/shared/redis/redis.constant';
|
||||||
|
|
||||||
export const RedisIoAdapterKey = 'm-shop-socket'
|
export const RedisIoAdapterKey = 'm-shop-socket';
|
||||||
|
|
||||||
export class RedisIoAdapter extends IoAdapter {
|
export class RedisIoAdapter extends IoAdapter {
|
||||||
constructor(private readonly app: INestApplication) {
|
constructor(private readonly app: INestApplication) {
|
||||||
super(app)
|
super(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
createIOServer(port: number, options?: any) {
|
createIOServer(port: number, options?: any) {
|
||||||
const server = super.createIOServer(port, options)
|
const server = super.createIOServer(port, options);
|
||||||
|
|
||||||
const { pubClient, subClient } = this.app.get(REDIS_PUBSUB)
|
const { pubClient, subClient } = this.app.get(REDIS_PUBSUB);
|
||||||
|
|
||||||
const redisAdapter = createAdapter(pubClient, subClient, {
|
const redisAdapter = createAdapter(pubClient, subClient, {
|
||||||
key: RedisIoAdapterKey,
|
key: RedisIoAdapterKey,
|
||||||
requestsTimeout: 10000,
|
requestsTimeout: 10000,
|
||||||
})
|
});
|
||||||
server.adapter(redisAdapter)
|
server.adapter(redisAdapter);
|
||||||
return server
|
return server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { HttpStatus, Type, applyDecorators } from '@nestjs/common'
|
import { HttpStatus, Type, applyDecorators } from '@nestjs/common';
|
||||||
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger'
|
import { ApiExtraModels, ApiResponse, getSchemaPath } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { ResOp } from '~/common/model/response.model'
|
import { ResOp } from '~/common/model/response.model';
|
||||||
|
|
||||||
const baseTypeNames = ['String', 'Number', 'Boolean']
|
const baseTypeNames = ['String', 'Number', 'Boolean'];
|
||||||
|
|
||||||
function genBaseProp(type: Type<any>) {
|
function genBaseProp(type: Type<any>) {
|
||||||
if (baseTypeNames.includes(type.name))
|
if (baseTypeNames.includes(type.name)) return { type: type.name.toLocaleLowerCase() };
|
||||||
return { type: type.name.toLocaleLowerCase() }
|
else return { $ref: getSchemaPath(type) };
|
||||||
else
|
|
||||||
return { $ref: getSchemaPath(type) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -20,11 +18,11 @@ export function ApiResult<TModel extends Type<any>>({
|
||||||
isPage,
|
isPage,
|
||||||
status,
|
status,
|
||||||
}: {
|
}: {
|
||||||
type?: TModel | TModel[]
|
type?: TModel | TModel[];
|
||||||
isPage?: boolean
|
isPage?: boolean;
|
||||||
status?: HttpStatus
|
status?: HttpStatus;
|
||||||
}) {
|
}) {
|
||||||
let prop = null
|
let prop = null;
|
||||||
|
|
||||||
if (Array.isArray(type)) {
|
if (Array.isArray(type)) {
|
||||||
if (isPage) {
|
if (isPage) {
|
||||||
|
@ -46,23 +44,20 @@ export function ApiResult<TModel extends Type<any>>({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
prop = {
|
prop = {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
items: genBaseProp(type[0]),
|
items: genBaseProp(type[0]),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
} else if (type) {
|
||||||
}
|
prop = genBaseProp(type);
|
||||||
else if (type) {
|
} else {
|
||||||
prop = genBaseProp(type)
|
prop = { type: 'null', default: null };
|
||||||
}
|
|
||||||
else {
|
|
||||||
prop = { type: 'null', default: null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const model = Array.isArray(type) ? type[0] : type
|
const model = Array.isArray(type) ? type[0] : type;
|
||||||
|
|
||||||
return applyDecorators(
|
return applyDecorators(
|
||||||
ApiExtraModels(model),
|
ApiExtraModels(model),
|
||||||
|
@ -78,6 +73,6 @@ export function ApiResult<TModel extends Type<any>>({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { SetMetadata } from '@nestjs/common'
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
export const BYPASS_KEY = '__bypass_key__'
|
export const BYPASS_KEY = '__bypass_key__';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当不需要转换成基础返回格式时添加该装饰器
|
* 当不需要转换成基础返回格式时添加该装饰器
|
||||||
*/
|
*/
|
||||||
export function Bypass() {
|
export function Bypass() {
|
||||||
return SetMetadata(BYPASS_KEY, true)
|
return SetMetadata(BYPASS_KEY, true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import type { ExecutionContext } from '@nestjs/common'
|
import type { ExecutionContext } from '@nestjs/common';
|
||||||
import { createParamDecorator } from '@nestjs/common'
|
import { createParamDecorator } from '@nestjs/common';
|
||||||
import type { FastifyRequest } from 'fastify'
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
|
export const Cookies = createParamDecorator((data: string, ctx: ExecutionContext) => {
|
||||||
const request = ctx.switchToHttp().getRequest<FastifyRequest>()
|
const request = ctx.switchToHttp().getRequest<FastifyRequest>();
|
||||||
return data ? request.cookies?.[data] : request.cookies
|
return data ? request.cookies?.[data] : request.cookies;
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import cluster from 'node:cluster'
|
import cluster from 'node:cluster';
|
||||||
|
|
||||||
import { Cron } from '@nestjs/schedule'
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
|
||||||
import { isMainProcess } from '~/global/env'
|
import { isMainProcess } from '~/global/env';
|
||||||
|
|
||||||
export const CronOnce: typeof Cron = (...rest): MethodDecorator => {
|
export const CronOnce: typeof Cron = (...rest): MethodDecorator => {
|
||||||
// If not in cluster mode, and PM2 main worker
|
// If not in cluster mode, and PM2 main worker
|
||||||
if (isMainProcess)
|
if (isMainProcess)
|
||||||
// eslint-disable-next-line no-useless-call
|
// eslint-disable-next-line no-useless-call
|
||||||
return Cron.call(null, ...rest)
|
return Cron.call(null, ...rest);
|
||||||
|
|
||||||
if (cluster.isWorker && cluster.worker?.id === 1)
|
if (cluster.isWorker && cluster.worker?.id === 1)
|
||||||
// eslint-disable-next-line no-useless-call
|
// eslint-disable-next-line no-useless-call
|
||||||
return Cron.call(null, ...rest)
|
return Cron.call(null, ...rest);
|
||||||
|
|
||||||
const returnNothing: MethodDecorator = () => {}
|
const returnNothing: MethodDecorator = () => {};
|
||||||
return returnNothing
|
return returnNothing;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { applyDecorators } from '@nestjs/common'
|
import { applyDecorators } from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
IsBoolean,
|
IsBoolean,
|
||||||
IsDate,
|
IsDate,
|
||||||
|
@ -12,8 +12,8 @@ import {
|
||||||
MaxLength,
|
MaxLength,
|
||||||
Min,
|
Min,
|
||||||
MinLength,
|
MinLength,
|
||||||
} from 'class-validator'
|
} from 'class-validator';
|
||||||
import { isNumber } from 'lodash'
|
import { isNumber } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ToArray,
|
ToArray,
|
||||||
|
@ -23,115 +23,86 @@ import {
|
||||||
ToNumber,
|
ToNumber,
|
||||||
ToTrim,
|
ToTrim,
|
||||||
ToUpperCase,
|
ToUpperCase,
|
||||||
} from './transform.decorator'
|
} from './transform.decorator';
|
||||||
|
|
||||||
interface IOptionalOptions {
|
interface IOptionalOptions {
|
||||||
required?: boolean
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INumberFieldOptions extends IOptionalOptions {
|
interface INumberFieldOptions extends IOptionalOptions {
|
||||||
each?: boolean
|
each?: boolean;
|
||||||
int?: boolean
|
int?: boolean;
|
||||||
min?: number
|
min?: number;
|
||||||
max?: number
|
max?: number;
|
||||||
positive?: boolean
|
positive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IStringFieldOptions extends IOptionalOptions {
|
interface IStringFieldOptions extends IOptionalOptions {
|
||||||
each?: boolean
|
each?: boolean;
|
||||||
minLength?: number
|
minLength?: number;
|
||||||
maxLength?: number
|
maxLength?: number;
|
||||||
lowerCase?: boolean
|
lowerCase?: boolean;
|
||||||
upperCase?: boolean
|
upperCase?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NumberField(
|
export function NumberField(options: INumberFieldOptions = {}): PropertyDecorator {
|
||||||
options: INumberFieldOptions = {},
|
const { each, min, max, int, positive, required = true } = options;
|
||||||
): PropertyDecorator {
|
|
||||||
const { each, min, max, int, positive, required = true } = options
|
|
||||||
|
|
||||||
const decorators = [ToNumber()]
|
const decorators = [ToNumber()];
|
||||||
|
|
||||||
if (each)
|
if (each) decorators.push(ToArray());
|
||||||
decorators.push(ToArray())
|
|
||||||
|
|
||||||
if (int)
|
if (int) decorators.push(IsInt({ each }));
|
||||||
decorators.push(IsInt({ each }))
|
else decorators.push(IsNumber({}, { each }));
|
||||||
else
|
|
||||||
decorators.push(IsNumber({}, { each }))
|
|
||||||
|
|
||||||
if (isNumber(min))
|
if (isNumber(min)) decorators.push(Min(min, { each }));
|
||||||
decorators.push(Min(min, { each }))
|
|
||||||
|
|
||||||
if (isNumber(max))
|
if (isNumber(max)) decorators.push(Max(max, { each }));
|
||||||
decorators.push(Max(max, { each }))
|
|
||||||
|
|
||||||
if (positive)
|
if (positive) decorators.push(IsPositive({ each }));
|
||||||
decorators.push(IsPositive({ each }))
|
|
||||||
|
|
||||||
if (!required)
|
if (!required) decorators.push(IsOptional());
|
||||||
decorators.push(IsOptional())
|
|
||||||
|
|
||||||
return applyDecorators(...decorators)
|
return applyDecorators(...decorators);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StringField(
|
export function StringField(options: IStringFieldOptions = {}): PropertyDecorator {
|
||||||
options: IStringFieldOptions = {},
|
const { each, minLength, maxLength, lowerCase, upperCase, required = true } = options;
|
||||||
): PropertyDecorator {
|
|
||||||
const {
|
|
||||||
each,
|
|
||||||
minLength,
|
|
||||||
maxLength,
|
|
||||||
lowerCase,
|
|
||||||
upperCase,
|
|
||||||
required = true,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
const decorators = [IsString({ each }), ToTrim()]
|
const decorators = [IsString({ each }), ToTrim()];
|
||||||
|
|
||||||
if (each)
|
if (each) decorators.push(ToArray());
|
||||||
decorators.push(ToArray())
|
|
||||||
|
|
||||||
if (isNumber(minLength))
|
if (isNumber(minLength)) decorators.push(MinLength(minLength, { each }));
|
||||||
decorators.push(MinLength(minLength, { each }))
|
|
||||||
|
|
||||||
if (isNumber(maxLength))
|
if (isNumber(maxLength)) decorators.push(MaxLength(maxLength, { each }));
|
||||||
decorators.push(MaxLength(maxLength, { each }))
|
|
||||||
|
|
||||||
if (lowerCase)
|
if (lowerCase) decorators.push(ToLowerCase());
|
||||||
decorators.push(ToLowerCase())
|
|
||||||
|
|
||||||
if (upperCase)
|
if (upperCase) decorators.push(ToUpperCase());
|
||||||
decorators.push(ToUpperCase())
|
|
||||||
|
|
||||||
if (!required)
|
if (!required) decorators.push(IsOptional());
|
||||||
decorators.push(IsOptional())
|
else decorators.push(IsNotEmpty({ each }));
|
||||||
else
|
|
||||||
decorators.push(IsNotEmpty({ each }))
|
|
||||||
|
|
||||||
return applyDecorators(...decorators)
|
return applyDecorators(...decorators);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BooleanField(
|
export function BooleanField(options: IOptionalOptions = {}): PropertyDecorator {
|
||||||
options: IOptionalOptions = {},
|
const decorators = [ToBoolean(), IsBoolean()];
|
||||||
): PropertyDecorator {
|
|
||||||
const decorators = [ToBoolean(), IsBoolean()]
|
|
||||||
|
|
||||||
const { required = true } = options
|
const { required = true } = options;
|
||||||
|
|
||||||
if (!required)
|
if (!required) decorators.push(IsOptional());
|
||||||
decorators.push(IsOptional())
|
|
||||||
|
|
||||||
return applyDecorators(...decorators)
|
return applyDecorators(...decorators);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DateField(options: IOptionalOptions = {}): PropertyDecorator {
|
export function DateField(options: IOptionalOptions = {}): PropertyDecorator {
|
||||||
const decorators = [ToDate(), IsDate()]
|
const decorators = [ToDate(), IsDate()];
|
||||||
|
|
||||||
const { required = true } = options
|
const { required = true } = options;
|
||||||
|
|
||||||
if (!required)
|
if (!required) decorators.push(IsOptional());
|
||||||
decorators.push(IsOptional())
|
|
||||||
|
|
||||||
return applyDecorators(...decorators)
|
return applyDecorators(...decorators);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,22 @@
|
||||||
import type { ExecutionContext } from '@nestjs/common'
|
import type { ExecutionContext } from '@nestjs/common';
|
||||||
|
|
||||||
import { createParamDecorator } from '@nestjs/common'
|
import { createParamDecorator } from '@nestjs/common';
|
||||||
import type { FastifyRequest } from 'fastify'
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
import { getIp } from '~/utils/ip.util'
|
import { getIp } from '~/utils/ip.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 快速获取IP
|
* 快速获取IP
|
||||||
*/
|
*/
|
||||||
export const Ip = createParamDecorator((_, context: ExecutionContext) => {
|
export const Ip = createParamDecorator((_, context: ExecutionContext) => {
|
||||||
const request = context.switchToHttp().getRequest<FastifyRequest>()
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
return getIp(request)
|
return getIp(request);
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 快速获取request path,并不包括url params
|
* 快速获取request path,并不包括url params
|
||||||
*/
|
*/
|
||||||
export const Uri = createParamDecorator((_, context: ExecutionContext) => {
|
export const Uri = createParamDecorator((_, context: ExecutionContext) => {
|
||||||
const request = context.switchToHttp().getRequest<FastifyRequest>()
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
return request.routerPath
|
return request.routerPath;
|
||||||
})
|
});
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { HttpStatus, NotAcceptableException, Param, ParseIntPipe } from '@nestjs/common'
|
import { HttpStatus, NotAcceptableException, Param, ParseIntPipe } from '@nestjs/common';
|
||||||
|
|
||||||
export function IdParam() {
|
export function IdParam() {
|
||||||
return Param('id', new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE, exceptionFactory: (_error) => {
|
return Param(
|
||||||
throw new NotAcceptableException('id 格式不正确')
|
'id',
|
||||||
} }))
|
new ParseIntPipe({
|
||||||
|
errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE,
|
||||||
|
exceptionFactory: _error => {
|
||||||
|
throw new NotAcceptableException('id 格式不正确');
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { SetMetadata } from '@nestjs/common'
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
import { IdempotenceOption } from '../interceptors/idempotence.interceptor'
|
import { IdempotenceOption } from '../interceptors/idempotence.interceptor';
|
||||||
|
|
||||||
export const HTTP_IDEMPOTENCE_KEY = '__idempotence_key__'
|
export const HTTP_IDEMPOTENCE_KEY = '__idempotence_key__';
|
||||||
export const HTTP_IDEMPOTENCE_OPTIONS = '__idempotence_options__'
|
export const HTTP_IDEMPOTENCE_OPTIONS = '__idempotence_options__';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 幂等
|
* 幂等
|
||||||
*/
|
*/
|
||||||
export function Idempotence(options?: IdempotenceOption): MethodDecorator {
|
export function Idempotence(options?: IdempotenceOption): MethodDecorator {
|
||||||
return function (target, key, descriptor: PropertyDescriptor) {
|
return function (target, key, descriptor: PropertyDescriptor) {
|
||||||
SetMetadata(HTTP_IDEMPOTENCE_OPTIONS, options || {})(descriptor.value)
|
SetMetadata(HTTP_IDEMPOTENCE_OPTIONS, options || {})(descriptor.value);
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { applyDecorators } from '@nestjs/common'
|
import { applyDecorators } from '@nestjs/common';
|
||||||
import { ApiSecurity } from '@nestjs/swagger'
|
import { ApiSecurity } from '@nestjs/swagger';
|
||||||
|
|
||||||
export const API_SECURITY_AUTH = 'auth'
|
export const API_SECURITY_AUTH = 'auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* like to @ApiSecurity('auth')
|
* like to @ApiSecurity('auth')
|
||||||
*/
|
*/
|
||||||
export function ApiSecurityAuth(): ClassDecorator & MethodDecorator {
|
export function ApiSecurityAuth(): ClassDecorator & MethodDecorator {
|
||||||
return applyDecorators(ApiSecurity(API_SECURITY_AUTH))
|
return applyDecorators(ApiSecurity(API_SECURITY_AUTH));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
import { Transform } from 'class-transformer'
|
import { Transform } from 'class-transformer';
|
||||||
import { castArray, isArray, isNil, trim } from 'lodash'
|
import { castArray, isArray, isNil, trim } from 'lodash';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* convert string to number
|
* convert string to number
|
||||||
*/
|
*/
|
||||||
export function ToNumber(): PropertyDecorator {
|
export function ToNumber(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
const value = params.value as string[] | string
|
const value = params.value as string[] | string;
|
||||||
|
|
||||||
if (isArray(value))
|
if (isArray(value)) return value.map(v => Number(v));
|
||||||
return value.map(v => Number(v))
|
|
||||||
|
|
||||||
return Number(value)
|
return Number(value);
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,16 +22,15 @@ export function ToNumber(): PropertyDecorator {
|
||||||
*/
|
*/
|
||||||
export function ToInt(): PropertyDecorator {
|
export function ToInt(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
const value = params.value as string[] | string
|
const value = params.value as string[] | string;
|
||||||
|
|
||||||
if (isArray(value))
|
if (isArray(value)) return value.map(v => Number.parseInt(v));
|
||||||
return value.map(v => Number.parseInt(v))
|
|
||||||
|
|
||||||
return Number.parseInt(value)
|
return Number.parseInt(value);
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,18 +38,18 @@ export function ToInt(): PropertyDecorator {
|
||||||
*/
|
*/
|
||||||
export function ToBoolean(): PropertyDecorator {
|
export function ToBoolean(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
switch (params.value) {
|
switch (params.value) {
|
||||||
case 'true':
|
case 'true':
|
||||||
return true
|
return true;
|
||||||
case 'false':
|
case 'false':
|
||||||
return false
|
return false;
|
||||||
default:
|
default:
|
||||||
return params.value
|
return params.value;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,16 +57,15 @@ export function ToBoolean(): PropertyDecorator {
|
||||||
*/
|
*/
|
||||||
export function ToDate(): PropertyDecorator {
|
export function ToDate(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
const { value } = params
|
const { value } = params;
|
||||||
|
|
||||||
if (!value)
|
if (!value) return;
|
||||||
return
|
|
||||||
|
|
||||||
return new Date(value)
|
return new Date(value);
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,16 +73,15 @@ export function ToDate(): PropertyDecorator {
|
||||||
*/
|
*/
|
||||||
export function ToArray(): PropertyDecorator {
|
export function ToArray(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
const { value } = params
|
const { value } = params;
|
||||||
|
|
||||||
if (isNil(value))
|
if (isNil(value)) return [];
|
||||||
return []
|
|
||||||
|
|
||||||
return castArray(value)
|
return castArray(value);
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -93,16 +89,15 @@ export function ToArray(): PropertyDecorator {
|
||||||
*/
|
*/
|
||||||
export function ToTrim(): PropertyDecorator {
|
export function ToTrim(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
const value = params.value as string[] | string
|
const value = params.value as string[] | string;
|
||||||
|
|
||||||
if (isArray(value))
|
if (isArray(value)) return value.map(v => trim(v));
|
||||||
return value.map(v => trim(v))
|
|
||||||
|
|
||||||
return trim(value)
|
return trim(value);
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -110,19 +105,17 @@ export function ToTrim(): PropertyDecorator {
|
||||||
*/
|
*/
|
||||||
export function ToLowerCase(): PropertyDecorator {
|
export function ToLowerCase(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
const value = params.value as string[] | string
|
const value = params.value as string[] | string;
|
||||||
|
|
||||||
if (!value)
|
if (!value) return;
|
||||||
return
|
|
||||||
|
|
||||||
if (isArray(value))
|
if (isArray(value)) return value.map(v => v.toLowerCase());
|
||||||
return value.map(v => v.toLowerCase())
|
|
||||||
|
|
||||||
return value.toLowerCase()
|
return value.toLowerCase();
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,17 +123,15 @@ export function ToLowerCase(): PropertyDecorator {
|
||||||
*/
|
*/
|
||||||
export function ToUpperCase(): PropertyDecorator {
|
export function ToUpperCase(): PropertyDecorator {
|
||||||
return Transform(
|
return Transform(
|
||||||
(params) => {
|
params => {
|
||||||
const value = params.value as string[] | string
|
const value = params.value as string[] | string;
|
||||||
|
|
||||||
if (!value)
|
if (!value) return;
|
||||||
return
|
|
||||||
|
|
||||||
if (isArray(value))
|
if (isArray(value)) return value.map(v => v.toUpperCase());
|
||||||
return value.map(v => v.toUpperCase())
|
|
||||||
|
|
||||||
return value.toUpperCase()
|
return value.toUpperCase();
|
||||||
},
|
},
|
||||||
{ toClassOnly: true },
|
{ toClassOnly: true }
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Expose, Transform } from 'class-transformer'
|
import { Expose, Transform } from 'class-transformer';
|
||||||
import { IsInt, IsOptional, Max, Min } from 'class-validator'
|
import { IsInt, IsOptional, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
export class CursorDto<T = any> {
|
export class CursorDto<T = any> {
|
||||||
@ApiProperty({ minimum: 0, default: 0 })
|
@ApiProperty({ minimum: 0, default: 0 })
|
||||||
|
@ -11,7 +11,7 @@ export class CursorDto<T = any> {
|
||||||
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 0), {
|
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 0), {
|
||||||
toClassOnly: true,
|
toClassOnly: true,
|
||||||
})
|
})
|
||||||
cursor?: number
|
cursor?: number;
|
||||||
|
|
||||||
@ApiProperty({ minimum: 1, maximum: 100, default: 10 })
|
@ApiProperty({ minimum: 1, maximum: 100, default: 10 })
|
||||||
@Min(1)
|
@Min(1)
|
||||||
|
@ -22,5 +22,5 @@ export class CursorDto<T = any> {
|
||||||
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), {
|
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), {
|
||||||
toClassOnly: true,
|
toClassOnly: true,
|
||||||
})
|
})
|
||||||
limit?: number
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { IsDefined, IsNotEmpty, IsNumber } from 'class-validator'
|
import { IsDefined, IsNotEmpty, IsNumber } from 'class-validator';
|
||||||
|
|
||||||
export class BatchDeleteDto {
|
export class BatchDeleteDto {
|
||||||
@IsDefined()
|
@IsDefined()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsNumber({}, { each: true })
|
@IsNumber({}, { each: true })
|
||||||
ids: number[]
|
ids: number[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { IsNumber } from 'class-validator'
|
import { IsNumber } from 'class-validator';
|
||||||
|
|
||||||
export class IdDto {
|
export class IdDto {
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
id: number
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Expose, Transform } from 'class-transformer'
|
import { Expose, Transform } from 'class-transformer';
|
||||||
import { Allow, IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'
|
import { Allow, IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator';
|
||||||
|
|
||||||
export enum Order {
|
export enum Order {
|
||||||
ASC = 'ASC',
|
ASC = 'ASC',
|
||||||
|
@ -16,7 +16,7 @@ export class PagerDto<T = any> {
|
||||||
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 1), {
|
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 1), {
|
||||||
toClassOnly: true,
|
toClassOnly: true,
|
||||||
})
|
})
|
||||||
page?: number
|
page?: number;
|
||||||
|
|
||||||
@ApiProperty({ minimum: 1, maximum: 100, default: 10 })
|
@ApiProperty({ minimum: 1, maximum: 100, default: 10 })
|
||||||
@Min(1)
|
@Min(1)
|
||||||
|
@ -27,19 +27,19 @@ export class PagerDto<T = any> {
|
||||||
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), {
|
@Transform(({ value: val }) => (val ? Number.parseInt(val) : 10), {
|
||||||
toClassOnly: true,
|
toClassOnly: true,
|
||||||
})
|
})
|
||||||
pageSize?: number
|
pageSize?: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
field?: string // | keyof T
|
field?: string; // | keyof T
|
||||||
|
|
||||||
@ApiProperty({ enum: Order })
|
@ApiProperty({ enum: Order })
|
||||||
@IsEnum(Order)
|
@IsEnum(Order)
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Transform(({ value }) => (value === 'asc' ? Order.ASC : Order.DESC))
|
@Transform(({ value }) => (value === 'asc' ? Order.ASC : Order.DESC))
|
||||||
order?: Order
|
order?: Order;
|
||||||
|
|
||||||
@Allow()
|
@Allow()
|
||||||
_t?: number
|
_t?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger'
|
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
|
||||||
import { Exclude } from 'class-transformer'
|
import { Exclude } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
BaseEntity,
|
BaseEntity,
|
||||||
Column,
|
Column,
|
||||||
|
@ -7,7 +7,7 @@ import {
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
VirtualColumn,
|
VirtualColumn,
|
||||||
} from 'typeorm'
|
} from 'typeorm';
|
||||||
|
|
||||||
// 如果觉得前端转换时间太麻烦,并且不考虑通用性的话,可以在服务端进行转换,eg: @UpdateDateColumn({ name: 'updated_at', transformer })
|
// 如果觉得前端转换时间太麻烦,并且不考虑通用性的话,可以在服务端进行转换,eg: @UpdateDateColumn({ name: 'updated_at', transformer })
|
||||||
// const transformer: ValueTransformer = {
|
// const transformer: ValueTransformer = {
|
||||||
|
@ -21,25 +21,25 @@ import {
|
||||||
|
|
||||||
export abstract class CommonEntity extends BaseEntity {
|
export abstract class CommonEntity extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id: number
|
id: number;
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at' })
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
createdAt: Date
|
createdAt: Date;
|
||||||
|
|
||||||
@UpdateDateColumn({ name: 'updated_at' })
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
updatedAt: Date
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class CompleteEntity extends CommonEntity {
|
export abstract class CompleteEntity extends CommonEntity {
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
@Exclude()
|
@Exclude()
|
||||||
@Column({ name: 'create_by', update: false, comment: '创建者' })
|
@Column({ name: 'create_by', update: false, comment: '创建者' })
|
||||||
createBy: number
|
createBy: number;
|
||||||
|
|
||||||
@ApiHideProperty()
|
@ApiHideProperty()
|
||||||
@Exclude()
|
@Exclude()
|
||||||
@Column({ name: 'update_by', comment: '更新者' })
|
@Column({ name: 'update_by', comment: '更新者' })
|
||||||
updateBy: number
|
updateBy: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 不会保存到数据库中的虚拟列,数据量大时可能会有性能问题,有性能要求请考虑在 service 层手动实现
|
* 不会保存到数据库中的虚拟列,数据量大时可能会有性能问题,有性能要求请考虑在 service 层手动实现
|
||||||
|
@ -47,9 +47,9 @@ export abstract class CompleteEntity extends CommonEntity {
|
||||||
*/
|
*/
|
||||||
@ApiProperty({ description: '创建者' })
|
@ApiProperty({ description: '创建者' })
|
||||||
@VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.create_by` })
|
@VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.create_by` })
|
||||||
creator: string
|
creator: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '更新者' })
|
@ApiProperty({ description: '更新者' })
|
||||||
@VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.update_by` })
|
@VirtualColumn({ query: alias => `SELECT username FROM sys_user WHERE id = ${alias}.update_by` })
|
||||||
updater: string
|
updater: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { HttpException, HttpStatus } from '@nestjs/common'
|
import { HttpException, HttpStatus } from '@nestjs/common';
|
||||||
|
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
import { RESPONSE_SUCCESS_CODE } from '~/constants/response.constant'
|
import { RESPONSE_SUCCESS_CODE } from '~/constants/response.constant';
|
||||||
|
|
||||||
export class BusinessException extends HttpException {
|
export class BusinessException extends HttpException {
|
||||||
private errorCode: number
|
private errorCode: number;
|
||||||
|
|
||||||
constructor(error: ErrorEnum | string) {
|
constructor(error: ErrorEnum | string) {
|
||||||
// 如果是非 ErrorEnum
|
// 如果是非 ErrorEnum
|
||||||
|
@ -14,27 +14,27 @@ export class BusinessException extends HttpException {
|
||||||
code: RESPONSE_SUCCESS_CODE,
|
code: RESPONSE_SUCCESS_CODE,
|
||||||
message: error,
|
message: error,
|
||||||
}),
|
}),
|
||||||
HttpStatus.OK,
|
HttpStatus.OK
|
||||||
)
|
);
|
||||||
this.errorCode = RESPONSE_SUCCESS_CODE
|
this.errorCode = RESPONSE_SUCCESS_CODE;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [code, message] = error.split(':')
|
const [code, message] = error.split(':');
|
||||||
super(
|
super(
|
||||||
HttpException.createBody({
|
HttpException.createBody({
|
||||||
code,
|
code,
|
||||||
message,
|
message,
|
||||||
}),
|
}),
|
||||||
HttpStatus.OK,
|
HttpStatus.OK
|
||||||
)
|
);
|
||||||
|
|
||||||
this.errorCode = Number(code)
|
this.errorCode = Number(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
getErrorCode(): number {
|
getErrorCode(): number {
|
||||||
return this.errorCode
|
return this.errorCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { BusinessException as BizException }
|
export { BusinessException as BizException };
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { NotFoundException } from '@nestjs/common'
|
import { NotFoundException } from '@nestjs/common';
|
||||||
import { sample } from 'lodash'
|
import { sample } from 'lodash';
|
||||||
|
|
||||||
export const NotFoundMessage = ['404, Not Found']
|
export const NotFoundMessage = ['404, Not Found'];
|
||||||
|
|
||||||
export class CannotFindException extends NotFoundException {
|
export class CannotFindException extends NotFoundException {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(sample(NotFoundMessage))
|
super(sample(NotFoundMessage));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,38 @@
|
||||||
import { HttpException } from '@nestjs/common'
|
import { HttpException } from '@nestjs/common';
|
||||||
import { WsException } from '@nestjs/websockets'
|
import { WsException } from '@nestjs/websockets';
|
||||||
|
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
|
|
||||||
export class SocketException extends WsException {
|
export class SocketException extends WsException {
|
||||||
private errorCode: number
|
private errorCode: number;
|
||||||
|
|
||||||
constructor(message: string)
|
constructor(message: string);
|
||||||
constructor(error: ErrorEnum)
|
constructor(error: ErrorEnum);
|
||||||
constructor(...args: any) {
|
constructor(...args: any) {
|
||||||
const error = args[0]
|
const error = args[0];
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
super(
|
super(
|
||||||
HttpException.createBody({
|
HttpException.createBody({
|
||||||
code: 0,
|
code: 0,
|
||||||
message: error,
|
message: error,
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
this.errorCode = 0
|
this.errorCode = 0;
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [code, message] = error.split(':')
|
const [code, message] = error.split(':');
|
||||||
super(
|
super(
|
||||||
HttpException.createBody({
|
HttpException.createBody({
|
||||||
code,
|
code,
|
||||||
message,
|
message,
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
this.errorCode = Number(code)
|
this.errorCode = Number(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
getErrorCode(): number {
|
getErrorCode(): number {
|
||||||
return this.errorCode
|
return this.errorCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,85 +5,76 @@ import {
|
||||||
HttpException,
|
HttpException,
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Logger,
|
Logger,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify'
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
import { BusinessException } from '~/common/exceptions/biz.exception';
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
|
|
||||||
import { isDev } from '~/global/env'
|
import { isDev } from '~/global/env';
|
||||||
|
|
||||||
interface myError {
|
interface myError {
|
||||||
readonly status: number
|
readonly status: number;
|
||||||
readonly statusCode?: number
|
readonly statusCode?: number;
|
||||||
|
|
||||||
readonly message?: string
|
readonly message?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Catch()
|
@Catch()
|
||||||
export class AllExceptionsFilter implements ExceptionFilter {
|
export class AllExceptionsFilter implements ExceptionFilter {
|
||||||
private readonly logger = new Logger(AllExceptionsFilter.name)
|
private readonly logger = new Logger(AllExceptionsFilter.name);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.registerCatchAllExceptionsHook()
|
this.registerCatchAllExceptionsHook();
|
||||||
}
|
}
|
||||||
|
|
||||||
catch(exception: unknown, host: ArgumentsHost) {
|
catch(exception: unknown, host: ArgumentsHost) {
|
||||||
const ctx = host.switchToHttp()
|
const ctx = host.switchToHttp();
|
||||||
const request = ctx.getRequest<FastifyRequest>()
|
const request = ctx.getRequest<FastifyRequest>();
|
||||||
const response = ctx.getResponse<FastifyReply>()
|
const response = ctx.getResponse<FastifyReply>();
|
||||||
|
|
||||||
const url = request.raw.url!
|
const url = request.raw.url!;
|
||||||
|
|
||||||
const status
|
const status =
|
||||||
= exception instanceof HttpException
|
exception instanceof HttpException
|
||||||
? exception.getStatus()
|
? exception.getStatus()
|
||||||
: (exception as myError)?.status
|
: (exception as myError)?.status ||
|
||||||
|| (exception as myError)?.statusCode
|
(exception as myError)?.statusCode ||
|
||||||
|| HttpStatus.INTERNAL_SERVER_ERROR
|
HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
let message
|
let message =
|
||||||
= (exception as any)?.response?.message
|
(exception as any)?.response?.message || (exception as myError)?.message || `${exception}`;
|
||||||
|| (exception as myError)?.message
|
|
||||||
|| `${exception}`
|
|
||||||
|
|
||||||
// 系统内部错误时
|
// 系统内部错误时
|
||||||
if (
|
if (status === HttpStatus.INTERNAL_SERVER_ERROR && !(exception instanceof BusinessException)) {
|
||||||
status === HttpStatus.INTERNAL_SERVER_ERROR
|
Logger.error(exception, undefined, 'Catch');
|
||||||
&& !(exception instanceof BusinessException)
|
|
||||||
) {
|
|
||||||
Logger.error(exception, undefined, 'Catch')
|
|
||||||
|
|
||||||
// 生产环境下隐藏错误信息
|
// 生产环境下隐藏错误信息
|
||||||
if (!isDev)
|
if (!isDev) message = ErrorEnum.SERVER_ERROR?.split(':')[1];
|
||||||
message = ErrorEnum.SERVER_ERROR?.split(':')[1]
|
} else {
|
||||||
}
|
this.logger.warn(`错误信息:(${status}) ${message} Path: ${decodeURI(url)}`);
|
||||||
else {
|
|
||||||
this.logger.warn(
|
|
||||||
`错误信息:(${status}) ${message} Path: ${decodeURI(url)}`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiErrorCode: number
|
const apiErrorCode: number =
|
||||||
= exception instanceof BusinessException ? exception.getErrorCode() : status
|
exception instanceof BusinessException ? exception.getErrorCode() : status;
|
||||||
|
|
||||||
// 返回基础响应结果
|
// 返回基础响应结果
|
||||||
const resBody: IBaseResponse = {
|
const resBody: IBaseResponse = {
|
||||||
code: apiErrorCode,
|
code: apiErrorCode,
|
||||||
message,
|
message,
|
||||||
data: null,
|
data: null,
|
||||||
}
|
};
|
||||||
|
|
||||||
response.status(status).send(resBody)
|
response.status(status).send(resBody);
|
||||||
}
|
}
|
||||||
|
|
||||||
registerCatchAllExceptionsHook() {
|
registerCatchAllExceptionsHook() {
|
||||||
process.on('unhandledRejection', (reason) => {
|
process.on('unhandledRejection', reason => {
|
||||||
console.error('unhandledRejection: ', reason)
|
console.error('unhandledRejection: ', reason);
|
||||||
})
|
});
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
process.on('uncaughtException', err => {
|
||||||
console.error('uncaughtException: ', err)
|
console.error('uncaughtException: ', err);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +1,69 @@
|
||||||
import type {
|
import type { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
||||||
CallHandler,
|
|
||||||
ExecutionContext,
|
import { ConflictException, Injectable, SetMetadata } from '@nestjs/common';
|
||||||
NestInterceptor,
|
import { Reflector } from '@nestjs/core';
|
||||||
} from '@nestjs/common'
|
import type { FastifyRequest } from 'fastify';
|
||||||
|
import { catchError, tap } from 'rxjs';
|
||||||
|
|
||||||
|
import { CacheService } from '~/shared/redis/cache.service';
|
||||||
|
import { hashString } from '~/utils';
|
||||||
|
import { getIp } from '~/utils/ip.util';
|
||||||
|
import { getRedisKey } from '~/utils/redis.util';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ConflictException,
|
HTTP_IDEMPOTENCE_KEY,
|
||||||
Injectable,
|
HTTP_IDEMPOTENCE_OPTIONS,
|
||||||
SetMetadata,
|
} from '../decorators/idempotence.decorator';
|
||||||
} from '@nestjs/common'
|
|
||||||
import { Reflector } from '@nestjs/core'
|
|
||||||
import type { FastifyRequest } from 'fastify'
|
|
||||||
import { catchError, tap } from 'rxjs'
|
|
||||||
|
|
||||||
import { CacheService } from '~/shared/redis/cache.service'
|
const IdempotenceHeaderKey = 'x-idempotence';
|
||||||
import { hashString } from '~/utils'
|
|
||||||
import { getIp } from '~/utils/ip.util'
|
|
||||||
import { getRedisKey } from '~/utils/redis.util'
|
|
||||||
|
|
||||||
import { HTTP_IDEMPOTENCE_KEY, HTTP_IDEMPOTENCE_OPTIONS } from '../decorators/idempotence.decorator'
|
|
||||||
|
|
||||||
const IdempotenceHeaderKey = 'x-idempotence'
|
|
||||||
|
|
||||||
export interface IdempotenceOption {
|
export interface IdempotenceOption {
|
||||||
errorMessage?: string
|
errorMessage?: string;
|
||||||
pendingMessage?: string
|
pendingMessage?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 如果重复请求的话,手动处理异常
|
* 如果重复请求的话,手动处理异常
|
||||||
*/
|
*/
|
||||||
handler?: (req: FastifyRequest) => any
|
handler?: (req: FastifyRequest) => any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录重复请求的时间
|
* 记录重复请求的时间
|
||||||
* @default 60
|
* @default 60
|
||||||
*/
|
*/
|
||||||
expired?: number
|
expired?: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 如果 header 没有幂等 key,根据 request 生成 key,如何生成这个 key 的方法
|
* 如果 header 没有幂等 key,根据 request 生成 key,如何生成这个 key 的方法
|
||||||
*/
|
*/
|
||||||
generateKey?: (req: FastifyRequest) => string
|
generateKey?: (req: FastifyRequest) => string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 仅读取 header 的 key,不自动生成
|
* 仅读取 header 的 key,不自动生成
|
||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
disableGenerateKey?: boolean
|
disableGenerateKey?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class IdempotenceInterceptor implements NestInterceptor {
|
export class IdempotenceInterceptor implements NestInterceptor {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly reflector: Reflector,
|
private readonly reflector: Reflector,
|
||||||
private readonly cacheService: CacheService,
|
private readonly cacheService: CacheService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async intercept(context: ExecutionContext, next: CallHandler) {
|
async intercept(context: ExecutionContext, next: CallHandler) {
|
||||||
const request = context.switchToHttp().getRequest<FastifyRequest>()
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
|
|
||||||
// skip Get 请求
|
// skip Get 请求
|
||||||
if (request.method.toUpperCase() === 'GET')
|
if (request.method.toUpperCase() === 'GET') return next.handle();
|
||||||
return next.handle()
|
|
||||||
|
|
||||||
const handler = context.getHandler()
|
const handler = context.getHandler();
|
||||||
const options: IdempotenceOption | undefined = this.reflector.get(
|
const options: IdempotenceOption | undefined = this.reflector.get(
|
||||||
HTTP_IDEMPOTENCE_OPTIONS,
|
HTTP_IDEMPOTENCE_OPTIONS,
|
||||||
handler,
|
handler
|
||||||
)
|
);
|
||||||
|
|
||||||
if (!options)
|
if (!options) return next.handle();
|
||||||
return next.handle()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
errorMessage = '相同请求成功后在 60 秒内只能发送一次',
|
errorMessage = '相同请求成功后在 60 秒内只能发送一次',
|
||||||
|
@ -78,71 +71,64 @@ export class IdempotenceInterceptor implements NestInterceptor {
|
||||||
handler: errorHandler,
|
handler: errorHandler,
|
||||||
expired = 60,
|
expired = 60,
|
||||||
disableGenerateKey = false,
|
disableGenerateKey = false,
|
||||||
} = options
|
} = options;
|
||||||
const redis = this.cacheService.getClient()
|
const redis = this.cacheService.getClient();
|
||||||
|
|
||||||
const idempotence = request.headers[IdempotenceHeaderKey] as string
|
const idempotence = request.headers[IdempotenceHeaderKey] as string;
|
||||||
const key = disableGenerateKey
|
const key = disableGenerateKey
|
||||||
? undefined
|
? undefined
|
||||||
: options.generateKey
|
: options.generateKey
|
||||||
? options.generateKey(request)
|
? options.generateKey(request)
|
||||||
: this.generateKey(request)
|
: this.generateKey(request);
|
||||||
|
|
||||||
const idempotenceKey
|
const idempotenceKey =
|
||||||
= !!(idempotence || key) && getRedisKey(`idempotence:${idempotence || key}`)
|
!!(idempotence || key) && getRedisKey(`idempotence:${idempotence || key}`);
|
||||||
|
|
||||||
SetMetadata(HTTP_IDEMPOTENCE_KEY, idempotenceKey)(handler)
|
SetMetadata(HTTP_IDEMPOTENCE_KEY, idempotenceKey)(handler);
|
||||||
|
|
||||||
if (idempotenceKey) {
|
if (idempotenceKey) {
|
||||||
const resultValue: '0' | '1' | null = (await redis.get(
|
const resultValue: '0' | '1' | null = (await redis.get(idempotenceKey)) as any;
|
||||||
idempotenceKey,
|
|
||||||
)) as any
|
|
||||||
if (resultValue !== null) {
|
if (resultValue !== null) {
|
||||||
if (errorHandler)
|
if (errorHandler) return await errorHandler(request);
|
||||||
return await errorHandler(request)
|
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
1: errorMessage,
|
1: errorMessage,
|
||||||
0: pendingMessage,
|
0: pendingMessage,
|
||||||
}[resultValue]
|
}[resultValue];
|
||||||
throw new ConflictException(message)
|
throw new ConflictException(message);
|
||||||
}
|
} else {
|
||||||
else {
|
await redis.set(idempotenceKey, '0', 'EX', expired);
|
||||||
await redis.set(idempotenceKey, '0', 'EX', expired)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
tap(async () => {
|
tap(async () => {
|
||||||
idempotenceKey && (await redis.set(idempotenceKey, '1', 'KEEPTTL'))
|
idempotenceKey && (await redis.set(idempotenceKey, '1', 'KEEPTTL'));
|
||||||
}),
|
}),
|
||||||
catchError(async (err) => {
|
catchError(async err => {
|
||||||
if (idempotenceKey)
|
if (idempotenceKey) await redis.del(idempotenceKey);
|
||||||
await redis.del(idempotenceKey)
|
|
||||||
|
|
||||||
throw err
|
throw err;
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateKey(req: FastifyRequest) {
|
private generateKey(req: FastifyRequest) {
|
||||||
const { body, params, query = {}, headers, url } = req
|
const { body, params, query = {}, headers, url } = req;
|
||||||
|
|
||||||
const obj = { body, url, params, query } as any
|
const obj = { body, url, params, query } as any;
|
||||||
|
|
||||||
const uuid = headers['x-uuid']
|
const uuid = headers['x-uuid'];
|
||||||
if (uuid) {
|
if (uuid) {
|
||||||
obj.uuid = uuid
|
obj.uuid = uuid;
|
||||||
}
|
} else {
|
||||||
else {
|
const ua = headers['user-agent'];
|
||||||
const ua = headers['user-agent']
|
const ip = getIp(req);
|
||||||
const ip = getIp(req)
|
|
||||||
|
|
||||||
if (!ua && !ip)
|
if (!ua && !ip) return undefined;
|
||||||
return undefined
|
|
||||||
|
|
||||||
Object.assign(obj, { ua, ip })
|
Object.assign(obj, { ua, ip });
|
||||||
}
|
}
|
||||||
|
|
||||||
return hashString(JSON.stringify(obj))
|
return hashString(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,24 @@
|
||||||
import {
|
import { CallHandler, ExecutionContext, Injectable, Logger, NestInterceptor } from '@nestjs/common';
|
||||||
CallHandler,
|
import { Observable, tap } from 'rxjs';
|
||||||
ExecutionContext,
|
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
NestInterceptor,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { Observable, tap } from 'rxjs'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoggingInterceptor implements NestInterceptor {
|
export class LoggingInterceptor implements NestInterceptor {
|
||||||
private logger = new Logger(LoggingInterceptor.name, { timestamp: false })
|
private logger = new Logger(LoggingInterceptor.name, { timestamp: false });
|
||||||
|
|
||||||
intercept(
|
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
|
||||||
context: ExecutionContext,
|
const call$ = next.handle();
|
||||||
next: CallHandler<any>,
|
const request = context.switchToHttp().getRequest();
|
||||||
): Observable<any> {
|
const content = `${request.method} -> ${request.url}`;
|
||||||
const call$ = next.handle()
|
const isSse = request.headers.accept === 'text/event-stream';
|
||||||
const request = context.switchToHttp().getRequest()
|
this.logger.debug(`+++ 请求:${content}`);
|
||||||
const content = `${request.method} -> ${request.url}`
|
const now = Date.now();
|
||||||
const isSse = request.headers.accept === 'text/event-stream'
|
|
||||||
this.logger.debug(`+++ 请求:${content}`)
|
|
||||||
const now = Date.now()
|
|
||||||
|
|
||||||
return call$.pipe(
|
return call$.pipe(
|
||||||
tap(() => {
|
tap(() => {
|
||||||
if (isSse)
|
if (isSse) return;
|
||||||
return
|
|
||||||
|
|
||||||
this.logger.debug(`--- 响应:${content}${` +${Date.now() - now}ms`}`)
|
this.logger.debug(`--- 响应:${content}${` +${Date.now() - now}ms`}`);
|
||||||
},
|
})
|
||||||
),
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,9 @@ import {
|
||||||
Injectable,
|
Injectable,
|
||||||
NestInterceptor,
|
NestInterceptor,
|
||||||
RequestTimeoutException,
|
RequestTimeoutException,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common';
|
||||||
import { Observable, TimeoutError, throwError } from 'rxjs'
|
import { Observable, TimeoutError, throwError } from 'rxjs';
|
||||||
import { catchError, timeout } from 'rxjs/operators'
|
import { catchError, timeout } from 'rxjs/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimeoutInterceptor implements NestInterceptor {
|
export class TimeoutInterceptor implements NestInterceptor {
|
||||||
|
@ -15,12 +15,11 @@ export class TimeoutInterceptor implements NestInterceptor {
|
||||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
timeout(this.time),
|
timeout(this.time),
|
||||||
catchError((err) => {
|
catchError(err => {
|
||||||
if (err instanceof TimeoutError)
|
if (err instanceof TimeoutError) return throwError(new RequestTimeoutException('请求超时'));
|
||||||
return throwError(new RequestTimeoutException('请求超时'))
|
|
||||||
|
|
||||||
return throwError(err)
|
return throwError(err);
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,14 +4,14 @@ import {
|
||||||
HttpStatus,
|
HttpStatus,
|
||||||
Injectable,
|
Injectable,
|
||||||
NestInterceptor,
|
NestInterceptor,
|
||||||
} from '@nestjs/common'
|
} from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core'
|
import { Reflector } from '@nestjs/core';
|
||||||
import { Observable } from 'rxjs'
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators'
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { ResOp } from '~/common/model/response.model'
|
import { ResOp } from '~/common/model/response.model';
|
||||||
|
|
||||||
import { BYPASS_KEY } from '../decorators/bypass.decorator'
|
import { BYPASS_KEY } from '../decorators/bypass.decorator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一处理返回接口结果,如果不需要则添加 @Bypass 装饰器
|
* 统一处理返回接口结果,如果不需要则添加 @Bypass 装饰器
|
||||||
|
@ -20,27 +20,20 @@ import { BYPASS_KEY } from '../decorators/bypass.decorator'
|
||||||
export class TransformInterceptor implements NestInterceptor {
|
export class TransformInterceptor implements NestInterceptor {
|
||||||
constructor(private readonly reflector: Reflector) {}
|
constructor(private readonly reflector: Reflector) {}
|
||||||
|
|
||||||
intercept(
|
intercept(context: ExecutionContext, next: CallHandler<any>): Observable<any> {
|
||||||
context: ExecutionContext,
|
const bypass = this.reflector.get<boolean>(BYPASS_KEY, context.getHandler());
|
||||||
next: CallHandler<any>,
|
|
||||||
): Observable<any> {
|
|
||||||
const bypass = this.reflector.get<boolean>(
|
|
||||||
BYPASS_KEY,
|
|
||||||
context.getHandler(),
|
|
||||||
)
|
|
||||||
|
|
||||||
if (bypass)
|
if (bypass) return next.handle();
|
||||||
return next.handle()
|
|
||||||
|
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
map((data) => {
|
map(data => {
|
||||||
// if (typeof data === 'undefined') {
|
// if (typeof data === 'undefined') {
|
||||||
// context.switchToHttp().getResponse().status(HttpStatus.NO_CONTENT);
|
// context.switchToHttp().getResponse().status(HttpStatus.NO_CONTENT);
|
||||||
// return data;
|
// return data;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return new ResOp(HttpStatus.OK, data ?? null)
|
return new ResOp(HttpStatus.OK, data ?? null);
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +1,39 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
import {
|
import { RESPONSE_SUCCESS_CODE, RESPONSE_SUCCESS_MSG } from '~/constants/response.constant';
|
||||||
RESPONSE_SUCCESS_CODE,
|
|
||||||
RESPONSE_SUCCESS_MSG,
|
|
||||||
} from '~/constants/response.constant'
|
|
||||||
|
|
||||||
export class ResOp<T = any> {
|
export class ResOp<T = any> {
|
||||||
@ApiProperty({ type: 'object' })
|
@ApiProperty({ type: 'object' })
|
||||||
data?: T
|
data?: T;
|
||||||
|
|
||||||
@ApiProperty({ type: 'number', default: RESPONSE_SUCCESS_CODE })
|
@ApiProperty({ type: 'number', default: RESPONSE_SUCCESS_CODE })
|
||||||
code: number
|
code: number;
|
||||||
|
|
||||||
@ApiProperty({ type: 'string', default: RESPONSE_SUCCESS_MSG })
|
@ApiProperty({ type: 'string', default: RESPONSE_SUCCESS_MSG })
|
||||||
message: string
|
message: string;
|
||||||
|
|
||||||
constructor(code: number, data: T, message = RESPONSE_SUCCESS_MSG) {
|
constructor(code: number, data: T, message = RESPONSE_SUCCESS_MSG) {
|
||||||
this.code = code
|
this.code = code;
|
||||||
this.data = data
|
this.data = data;
|
||||||
this.message = message
|
this.message = message;
|
||||||
}
|
}
|
||||||
|
|
||||||
static success<T>(data?: T, message?: string) {
|
static success<T>(data?: T, message?: string) {
|
||||||
return new ResOp(RESPONSE_SUCCESS_CODE, data, message)
|
return new ResOp(RESPONSE_SUCCESS_CODE, data, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static error(code: number, message) {
|
static error(code: number, message) {
|
||||||
return new ResOp(code, {}, message)
|
return new ResOp(code, {}, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TreeResult<T> {
|
export class TreeResult<T> {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
id: number
|
id: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
parentId: number
|
parentId: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
children?: TreeResult<T>[]
|
children?: TreeResult<T>[];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
import {
|
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
|
||||||
ArgumentMetadata,
|
|
||||||
BadRequestException,
|
|
||||||
Injectable,
|
|
||||||
PipeTransform,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ParseIntPipe implements PipeTransform<string, number> {
|
export class ParseIntPipe implements PipeTransform<string, number> {
|
||||||
transform(value: string, metadata: ArgumentMetadata): number {
|
transform(value: string, metadata: ArgumentMetadata): number {
|
||||||
const val = Number.parseInt(value, 10)
|
const val = Number.parseInt(value, 10);
|
||||||
|
|
||||||
if (Number.isNaN(val))
|
if (Number.isNaN(val)) throw new BadRequestException('id validation failed');
|
||||||
throw new BadRequestException('id validation failed')
|
|
||||||
|
|
||||||
return val
|
return val;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { ConfigType, registerAs } from '@nestjs/config'
|
import { ConfigType, registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
import { env, envNumber } from '~/global/env'
|
import { env, envNumber } from '~/global/env';
|
||||||
|
|
||||||
export const appRegToken = 'app'
|
export const appRegToken = 'app';
|
||||||
|
|
||||||
export const AppConfig = registerAs(appRegToken, () => ({
|
export const AppConfig = registerAs(appRegToken, () => ({
|
||||||
name: env('APP_NAME'),
|
name: env('APP_NAME'),
|
||||||
|
@ -15,6 +15,6 @@ export const AppConfig = registerAs(appRegToken, () => ({
|
||||||
level: env('LOGGER_LEVEL'),
|
level: env('LOGGER_LEVEL'),
|
||||||
maxFiles: envNumber('LOGGER_MAX_FILES'),
|
maxFiles: envNumber('LOGGER_MAX_FILES'),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
export type IAppConfig = ConfigType<typeof AppConfig>
|
export type IAppConfig = ConfigType<typeof AppConfig>;
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import { ConfigType, registerAs } from '@nestjs/config'
|
import { ConfigType, registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
import { DataSource, DataSourceOptions } from 'typeorm'
|
import { DataSource, DataSourceOptions } from 'typeorm';
|
||||||
|
|
||||||
import { env, envBoolean, envNumber } from '~/global/env'
|
import { env, envBoolean, envNumber } from '~/global/env';
|
||||||
|
|
||||||
// eslint-disable-next-line import/order
|
// eslint-disable-next-line import/order
|
||||||
import dotenv from 'dotenv'
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
dotenv.config({ path: `.env.${process.env.NODE_ENV}` })
|
dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
|
||||||
|
|
||||||
// 当前通过 npm scripts 执行的命令
|
// 当前通过 npm scripts 执行的命令
|
||||||
const currentScript = process.env.npm_lifecycle_event
|
const currentScript = process.env.npm_lifecycle_event;
|
||||||
|
|
||||||
const dataSourceOptions: DataSourceOptions = {
|
const dataSourceOptions: DataSourceOptions = {
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
|
@ -25,16 +25,13 @@ const dataSourceOptions: DataSourceOptions = {
|
||||||
entities: ['dist/modules/**/*.entity{.ts,.js}'],
|
entities: ['dist/modules/**/*.entity{.ts,.js}'],
|
||||||
migrations: ['dist/migrations/*{.ts,.js}'],
|
migrations: ['dist/migrations/*{.ts,.js}'],
|
||||||
subscribers: ['dist/modules/**/*.subscriber{.ts,.js}'],
|
subscribers: ['dist/modules/**/*.subscriber{.ts,.js}'],
|
||||||
}
|
};
|
||||||
export const dbRegToken = 'database'
|
export const dbRegToken = 'database';
|
||||||
|
|
||||||
export const DatabaseConfig = registerAs(
|
export const DatabaseConfig = registerAs(dbRegToken, (): DataSourceOptions => dataSourceOptions);
|
||||||
dbRegToken,
|
|
||||||
(): DataSourceOptions => dataSourceOptions,
|
|
||||||
)
|
|
||||||
|
|
||||||
export type IDatabaseConfig = ConfigType<typeof DatabaseConfig>
|
export type IDatabaseConfig = ConfigType<typeof DatabaseConfig>;
|
||||||
|
|
||||||
const dataSource = new DataSource(dataSourceOptions)
|
const dataSource = new DataSource(dataSourceOptions);
|
||||||
|
|
||||||
export default dataSource
|
export default dataSource;
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import { AppConfig, IAppConfig, appRegToken } from './app.config'
|
import { AppConfig, IAppConfig, appRegToken } from './app.config';
|
||||||
import { DatabaseConfig, IDatabaseConfig, dbRegToken } from './database.config'
|
import { DatabaseConfig, IDatabaseConfig, dbRegToken } from './database.config';
|
||||||
import { IMailerConfig, MailerConfig, mailerRegToken } from './mailer.config'
|
import { IMailerConfig, MailerConfig, mailerRegToken } from './mailer.config';
|
||||||
import { IOssConfig, OssConfig, ossRegToken } from './oss.config'
|
import { IOssConfig, OssConfig, ossRegToken } from './oss.config';
|
||||||
import { IRedisConfig, RedisConfig, redisRegToken } from './redis.config'
|
import { IRedisConfig, RedisConfig, redisRegToken } from './redis.config';
|
||||||
import { ISecurityConfig, SecurityConfig, securityRegToken } from './security.config'
|
import { ISecurityConfig, SecurityConfig, securityRegToken } from './security.config';
|
||||||
import { ISwaggerConfig, SwaggerConfig, swaggerRegToken } from './swagger.config'
|
import { ISwaggerConfig, SwaggerConfig, swaggerRegToken } from './swagger.config';
|
||||||
|
|
||||||
export * from './app.config'
|
export * from './app.config';
|
||||||
export * from './redis.config'
|
export * from './redis.config';
|
||||||
export * from './database.config'
|
export * from './database.config';
|
||||||
export * from './swagger.config'
|
export * from './swagger.config';
|
||||||
export * from './security.config'
|
export * from './security.config';
|
||||||
export * from './mailer.config'
|
export * from './mailer.config';
|
||||||
export * from './oss.config'
|
export * from './oss.config';
|
||||||
|
|
||||||
export interface AllConfigType {
|
export interface AllConfigType {
|
||||||
[appRegToken]: IAppConfig
|
[appRegToken]: IAppConfig;
|
||||||
[dbRegToken]: IDatabaseConfig
|
[dbRegToken]: IDatabaseConfig;
|
||||||
[mailerRegToken]: IMailerConfig
|
[mailerRegToken]: IMailerConfig;
|
||||||
[redisRegToken]: IRedisConfig
|
[redisRegToken]: IRedisConfig;
|
||||||
[securityRegToken]: ISecurityConfig
|
[securityRegToken]: ISecurityConfig;
|
||||||
[swaggerRegToken]: ISwaggerConfig
|
[swaggerRegToken]: ISwaggerConfig;
|
||||||
[ossRegToken]: IOssConfig
|
[ossRegToken]: IOssConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConfigKeyPaths = RecordNamePaths<AllConfigType>
|
export type ConfigKeyPaths = RecordNamePaths<AllConfigType>;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
AppConfig,
|
AppConfig,
|
||||||
|
@ -34,4 +34,4 @@ export default {
|
||||||
RedisConfig,
|
RedisConfig,
|
||||||
SecurityConfig,
|
SecurityConfig,
|
||||||
SwaggerConfig,
|
SwaggerConfig,
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { ConfigType, registerAs } from '@nestjs/config'
|
import { ConfigType, registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
import { env, envNumber } from '~/global/env'
|
import { env, envNumber } from '~/global/env';
|
||||||
|
|
||||||
export const mailerRegToken = 'mailer'
|
export const mailerRegToken = 'mailer';
|
||||||
|
|
||||||
export const MailerConfig = registerAs(mailerRegToken, () => ({
|
export const MailerConfig = registerAs(mailerRegToken, () => ({
|
||||||
host: env('SMTP_HOST'),
|
host: env('SMTP_HOST'),
|
||||||
|
@ -13,6 +13,6 @@ export const MailerConfig = registerAs(mailerRegToken, () => ({
|
||||||
user: env('SMTP_USER'),
|
user: env('SMTP_USER'),
|
||||||
pass: env('SMTP_PASS'),
|
pass: env('SMTP_PASS'),
|
||||||
},
|
},
|
||||||
}))
|
}));
|
||||||
|
|
||||||
export type IMailerConfig = ConfigType<typeof MailerConfig>
|
export type IMailerConfig = ConfigType<typeof MailerConfig>;
|
||||||
|
|
|
@ -1,24 +1,24 @@
|
||||||
import { ConfigType, registerAs } from '@nestjs/config'
|
import { ConfigType, registerAs } from '@nestjs/config';
|
||||||
import * as qiniu from 'qiniu'
|
import * as qiniu from 'qiniu';
|
||||||
|
|
||||||
import { env } from '~/global/env'
|
import { env } from '~/global/env';
|
||||||
|
|
||||||
function parseZone(zone: string) {
|
function parseZone(zone: string) {
|
||||||
switch (zone) {
|
switch (zone) {
|
||||||
case 'Zone_as0':
|
case 'Zone_as0':
|
||||||
return qiniu.zone.Zone_as0
|
return qiniu.zone.Zone_as0;
|
||||||
case 'Zone_na0':
|
case 'Zone_na0':
|
||||||
return qiniu.zone.Zone_na0
|
return qiniu.zone.Zone_na0;
|
||||||
case 'Zone_z0':
|
case 'Zone_z0':
|
||||||
return qiniu.zone.Zone_z0
|
return qiniu.zone.Zone_z0;
|
||||||
case 'Zone_z1':
|
case 'Zone_z1':
|
||||||
return qiniu.zone.Zone_z1
|
return qiniu.zone.Zone_z1;
|
||||||
case 'Zone_z2':
|
case 'Zone_z2':
|
||||||
return qiniu.zone.Zone_z2
|
return qiniu.zone.Zone_z2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ossRegToken = 'oss'
|
export const ossRegToken = 'oss';
|
||||||
|
|
||||||
export const OssConfig = registerAs(ossRegToken, () => ({
|
export const OssConfig = registerAs(ossRegToken, () => ({
|
||||||
accessKey: env('OSS_ACCESSKEY'),
|
accessKey: env('OSS_ACCESSKEY'),
|
||||||
|
@ -27,6 +27,6 @@ export const OssConfig = registerAs(ossRegToken, () => ({
|
||||||
bucket: env('OSS_BUCKET'),
|
bucket: env('OSS_BUCKET'),
|
||||||
zone: parseZone(env('OSS_ZONE') || 'Zone_z2'),
|
zone: parseZone(env('OSS_ZONE') || 'Zone_z2'),
|
||||||
access: (env('OSS_ACCESS_TYPE') as any) || 'public',
|
access: (env('OSS_ACCESS_TYPE') as any) || 'public',
|
||||||
}))
|
}));
|
||||||
|
|
||||||
export type IOssConfig = ConfigType<typeof OssConfig>
|
export type IOssConfig = ConfigType<typeof OssConfig>;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { ConfigType, registerAs } from '@nestjs/config'
|
import { ConfigType, registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
import { env, envNumber } from '~/global/env'
|
import { env, envNumber } from '~/global/env';
|
||||||
|
|
||||||
export const redisRegToken = 'redis'
|
export const redisRegToken = 'redis';
|
||||||
|
|
||||||
export const RedisConfig = registerAs(redisRegToken, () => ({
|
export const RedisConfig = registerAs(redisRegToken, () => ({
|
||||||
host: env('REDIS_HOST', '127.0.0.1'),
|
host: env('REDIS_HOST', '127.0.0.1'),
|
||||||
port: envNumber('REDIS_PORT', 6379),
|
port: envNumber('REDIS_PORT', 6379),
|
||||||
password: env('REDIS_PASSWORD'),
|
password: env('REDIS_PASSWORD'),
|
||||||
db: envNumber('REDIS_DB'),
|
db: envNumber('REDIS_DB'),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
export type IRedisConfig = ConfigType<typeof RedisConfig>
|
export type IRedisConfig = ConfigType<typeof RedisConfig>;
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { ConfigType, registerAs } from '@nestjs/config'
|
import { ConfigType, registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
import { env, envNumber } from '~/global/env'
|
import { env, envNumber } from '~/global/env';
|
||||||
|
|
||||||
export const securityRegToken = 'security'
|
export const securityRegToken = 'security';
|
||||||
|
|
||||||
export const SecurityConfig = registerAs(securityRegToken, () => ({
|
export const SecurityConfig = registerAs(securityRegToken, () => ({
|
||||||
jwtSecret: env('JWT_SECRET'),
|
jwtSecret: env('JWT_SECRET'),
|
||||||
jwtExprire: envNumber('JWT_EXPIRE'),
|
jwtExprire: envNumber('JWT_EXPIRE'),
|
||||||
refreshSecret: env('REFRESH_TOKEN_SECRET'),
|
refreshSecret: env('REFRESH_TOKEN_SECRET'),
|
||||||
refreshExpire: envNumber('REFRESH_TOKEN_EXPIRE'),
|
refreshExpire: envNumber('REFRESH_TOKEN_EXPIRE'),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
export type ISecurityConfig = ConfigType<typeof SecurityConfig>
|
export type ISecurityConfig = ConfigType<typeof SecurityConfig>;
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { ConfigType, registerAs } from '@nestjs/config'
|
import { ConfigType, registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
import { env, envBoolean } from '~/global/env'
|
import { env, envBoolean } from '~/global/env';
|
||||||
|
|
||||||
export const swaggerRegToken = 'swagger'
|
export const swaggerRegToken = 'swagger';
|
||||||
|
|
||||||
export const SwaggerConfig = registerAs(swaggerRegToken, () => ({
|
export const SwaggerConfig = registerAs(swaggerRegToken, () => ({
|
||||||
enable: envBoolean('SWAGGER_ENABLE'),
|
enable: envBoolean('SWAGGER_ENABLE'),
|
||||||
path: env('SWAGGER_PATH'),
|
path: env('SWAGGER_PATH'),
|
||||||
}))
|
}));
|
||||||
|
|
||||||
export type ISwaggerConfig = ConfigType<typeof SwaggerConfig>
|
export type ISwaggerConfig = ConfigType<typeof SwaggerConfig>;
|
||||||
|
|
|
@ -5,4 +5,4 @@ export enum RedisKeys {
|
||||||
AUTH_PERM_PREFIX = 'auth:permission:',
|
AUTH_PERM_PREFIX = 'auth:permission:',
|
||||||
AUTH_PASSWORD_V_PREFIX = 'auth:passwordVersion:',
|
AUTH_PASSWORD_V_PREFIX = 'auth:passwordVersion:',
|
||||||
}
|
}
|
||||||
export const API_CACHE_PREFIX = 'api-cache:'
|
export const API_CACHE_PREFIX = 'api-cache:';
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
export const OSS_CONFIG = 'admin_module:qiniu_config'
|
export const OSS_CONFIG = 'admin_module:qiniu_config';
|
||||||
export const OSS_API = 'http://api.qiniu.com'
|
export const OSS_API = 'http://api.qiniu.com';
|
||||||
|
|
||||||
// 目录分隔符
|
// 目录分隔符
|
||||||
export const NETDISK_DELIMITER = '/'
|
export const NETDISK_DELIMITER = '/';
|
||||||
export const NETDISK_LIMIT = 100
|
export const NETDISK_LIMIT = 100;
|
||||||
export const NETDISK_HANDLE_MAX_ITEM = 1000
|
export const NETDISK_HANDLE_MAX_ITEM = 1000;
|
||||||
export const NETDISK_COPY_SUFFIX = '的副本'
|
export const NETDISK_COPY_SUFFIX = '的副本';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export const RESPONSE_SUCCESS_CODE = 200
|
export const RESPONSE_SUCCESS_CODE = 200;
|
||||||
|
|
||||||
export const RESPONSE_SUCCESS_MSG = 'success'
|
export const RESPONSE_SUCCESS_MSG = 'success';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description: contentType
|
* @description: contentType
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export const SYS_USER_INITPASSWORD = 'sys_user_initPassword'
|
export const SYS_USER_INITPASSWORD = 'sys_user_initPassword';
|
||||||
export const SYS_API_TOKEN = 'sys_api_token'
|
export const SYS_API_TOKEN = 'sys_api_token';
|
||||||
/** 超级管理员用户 id */
|
/** 超级管理员用户 id */
|
||||||
export const ROOT_USER_ID = 1
|
export const ROOT_USER_ID = 1;
|
||||||
/** 超级管理员角色 id */
|
/** 超级管理员角色 id */
|
||||||
export const ROOT_ROLE_ID = 1
|
export const ROOT_ROLE_ID = 1;
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import cluster from 'node:cluster'
|
import cluster from 'node:cluster';
|
||||||
|
|
||||||
export const isMainCluster
|
export const isMainCluster =
|
||||||
= process.env.NODE_APP_INSTANCE && Number.parseInt(process.env.NODE_APP_INSTANCE) === 0
|
process.env.NODE_APP_INSTANCE && Number.parseInt(process.env.NODE_APP_INSTANCE) === 0;
|
||||||
export const isMainProcess = cluster.isPrimary || isMainCluster
|
export const isMainProcess = cluster.isPrimary || isMainCluster;
|
||||||
|
|
||||||
export const isDev = process.env.NODE_ENV === 'development'
|
export const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
export const isTest = !!process.env.TEST
|
export const isTest = !!process.env.TEST;
|
||||||
export const cwd = process.cwd()
|
export const cwd = process.cwd();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础类型接口
|
* 基础类型接口
|
||||||
*/
|
*/
|
||||||
export type BaseType = boolean | number | string | undefined | null
|
export type BaseType = boolean | number | string | undefined | null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化环境变量
|
* 格式化环境变量
|
||||||
|
@ -20,43 +20,43 @@ export type BaseType = boolean | number | string | undefined | null
|
||||||
* @param defaultValue 默认值
|
* @param defaultValue 默认值
|
||||||
* @param callback 格式化函数
|
* @param callback 格式化函数
|
||||||
*/
|
*/
|
||||||
function fromatValue<T extends BaseType = string>(key: string, defaultValue: T, callback?: (value: string) => T): T {
|
function fromatValue<T extends BaseType = string>(
|
||||||
const value: string | undefined = process.env[key]
|
key: string,
|
||||||
if (typeof value === 'undefined')
|
defaultValue: T,
|
||||||
return defaultValue
|
callback?: (value: string) => T
|
||||||
|
): T {
|
||||||
|
const value: string | undefined = process.env[key];
|
||||||
|
if (typeof value === 'undefined') return defaultValue;
|
||||||
|
|
||||||
if (!callback)
|
if (!callback) return value as unknown as T;
|
||||||
return value as unknown as T
|
|
||||||
|
|
||||||
return callback(value)
|
return callback(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function env(key: string, defaultValue: string = '') {
|
export function env(key: string, defaultValue: string = '') {
|
||||||
return fromatValue(key, defaultValue)
|
return fromatValue(key, defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function envString(key: string, defaultValue: string = '') {
|
export function envString(key: string, defaultValue: string = '') {
|
||||||
return fromatValue(key, defaultValue)
|
return fromatValue(key, defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function envNumber(key: string, defaultValue: number = 0) {
|
export function envNumber(key: string, defaultValue: number = 0) {
|
||||||
return fromatValue(key, defaultValue, (value) => {
|
return fromatValue(key, defaultValue, value => {
|
||||||
try {
|
try {
|
||||||
return Number(value)
|
return Number(value);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${key} environment variable is not a number`);
|
||||||
}
|
}
|
||||||
catch {
|
});
|
||||||
throw new Error(`${key} environment variable is not a number`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function envBoolean(key: string, defaultValue: boolean = false) {
|
export function envBoolean(key: string, defaultValue: boolean = false) {
|
||||||
return fromatValue(key, defaultValue, (value) => {
|
return fromatValue(key, defaultValue, value => {
|
||||||
try {
|
try {
|
||||||
return Boolean(JSON.parse(value))
|
return Boolean(JSON.parse(value));
|
||||||
|
} catch {
|
||||||
|
throw new Error(`${key} environment variable is not a boolean`);
|
||||||
}
|
}
|
||||||
catch {
|
});
|
||||||
throw new Error(`${key} environment variable is not a boolean`)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
export function catchError() {
|
export function catchError() {
|
||||||
process.on('unhandledRejection', (reason, p) => {
|
process.on('unhandledRejection', (reason, p) => {
|
||||||
console.log('Promise: ', p, 'Reason: ', reason)
|
console.log('Promise: ', p, 'Reason: ', reason);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,35 @@
|
||||||
import { NotFoundException } from '@nestjs/common'
|
import { NotFoundException } from '@nestjs/common';
|
||||||
import { ObjectLiteral, Repository } from 'typeorm'
|
import { ObjectLiteral, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { PagerDto } from '~/common/dto/pager.dto'
|
import { PagerDto } from '~/common/dto/pager.dto';
|
||||||
|
|
||||||
import { paginate } from '../paginate'
|
import { paginate } from '../paginate';
|
||||||
import { Pagination } from '../paginate/pagination'
|
import { Pagination } from '../paginate/pagination';
|
||||||
|
|
||||||
export class BaseService<E extends ObjectLiteral, R extends Repository<E> = Repository<E>> {
|
export class BaseService<E extends ObjectLiteral, R extends Repository<E> = Repository<E>> {
|
||||||
constructor(private repository: R) {
|
constructor(private repository: R) {}
|
||||||
}
|
|
||||||
|
|
||||||
async list({
|
async list({ page, pageSize }: PagerDto): Promise<Pagination<E>> {
|
||||||
page,
|
return paginate(this.repository, { page, pageSize });
|
||||||
pageSize,
|
|
||||||
}: PagerDto): Promise<Pagination<E>> {
|
|
||||||
return paginate(this.repository, { page, pageSize })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(id: number): Promise<E> {
|
async findOne(id: number): Promise<E> {
|
||||||
const item = await this.repository.createQueryBuilder().where({ id }).getOne()
|
const item = await this.repository.createQueryBuilder().where({ id }).getOne();
|
||||||
if (!item)
|
if (!item) throw new NotFoundException('未找到该记录');
|
||||||
throw new NotFoundException('未找到该记录')
|
|
||||||
|
|
||||||
return item
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: any): Promise<E> {
|
async create(dto: any): Promise<E> {
|
||||||
return await this.repository.save(dto)
|
return await this.repository.save(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(id: number, dto: any): Promise<void> {
|
async update(id: number, dto: any): Promise<void> {
|
||||||
await this.repository.update(id, dto)
|
await this.repository.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: number): Promise<void> {
|
async delete(id: number): Promise<void> {
|
||||||
const item = await this.findOne(id)
|
const item = await this.findOne(id);
|
||||||
await this.repository.remove(item)
|
await this.repository.remove(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,42 @@
|
||||||
import type { Type } from '@nestjs/common'
|
import type { Type } from '@nestjs/common';
|
||||||
|
|
||||||
import {
|
import { Body, Controller, Delete, Get, Patch, Post, Put, Query } from '@nestjs/common';
|
||||||
Body,
|
import { ApiBody, IntersectionType, PartialType } from '@nestjs/swagger';
|
||||||
Controller,
|
import pluralize from 'pluralize';
|
||||||
Delete,
|
|
||||||
Get,
|
|
||||||
Patch,
|
|
||||||
Post,
|
|
||||||
Put,
|
|
||||||
Query,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { ApiBody, IntersectionType, PartialType } from '@nestjs/swagger'
|
|
||||||
import pluralize from 'pluralize'
|
|
||||||
|
|
||||||
import { ApiResult } from '~/common/decorators/api-result.decorator'
|
import { ApiResult } from '~/common/decorators/api-result.decorator';
|
||||||
import { IdParam } from '~/common/decorators/id-param.decorator'
|
import { IdParam } from '~/common/decorators/id-param.decorator';
|
||||||
import { PagerDto } from '~/common/dto/pager.dto'
|
import { PagerDto } from '~/common/dto/pager.dto';
|
||||||
|
|
||||||
import { BaseService } from './base.service'
|
import { BaseService } from './base.service';
|
||||||
|
|
||||||
export function BaseCrudFactory<
|
export function BaseCrudFactory<E extends new (...args: any[]) => any>({
|
||||||
E extends new (...args: any[]) => any,
|
entity,
|
||||||
>({ entity, dto, permissions }: { entity: E, dto?: Type<any>, permissions?: Record<string, string> }): Type<any> {
|
dto,
|
||||||
const prefix = entity.name.toLowerCase().replace(/entity$/, '')
|
permissions,
|
||||||
const pluralizeName = pluralize(prefix) as string
|
}: {
|
||||||
|
entity: E;
|
||||||
|
dto?: Type<any>;
|
||||||
|
permissions?: Record<string, string>;
|
||||||
|
}): Type<any> {
|
||||||
|
const prefix = entity.name.toLowerCase().replace(/entity$/, '');
|
||||||
|
const pluralizeName = pluralize(prefix) as string;
|
||||||
|
|
||||||
dto = dto ?? class extends entity {}
|
dto = dto ?? class extends entity {};
|
||||||
|
|
||||||
class Dto extends dto {}
|
class Dto extends dto {}
|
||||||
class UpdateDto extends PartialType(Dto) {}
|
class UpdateDto extends PartialType(Dto) {}
|
||||||
class QueryDto extends IntersectionType(PagerDto, PartialType(Dto)) {}
|
class QueryDto extends IntersectionType(PagerDto, PartialType(Dto)) {}
|
||||||
|
|
||||||
permissions = permissions ?? {
|
permissions =
|
||||||
|
permissions ??
|
||||||
|
({
|
||||||
LIST: `${prefix}:list`,
|
LIST: `${prefix}:list`,
|
||||||
CREATE: `${prefix}:create`,
|
CREATE: `${prefix}:create`,
|
||||||
READ: `${prefix}:read`,
|
READ: `${prefix}:read`,
|
||||||
UPDATE: `${prefix}:update`,
|
UPDATE: `${prefix}:update`,
|
||||||
DELETE: `${prefix}:delete`,
|
DELETE: `${prefix}:delete`,
|
||||||
} as const
|
} as const);
|
||||||
|
|
||||||
@Controller(pluralizeName)
|
@Controller(pluralizeName)
|
||||||
class BaseController<S extends BaseService<E>> {
|
class BaseController<S extends BaseService<E>> {
|
||||||
|
@ -46,36 +45,36 @@ export function BaseCrudFactory<
|
||||||
@Get()
|
@Get()
|
||||||
@ApiResult({ type: [entity], isPage: true })
|
@ApiResult({ type: [entity], isPage: true })
|
||||||
async list(@Query() pager: QueryDto) {
|
async list(@Query() pager: QueryDto) {
|
||||||
return await this.service.list(pager)
|
return await this.service.list(pager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@ApiResult({ type: entity })
|
@ApiResult({ type: entity })
|
||||||
async get(@IdParam() id: number) {
|
async get(@IdParam() id: number) {
|
||||||
return await this.service.findOne(id)
|
return await this.service.findOne(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiBody({ type: dto })
|
@ApiBody({ type: dto })
|
||||||
async create(@Body() dto: Dto) {
|
async create(@Body() dto: Dto) {
|
||||||
return await this.service.create(dto)
|
return await this.service.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
async update(@IdParam() id: number, @Body() dto: UpdateDto) {
|
async update(@IdParam() id: number, @Body() dto: UpdateDto) {
|
||||||
return await this.service.update(id, dto)
|
return await this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
async patch(@IdParam() id: number, @Body() dto: UpdateDto) {
|
async patch(@IdParam() id: number, @Body() dto: UpdateDto) {
|
||||||
await this.service.update(id, dto)
|
await this.service.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
async delete(@IdParam() id: number) {
|
async delete(@IdParam() id: number) {
|
||||||
await this.service.delete(id)
|
await this.service.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return BaseController
|
return BaseController;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { RedisKeys } from '~/constants/cache.constant'
|
import { RedisKeys } from '~/constants/cache.constant';
|
||||||
|
|
||||||
/** 生成验证码 redis key */
|
/** 生成验证码 redis key */
|
||||||
export function genCaptchaImgKey(val: string | number) {
|
export function genCaptchaImgKey(val: string | number) {
|
||||||
return `${RedisKeys.CAPTCHA_IMG_PREFIX}${String(val)}` as const
|
return `${RedisKeys.CAPTCHA_IMG_PREFIX}${String(val)}` as const;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 生成 auth token redis key */
|
/** 生成 auth token redis key */
|
||||||
export function genAuthTokenKey(val: string | number) {
|
export function genAuthTokenKey(val: string | number) {
|
||||||
return `${RedisKeys.AUTH_TOKEN_PREFIX}${String(val)}` as const
|
return `${RedisKeys.AUTH_TOKEN_PREFIX}${String(val)}` as const;
|
||||||
}
|
}
|
||||||
/** 生成 auth permission redis key */
|
/** 生成 auth permission redis key */
|
||||||
export function genAuthPermKey(val: string | number) {
|
export function genAuthPermKey(val: string | number) {
|
||||||
return `${RedisKeys.AUTH_PERM_PREFIX}${String(val)}` as const
|
return `${RedisKeys.AUTH_PERM_PREFIX}${String(val)}` as const;
|
||||||
}
|
}
|
||||||
/** 生成 auth passwordVersion redis key */
|
/** 生成 auth passwordVersion redis key */
|
||||||
export function genAuthPVKey(val: string | number) {
|
export function genAuthPVKey(val: string | number) {
|
||||||
return `${RedisKeys.AUTH_PASSWORD_V_PREFIX}${String(val)}` as const
|
return `${RedisKeys.AUTH_PASSWORD_V_PREFIX}${String(val)}` as const;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { IPaginationMeta } from './interface'
|
import { IPaginationMeta } from './interface';
|
||||||
import { Pagination } from './pagination'
|
import { Pagination } from './pagination';
|
||||||
|
|
||||||
export function createPaginationObject<T>({
|
export function createPaginationObject<T>({
|
||||||
items,
|
items,
|
||||||
|
@ -7,13 +7,12 @@ export function createPaginationObject<T>({
|
||||||
currentPage,
|
currentPage,
|
||||||
limit,
|
limit,
|
||||||
}: {
|
}: {
|
||||||
items: T[]
|
items: T[];
|
||||||
totalItems?: number
|
totalItems?: number;
|
||||||
currentPage: number
|
currentPage: number;
|
||||||
limit: number
|
limit: number;
|
||||||
}): Pagination<T> {
|
}): Pagination<T> {
|
||||||
const totalPages
|
const totalPages = totalItems !== undefined ? Math.ceil(totalItems / limit) : undefined;
|
||||||
= totalItems !== undefined ? Math.ceil(totalItems / limit) : undefined
|
|
||||||
|
|
||||||
const meta: IPaginationMeta = {
|
const meta: IPaginationMeta = {
|
||||||
totalItems,
|
totalItems,
|
||||||
|
@ -21,7 +20,7 @@ export function createPaginationObject<T>({
|
||||||
itemsPerPage: limit,
|
itemsPerPage: limit,
|
||||||
totalPages,
|
totalPages,
|
||||||
currentPage,
|
currentPage,
|
||||||
}
|
};
|
||||||
|
|
||||||
return new Pagination<T>(items, meta)
|
return new Pagination<T>(items, meta);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,33 +4,31 @@ import {
|
||||||
ObjectLiteral,
|
ObjectLiteral,
|
||||||
Repository,
|
Repository,
|
||||||
SelectQueryBuilder,
|
SelectQueryBuilder,
|
||||||
} from 'typeorm'
|
} from 'typeorm';
|
||||||
|
|
||||||
import { createPaginationObject } from './create-pagination'
|
import { createPaginationObject } from './create-pagination';
|
||||||
import { IPaginationOptions, PaginationTypeEnum } from './interface'
|
import { IPaginationOptions, PaginationTypeEnum } from './interface';
|
||||||
import { Pagination } from './pagination'
|
import { Pagination } from './pagination';
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 10
|
const DEFAULT_LIMIT = 10;
|
||||||
const DEFAULT_PAGE = 1
|
const DEFAULT_PAGE = 1;
|
||||||
|
|
||||||
function resolveOptions(
|
function resolveOptions(options: IPaginationOptions): [number, number, PaginationTypeEnum] {
|
||||||
options: IPaginationOptions,
|
const { page, pageSize, paginationType } = options;
|
||||||
): [number, number, PaginationTypeEnum] {
|
|
||||||
const { page, pageSize, paginationType } = options
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
page || DEFAULT_PAGE,
|
page || DEFAULT_PAGE,
|
||||||
pageSize || DEFAULT_LIMIT,
|
pageSize || DEFAULT_LIMIT,
|
||||||
paginationType || PaginationTypeEnum.TAKE_AND_SKIP,
|
paginationType || PaginationTypeEnum.TAKE_AND_SKIP,
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function paginateRepository<T>(
|
async function paginateRepository<T>(
|
||||||
repository: Repository<T>,
|
repository: Repository<T>,
|
||||||
options: IPaginationOptions,
|
options: IPaginationOptions,
|
||||||
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>,
|
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>
|
||||||
): Promise<Pagination<T>> {
|
): Promise<Pagination<T>> {
|
||||||
const [page, limit] = resolveOptions(options)
|
const [page, limit] = resolveOptions(options);
|
||||||
|
|
||||||
const promises: [Promise<T[]>, Promise<number> | undefined] = [
|
const promises: [Promise<T[]>, Promise<number> | undefined] = [
|
||||||
repository.find({
|
repository.find({
|
||||||
|
@ -39,44 +37,43 @@ async function paginateRepository<T>(
|
||||||
...searchOptions,
|
...searchOptions,
|
||||||
}),
|
}),
|
||||||
undefined,
|
undefined,
|
||||||
]
|
];
|
||||||
|
|
||||||
const [items, total] = await Promise.all(promises)
|
const [items, total] = await Promise.all(promises);
|
||||||
|
|
||||||
return createPaginationObject<T>({
|
return createPaginationObject<T>({
|
||||||
items,
|
items,
|
||||||
totalItems: total,
|
totalItems: total,
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
limit,
|
limit,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function paginateQueryBuilder<T>(
|
async function paginateQueryBuilder<T>(
|
||||||
queryBuilder: SelectQueryBuilder<T>,
|
queryBuilder: SelectQueryBuilder<T>,
|
||||||
options: IPaginationOptions,
|
options: IPaginationOptions
|
||||||
): Promise<Pagination<T>> {
|
): Promise<Pagination<T>> {
|
||||||
const [page, limit, paginationType] = resolveOptions(options)
|
const [page, limit, paginationType] = resolveOptions(options);
|
||||||
|
|
||||||
if (paginationType === PaginationTypeEnum.TAKE_AND_SKIP)
|
if (paginationType === PaginationTypeEnum.TAKE_AND_SKIP)
|
||||||
queryBuilder.take(limit).skip((page - 1) * limit)
|
queryBuilder.take(limit).skip((page - 1) * limit);
|
||||||
else
|
else queryBuilder.limit(limit).offset((page - 1) * limit);
|
||||||
queryBuilder.limit(limit).offset((page - 1) * limit)
|
|
||||||
|
|
||||||
const [items, total] = await queryBuilder.getManyAndCount()
|
const [items, total] = await queryBuilder.getManyAndCount();
|
||||||
|
|
||||||
return createPaginationObject<T>({
|
return createPaginationObject<T>({
|
||||||
items,
|
items,
|
||||||
totalItems: total,
|
totalItems: total,
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
limit,
|
limit,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function paginateRaw<T>(
|
export async function paginateRaw<T>(
|
||||||
queryBuilder: SelectQueryBuilder<T>,
|
queryBuilder: SelectQueryBuilder<T>,
|
||||||
options: IPaginationOptions,
|
options: IPaginationOptions
|
||||||
): Promise<Pagination<T>> {
|
): Promise<Pagination<T>> {
|
||||||
const [page, limit, paginationType] = resolveOptions(options)
|
const [page, limit, paginationType] = resolveOptions(options);
|
||||||
|
|
||||||
const promises: [Promise<T[]>, Promise<number> | undefined] = [
|
const promises: [Promise<T[]>, Promise<number> | undefined] = [
|
||||||
(paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET
|
(paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET
|
||||||
|
@ -84,36 +81,33 @@ export async function paginateRaw<T>(
|
||||||
: queryBuilder.take(limit).skip((page - 1) * limit)
|
: queryBuilder.take(limit).skip((page - 1) * limit)
|
||||||
).getRawMany<T>(),
|
).getRawMany<T>(),
|
||||||
queryBuilder.getCount(),
|
queryBuilder.getCount(),
|
||||||
]
|
];
|
||||||
|
|
||||||
const [items, total] = await Promise.all(promises)
|
const [items, total] = await Promise.all(promises);
|
||||||
|
|
||||||
return createPaginationObject<T>({
|
return createPaginationObject<T>({
|
||||||
items,
|
items,
|
||||||
totalItems: total,
|
totalItems: total,
|
||||||
currentPage: page,
|
currentPage: page,
|
||||||
limit,
|
limit,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function paginateRawAndEntities<T>(
|
export async function paginateRawAndEntities<T>(
|
||||||
queryBuilder: SelectQueryBuilder<T>,
|
queryBuilder: SelectQueryBuilder<T>,
|
||||||
options: IPaginationOptions,
|
options: IPaginationOptions
|
||||||
): Promise<[Pagination<T>, Partial<T>[]]> {
|
): Promise<[Pagination<T>, Partial<T>[]]> {
|
||||||
const [page, limit, paginationType] = resolveOptions(options)
|
const [page, limit, paginationType] = resolveOptions(options);
|
||||||
|
|
||||||
const promises: [
|
const promises: [Promise<{ entities: T[]; raw: T[] }>, Promise<number> | undefined] = [
|
||||||
Promise<{ entities: T[], raw: T[] }>,
|
|
||||||
Promise<number> | undefined,
|
|
||||||
] = [
|
|
||||||
(paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET
|
(paginationType === PaginationTypeEnum.LIMIT_AND_OFFSET
|
||||||
? queryBuilder.limit(limit).offset((page - 1) * limit)
|
? queryBuilder.limit(limit).offset((page - 1) * limit)
|
||||||
: queryBuilder.take(limit).skip((page - 1) * limit)
|
: queryBuilder.take(limit).skip((page - 1) * limit)
|
||||||
).getRawAndEntities<T>(),
|
).getRawAndEntities<T>(),
|
||||||
queryBuilder.getCount(),
|
queryBuilder.getCount(),
|
||||||
]
|
];
|
||||||
|
|
||||||
const [itemObject, total] = await Promise.all(promises)
|
const [itemObject, total] = await Promise.all(promises);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
createPaginationObject<T>({
|
createPaginationObject<T>({
|
||||||
|
@ -123,25 +117,25 @@ export async function paginateRawAndEntities<T>(
|
||||||
limit,
|
limit,
|
||||||
}),
|
}),
|
||||||
itemObject.raw,
|
itemObject.raw,
|
||||||
]
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function paginate<T extends ObjectLiteral>(
|
export async function paginate<T extends ObjectLiteral>(
|
||||||
repository: Repository<T>,
|
repository: Repository<T>,
|
||||||
options: IPaginationOptions,
|
options: IPaginationOptions,
|
||||||
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>,
|
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>
|
||||||
): Promise<Pagination<T>>
|
): Promise<Pagination<T>>;
|
||||||
export async function paginate<T>(
|
export async function paginate<T>(
|
||||||
queryBuilder: SelectQueryBuilder<T>,
|
queryBuilder: SelectQueryBuilder<T>,
|
||||||
options: IPaginationOptions,
|
options: IPaginationOptions
|
||||||
): Promise<Pagination<T>>
|
): Promise<Pagination<T>>;
|
||||||
|
|
||||||
export async function paginate<T extends ObjectLiteral>(
|
export async function paginate<T extends ObjectLiteral>(
|
||||||
repositoryOrQueryBuilder: Repository<T> | SelectQueryBuilder<T>,
|
repositoryOrQueryBuilder: Repository<T> | SelectQueryBuilder<T>,
|
||||||
options: IPaginationOptions,
|
options: IPaginationOptions,
|
||||||
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>,
|
searchOptions?: FindOptionsWhere<T> | FindManyOptions<T>
|
||||||
) {
|
) {
|
||||||
return repositoryOrQueryBuilder instanceof Repository
|
return repositoryOrQueryBuilder instanceof Repository
|
||||||
? paginateRepository<T>(repositoryOrQueryBuilder, options, searchOptions)
|
? paginateRepository<T>(repositoryOrQueryBuilder, options, searchOptions)
|
||||||
: paginateQueryBuilder<T>(repositoryOrQueryBuilder, options)
|
: paginateQueryBuilder<T>(repositoryOrQueryBuilder, options);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ObjectLiteral } from 'typeorm'
|
import { ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
export enum PaginationTypeEnum {
|
export enum PaginationTypeEnum {
|
||||||
LIMIT_AND_OFFSET = 'limit',
|
LIMIT_AND_OFFSET = 'limit',
|
||||||
|
@ -6,22 +6,22 @@ export enum PaginationTypeEnum {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPaginationOptions {
|
export interface IPaginationOptions {
|
||||||
page: number
|
page: number;
|
||||||
pageSize: number
|
pageSize: number;
|
||||||
paginationType?: PaginationTypeEnum
|
paginationType?: PaginationTypeEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPaginationMeta extends ObjectLiteral {
|
export interface IPaginationMeta extends ObjectLiteral {
|
||||||
itemCount: number
|
itemCount: number;
|
||||||
totalItems?: number
|
totalItems?: number;
|
||||||
itemsPerPage: number
|
itemsPerPage: number;
|
||||||
totalPages?: number
|
totalPages?: number;
|
||||||
currentPage: number
|
currentPage: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPaginationLinks {
|
export interface IPaginationLinks {
|
||||||
first?: string
|
first?: string;
|
||||||
previous?: string
|
previous?: string;
|
||||||
next?: string
|
next?: string;
|
||||||
last?: string
|
last?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { ObjectLiteral } from 'typeorm'
|
import { ObjectLiteral } from 'typeorm';
|
||||||
|
|
||||||
import { IPaginationMeta } from './interface'
|
import { IPaginationMeta } from './interface';
|
||||||
|
|
||||||
export class Pagination<
|
export class Pagination<PaginationObject, T extends ObjectLiteral = IPaginationMeta> {
|
||||||
PaginationObject,
|
|
||||||
T extends ObjectLiteral = IPaginationMeta,
|
|
||||||
> {
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly items: PaginationObject[],
|
public readonly items: PaginationObject[],
|
||||||
|
|
||||||
public readonly meta: T,
|
public readonly meta: T
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
|
|
108
src/main.ts
108
src/main.ts
|
@ -1,56 +1,46 @@
|
||||||
import cluster from 'node:cluster'
|
import cluster from 'node:cluster';
|
||||||
import path from 'node:path'
|
import path from 'node:path';
|
||||||
|
|
||||||
import {
|
import { HttpStatus, Logger, UnprocessableEntityException, ValidationPipe } from '@nestjs/common';
|
||||||
HttpStatus,
|
import { ConfigService } from '@nestjs/config';
|
||||||
Logger,
|
import { NestFactory } from '@nestjs/core';
|
||||||
UnprocessableEntityException,
|
import { NestFastifyApplication } from '@nestjs/platform-fastify';
|
||||||
ValidationPipe,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { ConfigService } from '@nestjs/config'
|
|
||||||
import { NestFactory } from '@nestjs/core'
|
|
||||||
import { NestFastifyApplication } from '@nestjs/platform-fastify'
|
|
||||||
|
|
||||||
import { useContainer } from 'class-validator'
|
import { useContainer } from 'class-validator';
|
||||||
|
|
||||||
import { AppModule } from './app.module'
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
import { fastifyApp } from './common/adapters/fastify.adapter'
|
import { fastifyApp } from './common/adapters/fastify.adapter';
|
||||||
import { RedisIoAdapter } from './common/adapters/socket.adapter'
|
import { RedisIoAdapter } from './common/adapters/socket.adapter';
|
||||||
import { LoggingInterceptor } from './common/interceptors/logging.interceptor'
|
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
|
||||||
import type { ConfigKeyPaths } from './config'
|
import type { ConfigKeyPaths } from './config';
|
||||||
import { isDev, isMainProcess } from './global/env'
|
import { isDev, isMainProcess } from './global/env';
|
||||||
import { setupSwagger } from './setup-swagger'
|
import { setupSwagger } from './setup-swagger';
|
||||||
import { LoggerService } from './shared/logger/logger.service'
|
import { LoggerService } from './shared/logger/logger.service';
|
||||||
|
|
||||||
declare const module: any
|
declare const module: any;
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create<NestFastifyApplication>(
|
const app = await NestFactory.create<NestFastifyApplication>(AppModule, fastifyApp, {
|
||||||
AppModule,
|
|
||||||
fastifyApp,
|
|
||||||
{
|
|
||||||
bufferLogs: true,
|
bufferLogs: true,
|
||||||
snapshot: true,
|
snapshot: true,
|
||||||
// forceCloseConnections: true,
|
// forceCloseConnections: true,
|
||||||
},
|
});
|
||||||
)
|
|
||||||
|
|
||||||
const configService = app.get(ConfigService<ConfigKeyPaths>)
|
const configService = app.get(ConfigService<ConfigKeyPaths>);
|
||||||
|
|
||||||
const { port, globalPrefix } = configService.get('app', { infer: true })
|
const { port, globalPrefix } = configService.get('app', { infer: true });
|
||||||
|
|
||||||
// class-validator 的 DTO 类中注入 nest 容器的依赖 (用于自定义验证器)
|
// class-validator 的 DTO 类中注入 nest 容器的依赖 (用于自定义验证器)
|
||||||
useContainer(app.select(AppModule), { fallbackOnErrors: true })
|
useContainer(app.select(AppModule), { fallbackOnErrors: true });
|
||||||
|
|
||||||
app.enableCors({ origin: '*', credentials: true })
|
app.enableCors({ origin: '*', credentials: true });
|
||||||
app.setGlobalPrefix(globalPrefix)
|
app.setGlobalPrefix(globalPrefix);
|
||||||
app.useStaticAssets({ root: path.join(__dirname, '..', 'public') })
|
app.useStaticAssets({ root: path.join(__dirname, '..', 'public') });
|
||||||
// Starts listening for shutdown hooks
|
// Starts listening for shutdown hooks
|
||||||
!isDev && app.enableShutdownHooks()
|
!isDev && app.enableShutdownHooks();
|
||||||
|
|
||||||
if (isDev)
|
if (isDev) app.useGlobalInterceptors(new LoggingInterceptor());
|
||||||
app.useGlobalInterceptors(new LoggingInterceptor())
|
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
|
@ -62,40 +52,38 @@ async function bootstrap() {
|
||||||
stopAtFirstError: true,
|
stopAtFirstError: true,
|
||||||
exceptionFactory: errors =>
|
exceptionFactory: errors =>
|
||||||
new UnprocessableEntityException(
|
new UnprocessableEntityException(
|
||||||
errors.map((e) => {
|
errors.map(e => {
|
||||||
const rule = Object.keys(e.constraints!)[0]
|
const rule = Object.keys(e.constraints!)[0];
|
||||||
const msg = e.constraints![rule]
|
const msg = e.constraints![rule];
|
||||||
return msg
|
return msg;
|
||||||
})[0],
|
})[0]
|
||||||
),
|
),
|
||||||
}),
|
})
|
||||||
)
|
);
|
||||||
|
|
||||||
app.useWebSocketAdapter(new RedisIoAdapter(app))
|
app.useWebSocketAdapter(new RedisIoAdapter(app));
|
||||||
|
|
||||||
setupSwagger(app, configService)
|
setupSwagger(app, configService);
|
||||||
|
|
||||||
await app.listen(port, '0.0.0.0', async () => {
|
await app.listen(port, '0.0.0.0', async () => {
|
||||||
app.useLogger(app.get(LoggerService))
|
app.useLogger(app.get(LoggerService));
|
||||||
const url = await app.getUrl()
|
const url = await app.getUrl();
|
||||||
const { pid } = process
|
const { pid } = process;
|
||||||
const env = cluster.isPrimary
|
const env = cluster.isPrimary;
|
||||||
const prefix = env ? 'P' : 'W'
|
const prefix = env ? 'P' : 'W';
|
||||||
|
|
||||||
if (!isMainProcess)
|
if (!isMainProcess) return;
|
||||||
return
|
|
||||||
|
|
||||||
const logger = new Logger('NestApplication')
|
const logger = new Logger('NestApplication');
|
||||||
logger.log(`[${prefix + pid}] Server running on ${url}`)
|
logger.log(`[${prefix + pid}] Server running on ${url}`);
|
||||||
|
|
||||||
if (isDev)
|
if (isDev) logger.log(`[${prefix + pid}] OpenAPI: ${url}/api-docs`);
|
||||||
logger.log(`[${prefix + pid}] OpenAPI: ${url}/api-docs`)
|
});
|
||||||
})
|
|
||||||
|
|
||||||
if (module.hot) {
|
if (module.hot) {
|
||||||
module.hot.accept()
|
module.hot.accept();
|
||||||
module.hot.dispose(() => app.close())
|
module.hot.dispose(() => app.close());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap()
|
bootstrap();
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import fs from 'node:fs'
|
import fs from 'node:fs';
|
||||||
import path from 'node:path'
|
import path from 'node:path';
|
||||||
|
|
||||||
import { MigrationInterface, QueryRunner } from 'typeorm'
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
const sql = fs.readFileSync(path.join(__dirname, '../../deploy/sql/hxoa.sql'), 'utf8')
|
const sql = fs.readFileSync(path.join(__dirname, '../../deploy/sql/hxoa.sql'), 'utf8');
|
||||||
|
|
||||||
export class InitData1707996695540 implements MigrationInterface {
|
export class InitData1707996695540 implements MigrationInterface {
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(sql)
|
await queryRunner.query(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
export const PUBLIC_KEY = '__public_key__'
|
export const PUBLIC_KEY = '__public_key__';
|
||||||
|
|
||||||
export const PERMISSION_KEY = '__permission_key__'
|
export const PERMISSION_KEY = '__permission_key__';
|
||||||
|
|
||||||
export const RESOURCE_KEY = '__resource_key__'
|
export const RESOURCE_KEY = '__resource_key__';
|
||||||
|
|
||||||
export const ALLOW_ANON_KEY = '__allow_anon_permission_key__'
|
export const ALLOW_ANON_KEY = '__allow_anon_permission_key__';
|
||||||
|
|
||||||
export const AuthStrategy = {
|
export const AuthStrategy = {
|
||||||
LOCAL: 'local',
|
LOCAL: 'local',
|
||||||
|
@ -15,12 +15,12 @@ export const AuthStrategy = {
|
||||||
|
|
||||||
GITHUB: 'github',
|
GITHUB: 'github',
|
||||||
GOOGLE: 'google',
|
GOOGLE: 'google',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export const Roles = {
|
export const Roles = {
|
||||||
ADMIN: 'admin',
|
ADMIN: 'admin',
|
||||||
USER: 'user',
|
USER: 'user',
|
||||||
// GUEST: 'guest',
|
// GUEST: 'guest',
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type Role = (typeof Roles)[keyof typeof Roles]
|
export type Role = (typeof Roles)[keyof typeof Roles];
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common'
|
import { Body, Controller, Headers, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger'
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { ApiResult } from '~/common/decorators/api-result.decorator'
|
import { ApiResult } from '~/common/decorators/api-result.decorator';
|
||||||
import { Ip } from '~/common/decorators/http.decorator'
|
import { Ip } from '~/common/decorators/http.decorator';
|
||||||
|
|
||||||
import { UserService } from '../user/user.service'
|
import { UserService } from '../user/user.service';
|
||||||
|
|
||||||
import { AuthService } from './auth.service'
|
import { AuthService } from './auth.service';
|
||||||
import { Public } from './decorators/public.decorator'
|
import { Public } from './decorators/public.decorator';
|
||||||
import { LoginDto, RegisterDto } from './dto/auth.dto'
|
import { LoginDto, RegisterDto } from './dto/auth.dto';
|
||||||
import { LocalGuard } from './guards/local.guard'
|
import { LocalGuard } from './guards/local.guard';
|
||||||
import { LoginToken } from './models/auth.model'
|
import { LoginToken } from './models/auth.model';
|
||||||
import { CaptchaService } from './services/captcha.service'
|
import { CaptchaService } from './services/captcha.service';
|
||||||
|
|
||||||
@ApiTags('Auth - 认证模块')
|
@ApiTags('Auth - 认证模块')
|
||||||
@UseGuards(LocalGuard)
|
@UseGuards(LocalGuard)
|
||||||
|
@ -21,27 +21,25 @@ export class AuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private captchaService: CaptchaService,
|
private captchaService: CaptchaService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('login')
|
@Post('login')
|
||||||
@ApiOperation({ summary: '登录' })
|
@ApiOperation({ summary: '登录' })
|
||||||
@ApiResult({ type: LoginToken })
|
@ApiResult({ type: LoginToken })
|
||||||
async login(
|
async login(
|
||||||
@Body() dto: LoginDto, @Ip() ip: string, @Headers('user-agent') ua: string): Promise<LoginToken> {
|
@Body() dto: LoginDto,
|
||||||
await this.captchaService.checkImgCaptcha(dto.captchaId, dto.verifyCode)
|
@Ip() ip: string,
|
||||||
const token = await this.authService.login(
|
@Headers('user-agent') ua: string
|
||||||
dto.username,
|
): Promise<LoginToken> {
|
||||||
dto.password,
|
await this.captchaService.checkImgCaptcha(dto.captchaId, dto.verifyCode);
|
||||||
ip,
|
const token = await this.authService.login(dto.username, dto.password, ip, ua);
|
||||||
ua,
|
return { token };
|
||||||
)
|
|
||||||
return { token }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('register')
|
@Post('register')
|
||||||
@ApiOperation({ summary: '注册' })
|
@ApiOperation({ summary: '注册' })
|
||||||
async register(@Body() dto: RegisterDto): Promise<void> {
|
async register(@Body() dto: RegisterDto): Promise<void> {
|
||||||
await this.userService.register(dto)
|
await this.userService.register(dto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,38 +1,33 @@
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config'
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { JwtModule } from '@nestjs/jwt'
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
import { PassportModule } from '@nestjs/passport'
|
import { PassportModule } from '@nestjs/passport';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm'
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { ConfigKeyPaths, ISecurityConfig } from '~/config'
|
import { ConfigKeyPaths, ISecurityConfig } from '~/config';
|
||||||
import { isDev } from '~/global/env'
|
import { isDev } from '~/global/env';
|
||||||
|
|
||||||
import { LogModule } from '../system/log/log.module'
|
import { LogModule } from '../system/log/log.module';
|
||||||
import { MenuModule } from '../system/menu/menu.module'
|
import { MenuModule } from '../system/menu/menu.module';
|
||||||
import { RoleModule } from '../system/role/role.module'
|
import { RoleModule } from '../system/role/role.module';
|
||||||
import { UserModule } from '../user/user.module'
|
import { UserModule } from '../user/user.module';
|
||||||
|
|
||||||
import { AuthController } from './auth.controller'
|
import { AuthController } from './auth.controller';
|
||||||
import { AuthService } from './auth.service'
|
import { AuthService } from './auth.service';
|
||||||
import { AccountController } from './controllers/account.controller'
|
import { AccountController } from './controllers/account.controller';
|
||||||
import { CaptchaController } from './controllers/captcha.controller'
|
import { CaptchaController } from './controllers/captcha.controller';
|
||||||
import { EmailController } from './controllers/email.controller'
|
import { EmailController } from './controllers/email.controller';
|
||||||
import { AccessTokenEntity } from './entities/access-token.entity'
|
import { AccessTokenEntity } from './entities/access-token.entity';
|
||||||
import { RefreshTokenEntity } from './entities/refresh-token.entity'
|
import { RefreshTokenEntity } from './entities/refresh-token.entity';
|
||||||
import { CaptchaService } from './services/captcha.service'
|
import { CaptchaService } from './services/captcha.service';
|
||||||
import { TokenService } from './services/token.service'
|
import { TokenService } from './services/token.service';
|
||||||
import { JwtStrategy } from './strategies/jwt.strategy'
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
import { LocalStrategy } from './strategies/local.strategy'
|
import { LocalStrategy } from './strategies/local.strategy';
|
||||||
|
|
||||||
const controllers = [
|
const controllers = [AuthController, AccountController, CaptchaController, EmailController];
|
||||||
AuthController,
|
const providers = [AuthService, TokenService, CaptchaService];
|
||||||
AccountController,
|
const strategies = [LocalStrategy, JwtStrategy];
|
||||||
CaptchaController,
|
|
||||||
EmailController,
|
|
||||||
]
|
|
||||||
const providers = [AuthService, TokenService, CaptchaService]
|
|
||||||
const strategies = [LocalStrategy, JwtStrategy]
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
@ -41,14 +36,13 @@ const strategies = [LocalStrategy, JwtStrategy]
|
||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: (configService: ConfigService<ConfigKeyPaths>) => {
|
useFactory: (configService: ConfigService<ConfigKeyPaths>) => {
|
||||||
const { jwtSecret, jwtExprire }
|
const { jwtSecret, jwtExprire } = configService.get<ISecurityConfig>('security');
|
||||||
= configService.get<ISecurityConfig>('security')
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
secret: jwtSecret,
|
secret: jwtSecret,
|
||||||
expires: jwtExprire,
|
expires: jwtExprire,
|
||||||
ignoreExpiration: isDev,
|
ignoreExpiration: isDev,
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import { InjectRedis } from '@liaoliaots/nestjs-redis'
|
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import Redis from 'ioredis'
|
import Redis from 'ioredis';
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
import { BusinessException } from '~/common/exceptions/biz.exception';
|
||||||
|
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
import { genAuthPVKey, genAuthPermKey, genAuthTokenKey } from '~/helper/genRedisKey'
|
import { genAuthPVKey, genAuthPermKey, genAuthTokenKey } from '~/helper/genRedisKey';
|
||||||
|
|
||||||
import { UserService } from '~/modules/user/user.service'
|
import { UserService } from '~/modules/user/user.service';
|
||||||
|
|
||||||
import { md5 } from '~/utils'
|
import { md5 } from '~/utils';
|
||||||
|
|
||||||
import { LoginLogService } from '../system/log/services/login-log.service'
|
import { LoginLogService } from '../system/log/services/login-log.service';
|
||||||
import { MenuService } from '../system/menu/menu.service'
|
import { MenuService } from '../system/menu/menu.service';
|
||||||
import { RoleService } from '../system/role/role.service'
|
import { RoleService } from '../system/role/role.service';
|
||||||
|
|
||||||
import { TokenService } from './services/token.service'
|
import { TokenService } from './services/token.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
|
@ -27,130 +27,123 @@ export class AuthService {
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private loginLogService: LoginLogService,
|
private loginLogService: LoginLogService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async validateUser(credential: string, password: string): Promise<any> {
|
async validateUser(credential: string, password: string): Promise<any> {
|
||||||
const user = await this.userService.findUserByUserName(credential)
|
const user = await this.userService.findUserByUserName(credential);
|
||||||
|
|
||||||
if (isEmpty(user))
|
if (isEmpty(user)) throw new BusinessException(ErrorEnum.USER_NOT_FOUND);
|
||||||
throw new BusinessException(ErrorEnum.USER_NOT_FOUND)
|
|
||||||
|
|
||||||
const comparePassword = md5(`${password}${user.psalt}`)
|
const comparePassword = md5(`${password}${user.psalt}`);
|
||||||
if (user.password !== comparePassword)
|
if (user.password !== comparePassword)
|
||||||
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
|
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD);
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
const { password, ...result } = user
|
const { password, ...result } = user;
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取登录JWT
|
* 获取登录JWT
|
||||||
* 返回null则账号密码有误,不存在该用户
|
* 返回null则账号密码有误,不存在该用户
|
||||||
*/
|
*/
|
||||||
async login(
|
async login(username: string, password: string, ip: string, ua: string): Promise<string> {
|
||||||
username: string,
|
const user = await this.userService.findUserByUserName(username);
|
||||||
password: string,
|
if (isEmpty(user)) throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD);
|
||||||
ip: string,
|
|
||||||
ua: string,
|
|
||||||
): Promise<string> {
|
|
||||||
const user = await this.userService.findUserByUserName(username)
|
|
||||||
if (isEmpty(user))
|
|
||||||
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
|
|
||||||
|
|
||||||
const comparePassword = md5(`${password}${user.psalt}`)
|
const comparePassword = md5(`${password}${user.psalt}`);
|
||||||
if (user.password !== comparePassword)
|
if (user.password !== comparePassword)
|
||||||
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
|
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD);
|
||||||
|
|
||||||
const roleIds = await this.roleService.getRoleIdsByUser(user.id)
|
const roleIds = await this.roleService.getRoleIdsByUser(user.id);
|
||||||
|
|
||||||
const roles = await this.roleService.getRoleValues(roleIds)
|
const roles = await this.roleService.getRoleValues(roleIds);
|
||||||
|
|
||||||
// 包含access_token和refresh_token
|
// 包含access_token和refresh_token
|
||||||
const token = await this.tokenService.generateAccessToken(user.id, roles)
|
const token = await this.tokenService.generateAccessToken(user.id, roles);
|
||||||
|
|
||||||
await this.redis.set(genAuthTokenKey(user.id), token.accessToken)
|
await this.redis.set(genAuthTokenKey(user.id), token.accessToken);
|
||||||
|
|
||||||
// 设置密码版本号 当密码修改时,版本号+1
|
// 设置密码版本号 当密码修改时,版本号+1
|
||||||
await this.redis.set(genAuthPVKey(user.id), 1)
|
await this.redis.set(genAuthPVKey(user.id), 1);
|
||||||
|
|
||||||
// 设置菜单权限
|
// 设置菜单权限
|
||||||
const permissions = await this.menuService.getPermissions(user.id)
|
const permissions = await this.menuService.getPermissions(user.id);
|
||||||
await this.setPermissionsCache(user.id, permissions)
|
await this.setPermissionsCache(user.id, permissions);
|
||||||
|
|
||||||
await this.loginLogService.create(user.id, ip, ua)
|
await this.loginLogService.create(user.id, ip, ua);
|
||||||
|
|
||||||
return token.accessToken
|
return token.accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 效验账号密码
|
* 效验账号密码
|
||||||
*/
|
*/
|
||||||
async checkPassword(username: string, password: string) {
|
async checkPassword(username: string, password: string) {
|
||||||
const user = await this.userService.findUserByUserName(username)
|
const user = await this.userService.findUserByUserName(username);
|
||||||
|
|
||||||
const comparePassword = md5(`${password}${user.psalt}`)
|
const comparePassword = md5(`${password}${user.psalt}`);
|
||||||
if (user.password !== comparePassword)
|
if (user.password !== comparePassword)
|
||||||
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD)
|
throw new BusinessException(ErrorEnum.INVALID_USERNAME_PASSWORD);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loginLog(uid: number, ip: string, ua: string) {
|
async loginLog(uid: number, ip: string, ua: string) {
|
||||||
await this.loginLogService.create(uid, ip, ua)
|
await this.loginLogService.create(uid, ip, ua);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logout(uid: number) {
|
async logout(uid: number) {
|
||||||
// 删除token
|
// 删除token
|
||||||
await this.userService.forbidden(uid)
|
await this.userService.forbidden(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重置密码
|
* 重置密码
|
||||||
*/
|
*/
|
||||||
async resetPassword(username: string, password: string) {
|
async resetPassword(username: string, password: string) {
|
||||||
const user = await this.userService.findUserByUserName(username)
|
const user = await this.userService.findUserByUserName(username);
|
||||||
|
|
||||||
await this.userService.forceUpdatePassword(user.id, password)
|
await this.userService.forceUpdatePassword(user.id, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除登录状态信息
|
* 清除登录状态信息
|
||||||
*/
|
*/
|
||||||
async clearLoginStatus(uid: number): Promise<void> {
|
async clearLoginStatus(uid: number): Promise<void> {
|
||||||
await this.userService.forbidden(uid)
|
await this.userService.forbidden(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取菜单列表
|
* 获取菜单列表
|
||||||
*/
|
*/
|
||||||
async getMenus(uid: number): Promise<string[]> {
|
async getMenus(uid: number): Promise<string[]> {
|
||||||
return this.menuService.getMenus(uid)
|
return this.menuService.getMenus(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取权限列表
|
* 获取权限列表
|
||||||
*/
|
*/
|
||||||
async getPermissions(uid: number): Promise<string[]> {
|
async getPermissions(uid: number): Promise<string[]> {
|
||||||
return this.menuService.getPermissions(uid)
|
return this.menuService.getPermissions(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPermissionsCache(uid: number): Promise<string[]> {
|
async getPermissionsCache(uid: number): Promise<string[]> {
|
||||||
const permissionString = await this.redis.get(genAuthPermKey(uid))
|
const permissionString = await this.redis.get(genAuthPermKey(uid));
|
||||||
return permissionString ? JSON.parse(permissionString) : []
|
return permissionString ? JSON.parse(permissionString) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPermissionsCache(uid: number, permissions: string[]): Promise<void> {
|
async setPermissionsCache(uid: number, permissions: string[]): Promise<void> {
|
||||||
await this.redis.set(genAuthPermKey(uid), JSON.stringify(permissions))
|
await this.redis.set(genAuthPermKey(uid), JSON.stringify(permissions));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPasswordVersionByUid(uid: number): Promise<string> {
|
async getPasswordVersionByUid(uid: number): Promise<string> {
|
||||||
return this.redis.get(genAuthPVKey(uid))
|
return this.redis.get(genAuthPVKey(uid));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTokenByUid(uid: number): Promise<string> {
|
async getTokenByUid(uid: number): Promise<string> {
|
||||||
return this.redis.get(genAuthTokenKey(uid))
|
return this.redis.get(genAuthTokenKey(uid));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common'
|
import { Body, Controller, Get, Post, Put, UseGuards } from '@nestjs/common';
|
||||||
import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger'
|
import { ApiExtraModels, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { ApiResult } from '~/common/decorators/api-result.decorator'
|
import { ApiResult } from '~/common/decorators/api-result.decorator';
|
||||||
|
|
||||||
import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'
|
import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator';
|
||||||
import { AllowAnon } from '~/modules/auth/decorators/allow-anon.decorator'
|
import { AllowAnon } from '~/modules/auth/decorators/allow-anon.decorator';
|
||||||
import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'
|
import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator';
|
||||||
|
|
||||||
import { PasswordUpdateDto } from '~/modules/user/dto/password.dto'
|
import { PasswordUpdateDto } from '~/modules/user/dto/password.dto';
|
||||||
|
|
||||||
import { AccountInfo } from '../../user/user.model'
|
import { AccountInfo } from '../../user/user.model';
|
||||||
import { UserService } from '../../user/user.service'
|
import { UserService } from '../../user/user.service';
|
||||||
import { AuthService } from '../auth.service'
|
import { AuthService } from '../auth.service';
|
||||||
import { AccountMenus, AccountUpdateDto } from '../dto/account.dto'
|
import { AccountMenus, AccountUpdateDto } from '../dto/account.dto';
|
||||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard'
|
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@ApiTags('Account - 账户模块')
|
@ApiTags('Account - 账户模块')
|
||||||
@ApiSecurityAuth()
|
@ApiSecurityAuth()
|
||||||
|
@ -23,7 +23,7 @@ import { JwtAuthGuard } from '../guards/jwt-auth.guard'
|
||||||
export class AccountController {
|
export class AccountController {
|
||||||
constructor(
|
constructor(
|
||||||
private userService: UserService,
|
private userService: UserService,
|
||||||
private authService: AuthService,
|
private authService: AuthService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('profile')
|
@Get('profile')
|
||||||
|
@ -31,14 +31,14 @@ export class AccountController {
|
||||||
@ApiResult({ type: AccountInfo })
|
@ApiResult({ type: AccountInfo })
|
||||||
@AllowAnon()
|
@AllowAnon()
|
||||||
async profile(@AuthUser() user: IAuthUser): Promise<AccountInfo> {
|
async profile(@AuthUser() user: IAuthUser): Promise<AccountInfo> {
|
||||||
return this.userService.getAccountInfo(user.uid)
|
return this.userService.getAccountInfo(user.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('logout')
|
@Get('logout')
|
||||||
@ApiOperation({ summary: '账户登出' })
|
@ApiOperation({ summary: '账户登出' })
|
||||||
@AllowAnon()
|
@AllowAnon()
|
||||||
async logout(@AuthUser() user: IAuthUser): Promise<void> {
|
async logout(@AuthUser() user: IAuthUser): Promise<void> {
|
||||||
await this.authService.clearLoginStatus(user.uid)
|
await this.authService.clearLoginStatus(user.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('menus')
|
@Get('menus')
|
||||||
|
@ -46,7 +46,7 @@ export class AccountController {
|
||||||
@ApiResult({ type: [AccountMenus] })
|
@ApiResult({ type: [AccountMenus] })
|
||||||
@AllowAnon()
|
@AllowAnon()
|
||||||
async menu(@AuthUser() user: IAuthUser): Promise<string[]> {
|
async menu(@AuthUser() user: IAuthUser): Promise<string[]> {
|
||||||
return this.authService.getMenus(user.uid)
|
return this.authService.getMenus(user.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('permissions')
|
@Get('permissions')
|
||||||
|
@ -54,22 +54,20 @@ export class AccountController {
|
||||||
@ApiResult({ type: [String] })
|
@ApiResult({ type: [String] })
|
||||||
@AllowAnon()
|
@AllowAnon()
|
||||||
async permissions(@AuthUser() user: IAuthUser): Promise<string[]> {
|
async permissions(@AuthUser() user: IAuthUser): Promise<string[]> {
|
||||||
return this.authService.getPermissions(user.uid)
|
return this.authService.getPermissions(user.uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('update')
|
@Put('update')
|
||||||
@ApiOperation({ summary: '更改账户资料' })
|
@ApiOperation({ summary: '更改账户资料' })
|
||||||
@AllowAnon()
|
@AllowAnon()
|
||||||
async update(
|
async update(@AuthUser() user: IAuthUser, @Body() dto: AccountUpdateDto): Promise<void> {
|
||||||
@AuthUser() user: IAuthUser, @Body() dto: AccountUpdateDto): Promise<void> {
|
await this.userService.updateAccountInfo(user.uid, dto);
|
||||||
await this.userService.updateAccountInfo(user.uid, dto)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('password')
|
@Post('password')
|
||||||
@ApiOperation({ summary: '更改账户密码' })
|
@ApiOperation({ summary: '更改账户密码' })
|
||||||
@AllowAnon()
|
@AllowAnon()
|
||||||
async password(
|
async password(@AuthUser() user: IAuthUser, @Body() dto: PasswordUpdateDto): Promise<void> {
|
||||||
@AuthUser() user: IAuthUser, @Body() dto: PasswordUpdateDto): Promise<void> {
|
await this.userService.updatePassword(user.uid, dto);
|
||||||
await this.userService.updatePassword(user.uid, dto)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import { InjectRedis } from '@liaoliaots/nestjs-redis'
|
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||||
import { Controller, Get, Query } from '@nestjs/common'
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger'
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import Redis from 'ioredis'
|
import Redis from 'ioredis';
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash';
|
||||||
import * as svgCaptcha from 'svg-captcha'
|
import * as svgCaptcha from 'svg-captcha';
|
||||||
|
|
||||||
import { ApiResult } from '~/common/decorators/api-result.decorator'
|
import { ApiResult } from '~/common/decorators/api-result.decorator';
|
||||||
import { genCaptchaImgKey } from '~/helper/genRedisKey'
|
import { genCaptchaImgKey } from '~/helper/genRedisKey';
|
||||||
import { generateUUID } from '~/utils'
|
import { generateUUID } from '~/utils';
|
||||||
|
|
||||||
import { Public } from '../decorators/public.decorator'
|
import { Public } from '../decorators/public.decorator';
|
||||||
|
|
||||||
import { ImageCaptchaDto } from '../dto/captcha.dto'
|
import { ImageCaptchaDto } from '../dto/captcha.dto';
|
||||||
import { ImageCaptcha } from '../models/auth.model'
|
import { ImageCaptcha } from '../models/auth.model';
|
||||||
|
|
||||||
@ApiTags('Captcha - 验证码模块')
|
@ApiTags('Captcha - 验证码模块')
|
||||||
// @UseGuards(ThrottlerGuard)
|
// @UseGuards(ThrottlerGuard)
|
||||||
|
@ -27,7 +27,7 @@ export class CaptchaController {
|
||||||
@Public()
|
@Public()
|
||||||
// @Throttle({ default: { limit: 2, ttl: 600000 } })
|
// @Throttle({ default: { limit: 2, ttl: 600000 } })
|
||||||
async captchaByImg(@Query() dto: ImageCaptchaDto): Promise<ImageCaptcha> {
|
async captchaByImg(@Query() dto: ImageCaptchaDto): Promise<ImageCaptcha> {
|
||||||
const { width, height } = dto
|
const { width, height } = dto;
|
||||||
|
|
||||||
const svg = svgCaptcha.create({
|
const svg = svgCaptcha.create({
|
||||||
size: 4,
|
size: 4,
|
||||||
|
@ -36,15 +36,13 @@ export class CaptchaController {
|
||||||
width: isEmpty(width) ? 100 : width,
|
width: isEmpty(width) ? 100 : width,
|
||||||
height: isEmpty(height) ? 50 : height,
|
height: isEmpty(height) ? 50 : height,
|
||||||
charPreset: '1234567890',
|
charPreset: '1234567890',
|
||||||
})
|
});
|
||||||
const result = {
|
const result = {
|
||||||
img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString(
|
img: `data:image/svg+xml;base64,${Buffer.from(svg.data).toString('base64')}`,
|
||||||
'base64',
|
|
||||||
)}`,
|
|
||||||
id: generateUUID(),
|
id: generateUUID(),
|
||||||
}
|
};
|
||||||
// 5分钟过期时间
|
// 5分钟过期时间
|
||||||
await this.redis.set(genCaptchaImgKey(result.id), svg.text, 'EX', 60 * 5)
|
await this.redis.set(genCaptchaImgKey(result.id), svg.text, 'EX', 60 * 5);
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common'
|
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger'
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { Throttle, ThrottlerGuard } from '@nestjs/throttler'
|
import { Throttle, ThrottlerGuard } from '@nestjs/throttler';
|
||||||
|
|
||||||
import { Ip } from '~/common/decorators/http.decorator'
|
import { Ip } from '~/common/decorators/http.decorator';
|
||||||
|
|
||||||
import { MailerService } from '~/shared/mailer/mailer.service'
|
import { MailerService } from '~/shared/mailer/mailer.service';
|
||||||
|
|
||||||
import { Public } from '../decorators/public.decorator'
|
import { Public } from '../decorators/public.decorator';
|
||||||
|
|
||||||
import { SendEmailCodeDto } from '../dto/captcha.dto'
|
import { SendEmailCodeDto } from '../dto/captcha.dto';
|
||||||
|
|
||||||
@ApiTags('Auth - 认证模块')
|
@ApiTags('Auth - 认证模块')
|
||||||
@UseGuards(ThrottlerGuard)
|
@UseGuards(ThrottlerGuard)
|
||||||
|
@ -21,17 +21,14 @@ export class EmailController {
|
||||||
@ApiOperation({ summary: '发送邮箱验证码' })
|
@ApiOperation({ summary: '发送邮箱验证码' })
|
||||||
@Public()
|
@Public()
|
||||||
@Throttle({ default: { limit: 2, ttl: 600000 } })
|
@Throttle({ default: { limit: 2, ttl: 600000 } })
|
||||||
async sendEmailCode(
|
async sendEmailCode(@Body() dto: SendEmailCodeDto, @Ip() ip: string): Promise<void> {
|
||||||
@Body() dto: SendEmailCodeDto,
|
|
||||||
@Ip() ip: string,
|
|
||||||
): Promise<void> {
|
|
||||||
// await this.authService.checkImgCaptcha(dto.captchaId, dto.verifyCode);
|
// await this.authService.checkImgCaptcha(dto.captchaId, dto.verifyCode);
|
||||||
const { email } = dto
|
const { email } = dto;
|
||||||
|
|
||||||
await this.mailerService.checkLimit(email, ip)
|
await this.mailerService.checkLimit(email, ip);
|
||||||
const { code } = await this.mailerService.sendVerificationCode(email)
|
const { code } = await this.mailerService.sendVerificationCode(email);
|
||||||
|
|
||||||
await this.mailerService.log(email, code, ip)
|
await this.mailerService.log(email, code, ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Post()
|
// @Post()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { SetMetadata } from '@nestjs/common'
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
import { ALLOW_ANON_KEY } from '../auth.constant'
|
import { ALLOW_ANON_KEY } from '../auth.constant';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当接口不需要检测用户是否具有操作权限时添加该装饰器
|
* 当接口不需要检测用户是否具有操作权限时添加该装饰器
|
||||||
*/
|
*/
|
||||||
export const AllowAnon = () => SetMetadata(ALLOW_ANON_KEY, true)
|
export const AllowAnon = () => SetMetadata(ALLOW_ANON_KEY, true);
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
import { ExecutionContext, createParamDecorator } from '@nestjs/common'
|
import { ExecutionContext, createParamDecorator } from '@nestjs/common';
|
||||||
import { FastifyRequest } from 'fastify'
|
import { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
type Payload = keyof IAuthUser
|
type Payload = keyof IAuthUser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取当前登录用户信息, 并挂载到request上
|
* @description 获取当前登录用户信息, 并挂载到request上
|
||||||
*/
|
*/
|
||||||
export const AuthUser = createParamDecorator(
|
export const AuthUser = createParamDecorator((data: Payload, ctx: ExecutionContext) => {
|
||||||
(data: Payload, ctx: ExecutionContext) => {
|
const request = ctx.switchToHttp().getRequest<FastifyRequest>();
|
||||||
const request = ctx.switchToHttp().getRequest<FastifyRequest>()
|
|
||||||
// auth guard will mount this
|
// auth guard will mount this
|
||||||
const user = request.user as IAuthUser
|
const user = request.user as IAuthUser;
|
||||||
|
|
||||||
return data ? user?.[data] : user
|
return data ? user?.[data] : user;
|
||||||
},
|
});
|
||||||
)
|
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import { SetMetadata, applyDecorators } from '@nestjs/common'
|
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||||
|
|
||||||
import { isPlainObject } from 'lodash'
|
import { isPlainObject } from 'lodash';
|
||||||
|
|
||||||
import { PERMISSION_KEY } from '../auth.constant'
|
import { PERMISSION_KEY } from '../auth.constant';
|
||||||
|
|
||||||
type TupleToObject<T extends string, P extends ReadonlyArray<string>> = {
|
type TupleToObject<T extends string, P extends ReadonlyArray<string>> = {
|
||||||
[K in Uppercase<P[number]>]: `${T}:${Lowercase<K>}`
|
[K in Uppercase<P[number]>]: `${T}:${Lowercase<K>}`;
|
||||||
}
|
};
|
||||||
type AddPrefixToObjectValue<T extends string, P extends Record<string, string>> = {
|
type AddPrefixToObjectValue<T extends string, P extends Record<string, string>> = {
|
||||||
[K in keyof P]: K extends string ? `${T}:${P[K]}` : never
|
[K in keyof P]: K extends string ? `${T}:${P[K]}` : never;
|
||||||
}
|
};
|
||||||
|
|
||||||
/** 资源操作需要特定的权限 */
|
/** 资源操作需要特定的权限 */
|
||||||
export function Perm(permission: string | string[]) {
|
export function Perm(permission: string | string[]) {
|
||||||
return applyDecorators(SetMetadata(PERMISSION_KEY, permission))
|
return applyDecorators(SetMetadata(PERMISSION_KEY, permission));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** (此举非必需)保存通过 definePermission 定义的所有权限,可用于前端开发人员开发阶段的 ts 类型提示,避免前端权限定义与后端定义不匹配 */
|
/** (此举非必需)保存通过 definePermission 定义的所有权限,可用于前端开发人员开发阶段的 ts 类型提示,避免前端权限定义与后端定义不匹配 */
|
||||||
let permissions: string[] = []
|
let permissions: string[] = [];
|
||||||
/**
|
/**
|
||||||
* 定义权限,同时收集所有被定义的权限
|
* 定义权限,同时收集所有被定义的权限
|
||||||
*
|
*
|
||||||
|
@ -33,26 +33,31 @@ let permissions: string[] = []
|
||||||
* definePermission('app:health', ['network']);
|
* definePermission('app:health', ['network']);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export function definePermission<T extends string, U extends Record<string, string>>(modulePrefix: T, actionMap: U): AddPrefixToObjectValue<T, U>
|
export function definePermission<T extends string, U extends Record<string, string>>(
|
||||||
export function definePermission<T extends string, U extends ReadonlyArray<string>>(modulePrefix: T, actions: U): TupleToObject<T, U>
|
modulePrefix: T,
|
||||||
|
actionMap: U
|
||||||
|
): AddPrefixToObjectValue<T, U>;
|
||||||
|
export function definePermission<T extends string, U extends ReadonlyArray<string>>(
|
||||||
|
modulePrefix: T,
|
||||||
|
actions: U
|
||||||
|
): TupleToObject<T, U>;
|
||||||
export function definePermission(modulePrefix: string, actions) {
|
export function definePermission(modulePrefix: string, actions) {
|
||||||
if (isPlainObject(actions)) {
|
if (isPlainObject(actions)) {
|
||||||
Object.entries(actions).forEach(([key, action]) => {
|
Object.entries(actions).forEach(([key, action]) => {
|
||||||
actions[key] = `${modulePrefix}:${action}`
|
actions[key] = `${modulePrefix}:${action}`;
|
||||||
})
|
});
|
||||||
permissions = [...new Set([...permissions, ...Object.values<string>(actions)])]
|
permissions = [...new Set([...permissions, ...Object.values<string>(actions)])];
|
||||||
return actions
|
return actions;
|
||||||
}
|
} else if (Array.isArray(actions)) {
|
||||||
else if (Array.isArray(actions)) {
|
const permissionFormats = actions.map(action => `${modulePrefix}:${action}`);
|
||||||
const permissionFormats = actions.map(action => `${modulePrefix}:${action}`)
|
permissions = [...new Set([...permissions, ...permissionFormats])];
|
||||||
permissions = [...new Set([...permissions, ...permissionFormats])]
|
|
||||||
|
|
||||||
return actions.reduce((prev, action) => {
|
return actions.reduce((prev, action) => {
|
||||||
prev[action.toUpperCase()] = `${modulePrefix}:${action}`
|
prev[action.toUpperCase()] = `${modulePrefix}:${action}`;
|
||||||
return prev
|
return prev;
|
||||||
}, {})
|
}, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取所有通过 definePermission 定义的权限 */
|
/** 获取所有通过 definePermission 定义的权限 */
|
||||||
export const getDefinePermissions = () => permissions
|
export const getDefinePermissions = () => permissions;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { SetMetadata } from '@nestjs/common'
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
import { PUBLIC_KEY } from '../auth.constant'
|
import { PUBLIC_KEY } from '../auth.constant';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当接口不需要检测用户登录时添加该装饰器
|
* 当接口不需要检测用户登录时添加该装饰器
|
||||||
*/
|
*/
|
||||||
export const Public = () => SetMetadata(PUBLIC_KEY, true)
|
export const Public = () => SetMetadata(PUBLIC_KEY, true);
|
||||||
|
|
|
@ -1,12 +1,22 @@
|
||||||
import { SetMetadata, applyDecorators } from '@nestjs/common'
|
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||||
|
|
||||||
import { ObjectLiteral, ObjectType, Repository } from 'typeorm'
|
import { ObjectLiteral, ObjectType, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { RESOURCE_KEY } from '../auth.constant'
|
import { RESOURCE_KEY } from '../auth.constant';
|
||||||
|
|
||||||
export type Condition<E extends ObjectLiteral = any> = (Repository: Repository<E>, items: number[], user: IAuthUser) => Promise<boolean>
|
export type Condition<E extends ObjectLiteral = any> = (
|
||||||
|
Repository: Repository<E>,
|
||||||
|
items: number[],
|
||||||
|
user: IAuthUser
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
export interface ResourceObject { entity: ObjectType<any>, condition: Condition }
|
export interface ResourceObject {
|
||||||
export function Resource<E extends ObjectLiteral = any>(entity: ObjectType<E>, condition?: Condition<E>) {
|
entity: ObjectType<any>;
|
||||||
return applyDecorators(SetMetadata(RESOURCE_KEY, { entity, condition }))
|
condition: Condition;
|
||||||
|
}
|
||||||
|
export function Resource<E extends ObjectLiteral = any>(
|
||||||
|
entity: ObjectType<E>,
|
||||||
|
condition?: Condition<E>
|
||||||
|
) {
|
||||||
|
return applyDecorators(SetMetadata(RESOURCE_KEY, { entity, condition }));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,17 @@
|
||||||
import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger'
|
import { ApiProperty, OmitType, PartialType, PickType } from '@nestjs/swagger';
|
||||||
import {
|
import { IsEmail, IsOptional, IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
||||||
IsEmail,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
Matches,
|
|
||||||
MaxLength,
|
|
||||||
MinLength,
|
|
||||||
} from 'class-validator'
|
|
||||||
|
|
||||||
import { MenuEntity } from '~/modules/system/menu/menu.entity'
|
import { MenuEntity } from '~/modules/system/menu/menu.entity';
|
||||||
|
|
||||||
export class AccountUpdateDto {
|
export class AccountUpdateDto {
|
||||||
@ApiProperty({ description: '用户呢称' })
|
@ApiProperty({ description: '用户呢称' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
nickname: string
|
nickname: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '用户邮箱' })
|
@ApiProperty({ description: '用户邮箱' })
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string
|
email: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '用户QQ' })
|
@ApiProperty({ description: '用户QQ' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
@ -26,39 +19,54 @@ export class AccountUpdateDto {
|
||||||
@Matches(/^[0-9]+$/)
|
@Matches(/^[0-9]+$/)
|
||||||
@MinLength(5)
|
@MinLength(5)
|
||||||
@MaxLength(11)
|
@MaxLength(11)
|
||||||
qq: string
|
qq: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '用户手机号' })
|
@ApiProperty({ description: '用户手机号' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
phone: string
|
phone: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '用户头像' })
|
@ApiProperty({ description: '用户头像' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
avatar: string
|
avatar: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '用户备注' })
|
@ApiProperty({ description: '用户备注' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
remark: string
|
remark: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ResetPasswordDto {
|
export class ResetPasswordDto {
|
||||||
@ApiProperty({ description: '临时token', example: 'uuid' })
|
@ApiProperty({ description: '临时token', example: 'uuid' })
|
||||||
@IsString()
|
@IsString()
|
||||||
accessToken: string
|
accessToken: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '密码', example: 'a123456' })
|
@ApiProperty({ description: '密码', example: 'a123456' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
|
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
|
||||||
@MinLength(6)
|
@MinLength(6)
|
||||||
password: string
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MenuMeta extends PartialType(OmitType(MenuEntity, ['parentId', 'createdAt', 'updatedAt', 'id', 'roles', 'path', 'name'] as const)) {
|
export class MenuMeta extends PartialType(
|
||||||
title: string
|
OmitType(MenuEntity, [
|
||||||
|
'parentId',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
'id',
|
||||||
|
'roles',
|
||||||
|
'path',
|
||||||
|
'name',
|
||||||
|
] as const)
|
||||||
|
) {
|
||||||
|
title: string;
|
||||||
}
|
}
|
||||||
export class AccountMenus extends PickType(MenuEntity, ['id', 'path', 'name', 'component'] as const) {
|
export class AccountMenus extends PickType(MenuEntity, [
|
||||||
meta: MenuMeta
|
'id',
|
||||||
|
'path',
|
||||||
|
'name',
|
||||||
|
'component',
|
||||||
|
] as const) {
|
||||||
|
meta: MenuMeta;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +1,43 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { IsString, Matches, MaxLength, MinLength } from 'class-validator'
|
import { IsString, Matches, MaxLength, MinLength } from 'class-validator';
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@ApiProperty({ description: '手机号/邮箱' })
|
@ApiProperty({ description: '手机号/邮箱' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(4)
|
@MinLength(4)
|
||||||
username: string
|
username: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '密码', example: 'a123456' })
|
@ApiProperty({ description: '密码', example: 'a123456' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
|
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
|
||||||
@MinLength(6)
|
@MinLength(6)
|
||||||
password: string
|
password: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '验证码标识' })
|
@ApiProperty({ description: '验证码标识' })
|
||||||
@IsString()
|
@IsString()
|
||||||
captchaId: string
|
captchaId: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '用户输入的验证码' })
|
@ApiProperty({ description: '用户输入的验证码' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(4)
|
@MinLength(4)
|
||||||
@MaxLength(4)
|
@MaxLength(4)
|
||||||
verifyCode: string
|
verifyCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RegisterDto {
|
export class RegisterDto {
|
||||||
@ApiProperty({ description: '账号' })
|
@ApiProperty({ description: '账号' })
|
||||||
@IsString()
|
@IsString()
|
||||||
username: string
|
username: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '密码' })
|
@ApiProperty({ description: '密码' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
|
@Matches(/^\S*(?=\S{6,})(?=\S*\d)(?=\S*[A-Za-z])\S*$/)
|
||||||
@MinLength(6)
|
@MinLength(6)
|
||||||
@MaxLength(16)
|
@MaxLength(16)
|
||||||
password: string
|
password: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '语言', examples: ['EN', 'ZH'] })
|
@ApiProperty({ description: '语言', examples: ['EN', 'ZH'] })
|
||||||
@IsString()
|
@IsString()
|
||||||
lang: string
|
lang: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer'
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import { IsEmail, IsInt, IsMobilePhone, IsOptional, IsString } from 'class-validator';
|
||||||
IsEmail,
|
|
||||||
IsInt,
|
|
||||||
IsMobilePhone,
|
|
||||||
IsOptional,
|
|
||||||
IsString,
|
|
||||||
} from 'class-validator'
|
|
||||||
|
|
||||||
export class ImageCaptchaDto {
|
export class ImageCaptchaDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
|
@ -17,7 +11,7 @@ export class ImageCaptchaDto {
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
readonly width: number = 100
|
readonly width: number = 100;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -27,27 +21,27 @@ export class ImageCaptchaDto {
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
readonly height: number = 50
|
readonly height: number = 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendEmailCodeDto {
|
export class SendEmailCodeDto {
|
||||||
@ApiProperty({ description: '邮箱' })
|
@ApiProperty({ description: '邮箱' })
|
||||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||||
email: string
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SendSmsCodeDto {
|
export class SendSmsCodeDto {
|
||||||
@ApiProperty({ description: '手机号' })
|
@ApiProperty({ description: '手机号' })
|
||||||
@IsMobilePhone('zh-CN', {}, { message: '手机号格式不正确' })
|
@IsMobilePhone('zh-CN', {}, { message: '手机号格式不正确' })
|
||||||
phone: string
|
phone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CheckCodeDto {
|
export class CheckCodeDto {
|
||||||
@ApiProperty({ description: '手机号/邮箱' })
|
@ApiProperty({ description: '手机号/邮箱' })
|
||||||
@IsString()
|
@IsString()
|
||||||
account: string
|
account: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '验证码' })
|
@ApiProperty({ description: '验证码' })
|
||||||
@IsString()
|
@IsString()
|
||||||
code: string
|
code: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,34 +7,34 @@ import {
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm'
|
} from 'typeorm';
|
||||||
|
|
||||||
import { UserEntity } from '~/modules/user/user.entity'
|
import { UserEntity } from '~/modules/user/user.entity';
|
||||||
|
|
||||||
import { RefreshTokenEntity } from './refresh-token.entity'
|
import { RefreshTokenEntity } from './refresh-token.entity';
|
||||||
|
|
||||||
@Entity('user_access_tokens')
|
@Entity('user_access_tokens')
|
||||||
export class AccessTokenEntity extends BaseEntity {
|
export class AccessTokenEntity extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string
|
id!: string;
|
||||||
|
|
||||||
@Column({ length: 500 })
|
@Column({ length: 500 })
|
||||||
value!: string
|
value!: string;
|
||||||
|
|
||||||
@Column({ comment: '令牌过期时间' })
|
@Column({ comment: '令牌过期时间' })
|
||||||
expired_at!: Date
|
expired_at!: Date;
|
||||||
|
|
||||||
@CreateDateColumn({ comment: '令牌创建时间' })
|
@CreateDateColumn({ comment: '令牌创建时间' })
|
||||||
created_at!: Date
|
created_at!: Date;
|
||||||
|
|
||||||
@OneToOne(() => RefreshTokenEntity, refreshToken => refreshToken.accessToken, {
|
@OneToOne(() => RefreshTokenEntity, refreshToken => refreshToken.accessToken, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
})
|
})
|
||||||
refreshToken!: RefreshTokenEntity
|
refreshToken!: RefreshTokenEntity;
|
||||||
|
|
||||||
@ManyToOne(() => UserEntity, user => user.accessTokens, {
|
@ManyToOne(() => UserEntity, user => user.accessTokens, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn({ name: 'user_id' })
|
@JoinColumn({ name: 'user_id' })
|
||||||
user!: UserEntity
|
user!: UserEntity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,27 +6,27 @@ import {
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
OneToOne,
|
OneToOne,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
} from 'typeorm'
|
} from 'typeorm';
|
||||||
|
|
||||||
import { AccessTokenEntity } from './access-token.entity'
|
import { AccessTokenEntity } from './access-token.entity';
|
||||||
|
|
||||||
@Entity('user_refresh_tokens')
|
@Entity('user_refresh_tokens')
|
||||||
export class RefreshTokenEntity extends BaseEntity {
|
export class RefreshTokenEntity extends BaseEntity {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string
|
id!: string;
|
||||||
|
|
||||||
@Column({ length: 500 })
|
@Column({ length: 500 })
|
||||||
value!: string
|
value!: string;
|
||||||
|
|
||||||
@Column({ comment: '令牌过期时间' })
|
@Column({ comment: '令牌过期时间' })
|
||||||
expired_at!: Date
|
expired_at!: Date;
|
||||||
|
|
||||||
@CreateDateColumn({ comment: '令牌创建时间' })
|
@CreateDateColumn({ comment: '令牌创建时间' })
|
||||||
created_at!: Date
|
created_at!: Date;
|
||||||
|
|
||||||
@OneToOne(() => AccessTokenEntity, accessToken => accessToken.refreshToken, {
|
@OneToOne(() => AccessTokenEntity, accessToken => accessToken.refreshToken, {
|
||||||
onDelete: 'CASCADE',
|
onDelete: 'CASCADE',
|
||||||
})
|
})
|
||||||
@JoinColumn()
|
@JoinColumn()
|
||||||
accessToken!: AccessTokenEntity
|
accessToken!: AccessTokenEntity;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,17 @@
|
||||||
import {
|
import { ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
ExecutionContext,
|
import { Reflector } from '@nestjs/core';
|
||||||
Injectable,
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
UnauthorizedException,
|
import { FastifyRequest } from 'fastify';
|
||||||
} from '@nestjs/common'
|
import { isEmpty, isNil } from 'lodash';
|
||||||
import { Reflector } from '@nestjs/core'
|
|
||||||
import { AuthGuard } from '@nestjs/passport'
|
|
||||||
import { FastifyRequest } from 'fastify'
|
|
||||||
import { isEmpty, isNil } from 'lodash'
|
|
||||||
|
|
||||||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
import { BusinessException } from '~/common/exceptions/biz.exception';
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
import { AuthService } from '~/modules/auth/auth.service'
|
import { AuthService } from '~/modules/auth/auth.service';
|
||||||
|
|
||||||
import { checkIsDemoMode } from '~/utils'
|
import { checkIsDemoMode } from '~/utils';
|
||||||
|
|
||||||
import { AuthStrategy, PUBLIC_KEY } from '../auth.constant'
|
import { AuthStrategy, PUBLIC_KEY } from '../auth.constant';
|
||||||
import { TokenService } from '../services/token.service'
|
import { TokenService } from '../services/token.service';
|
||||||
|
|
||||||
// https://docs.nestjs.com/recipes/passport#implement-protected-route-and-jwt-strategy-guards
|
// https://docs.nestjs.com/recipes/passport#implement-protected-route-and-jwt-strategy-guards
|
||||||
@Injectable()
|
@Injectable()
|
||||||
|
@ -23,66 +19,60 @@ export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) {
|
||||||
constructor(
|
constructor(
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService
|
||||||
) {
|
) {
|
||||||
super()
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<any> {
|
async canActivate(context: ExecutionContext): Promise<any> {
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_KEY, [
|
const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_KEY, [
|
||||||
context.getHandler(),
|
context.getHandler(),
|
||||||
context.getClass(),
|
context.getClass(),
|
||||||
])
|
]);
|
||||||
const request = context.switchToHttp().getRequest<FastifyRequest>()
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
// const response = context.switchToHttp().getResponse<FastifyReply>()
|
// const response = context.switchToHttp().getResponse<FastifyReply>()
|
||||||
|
|
||||||
// TODO 此处代码的作用是判断如果在演示环境下,则拒绝用户的增删改操作,去掉此代码不影响正常的业务逻辑
|
// TODO 此处代码的作用是判断如果在演示环境下,则拒绝用户的增删改操作,去掉此代码不影响正常的业务逻辑
|
||||||
if (request.method !== 'GET' && !request.url.includes('/auth/login'))
|
if (request.method !== 'GET' && !request.url.includes('/auth/login')) checkIsDemoMode();
|
||||||
checkIsDemoMode()
|
|
||||||
|
|
||||||
const isSse = request.headers.accept === 'text/event-stream'
|
const isSse = request.headers.accept === 'text/event-stream';
|
||||||
|
|
||||||
if (isSse && !request.headers.authorization?.startsWith('Bearer')) {
|
if (isSse && !request.headers.authorization?.startsWith('Bearer')) {
|
||||||
const { token } = request.query as Record<string, string>
|
const { token } = request.query as Record<string, string>;
|
||||||
if (token)
|
if (token) request.headers.authorization = `Bearer ${token}`;
|
||||||
request.headers.authorization = `Bearer ${token}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Authorization = request.headers.authorization
|
const Authorization = request.headers.authorization;
|
||||||
|
|
||||||
let result: any = false
|
let result: any = false;
|
||||||
try {
|
try {
|
||||||
result = await super.canActivate(context)
|
result = await super.canActivate(context);
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
|
||||||
// 需要后置判断 这样携带了 token 的用户就能够解析到 request.user
|
// 需要后置判断 这样携带了 token 的用户就能够解析到 request.user
|
||||||
if (isPublic)
|
if (isPublic) return true;
|
||||||
return true
|
|
||||||
|
|
||||||
if (isEmpty(Authorization))
|
if (isEmpty(Authorization)) throw new UnauthorizedException('未登录');
|
||||||
throw new UnauthorizedException('未登录')
|
|
||||||
|
|
||||||
// 判断 token 是否存在, 如果不存在则认证失败
|
// 判断 token 是否存在, 如果不存在则认证失败
|
||||||
const accessToken = isNil(Authorization)
|
const accessToken = isNil(Authorization)
|
||||||
? undefined
|
? undefined
|
||||||
: await this.tokenService.checkAccessToken(Authorization!)
|
: await this.tokenService.checkAccessToken(Authorization!);
|
||||||
|
|
||||||
if (!accessToken)
|
if (!accessToken) throw new UnauthorizedException('令牌无效');
|
||||||
throw new UnauthorizedException('令牌无效')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSE 请求
|
// SSE 请求
|
||||||
if (isSse) {
|
if (isSse) {
|
||||||
const { uid } = request.params as Record<string, any>
|
const { uid } = request.params as Record<string, any>;
|
||||||
|
|
||||||
if (Number(uid) !== request.user.uid)
|
if (Number(uid) !== request.user.uid)
|
||||||
throw new UnauthorizedException('路径参数 uid 与当前 token 登录的用户 uid 不一致')
|
throw new UnauthorizedException('路径参数 uid 与当前 token 登录的用户 uid 不一致');
|
||||||
}
|
}
|
||||||
|
|
||||||
const pv = await this.authService.getPasswordVersionByUid(request.user.uid)
|
const pv = await this.authService.getPasswordVersionByUid(request.user.uid);
|
||||||
if (pv !== `${request.user.pv}`) {
|
if (pv !== `${request.user.pv}`) {
|
||||||
// 密码版本不一致,登录期间已更改过密码
|
// 密码版本不一致,登录期间已更改过密码
|
||||||
throw new BusinessException(ErrorEnum.INVALID_LOGIN)
|
throw new BusinessException(ErrorEnum.INVALID_LOGIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 不允许多端登录
|
// 不允许多端登录
|
||||||
|
@ -92,14 +82,13 @@ export class JwtAuthGuard extends AuthGuard(AuthStrategy.JWT) {
|
||||||
// throw new ApiException(ErrorEnum.CODE_1106);
|
// throw new ApiException(ErrorEnum.CODE_1106);
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return result
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleRequest(err, user, info) {
|
handleRequest(err, user, info) {
|
||||||
// You can throw an exception based on either "info" or "err" arguments
|
// You can throw an exception based on either "info" or "err" arguments
|
||||||
if (err || !user)
|
if (err || !user) throw err || new UnauthorizedException();
|
||||||
throw err || new UnauthorizedException()
|
|
||||||
|
|
||||||
return user
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { ExecutionContext, Injectable } from '@nestjs/common'
|
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { AuthGuard } from '@nestjs/passport'
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
import { AuthStrategy } from '../auth.constant'
|
import { AuthStrategy } from '../auth.constant';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalGuard extends AuthGuard(AuthStrategy.LOCAL) {
|
export class LocalGuard extends AuthGuard(AuthStrategy.LOCAL) {
|
||||||
async canActivate(context: ExecutionContext) {
|
async canActivate(context: ExecutionContext) {
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +1,64 @@
|
||||||
import {
|
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
CanActivate,
|
import { Reflector } from '@nestjs/core';
|
||||||
ExecutionContext,
|
import { FastifyRequest } from 'fastify';
|
||||||
Injectable,
|
|
||||||
UnauthorizedException,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import { Reflector } from '@nestjs/core'
|
|
||||||
import { FastifyRequest } from 'fastify'
|
|
||||||
|
|
||||||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
import { BusinessException } from '~/common/exceptions/biz.exception';
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
import { AuthService } from '~/modules/auth/auth.service'
|
import { AuthService } from '~/modules/auth/auth.service';
|
||||||
|
|
||||||
import { ALLOW_ANON_KEY, PERMISSION_KEY, PUBLIC_KEY, Roles } from '../auth.constant'
|
import { ALLOW_ANON_KEY, PERMISSION_KEY, PUBLIC_KEY, Roles } from '../auth.constant';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RbacGuard implements CanActivate {
|
export class RbacGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private authService: AuthService,
|
private authService: AuthService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<any> {
|
async canActivate(context: ExecutionContext): Promise<any> {
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_KEY, [
|
const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_KEY, [
|
||||||
context.getHandler(),
|
context.getHandler(),
|
||||||
context.getClass(),
|
context.getClass(),
|
||||||
])
|
]);
|
||||||
|
|
||||||
if (isPublic)
|
if (isPublic) return true;
|
||||||
return true
|
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest<FastifyRequest>()
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
|
|
||||||
const { user } = request
|
const { user } = request;
|
||||||
if (!user)
|
if (!user) throw new UnauthorizedException('登录无效');
|
||||||
throw new UnauthorizedException('登录无效')
|
|
||||||
|
|
||||||
// allowAnon 是需要登录后可访问(无需权限), Public 则是无需登录也可访问.
|
// allowAnon 是需要登录后可访问(无需权限), Public 则是无需登录也可访问.
|
||||||
const allowAnon = this.reflector.get<boolean>(
|
const allowAnon = this.reflector.get<boolean>(ALLOW_ANON_KEY, context.getHandler());
|
||||||
ALLOW_ANON_KEY,
|
if (allowAnon) return true;
|
||||||
context.getHandler(),
|
|
||||||
)
|
|
||||||
if (allowAnon)
|
|
||||||
return true
|
|
||||||
|
|
||||||
const payloadPermission = this.reflector.getAllAndOverride<
|
const payloadPermission = this.reflector.getAllAndOverride<string | string[]>(PERMISSION_KEY, [
|
||||||
string | string[]
|
context.getHandler(),
|
||||||
>(PERMISSION_KEY, [context.getHandler(), context.getClass()])
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
// 控制器没有设置接口权限,则默认通过
|
// 控制器没有设置接口权限,则默认通过
|
||||||
if (!payloadPermission)
|
if (!payloadPermission) return true;
|
||||||
return true
|
|
||||||
|
|
||||||
// 管理员放开所有权限
|
// 管理员放开所有权限
|
||||||
if (user.roles.includes(Roles.ADMIN))
|
if (user.roles.includes(Roles.ADMIN)) return true;
|
||||||
return true
|
|
||||||
|
|
||||||
const allPermissions = await this.authService.getPermissionsCache(user.uid) ?? await this.authService.getPermissions(user.uid)
|
const allPermissions =
|
||||||
|
(await this.authService.getPermissionsCache(user.uid)) ??
|
||||||
|
(await this.authService.getPermissions(user.uid));
|
||||||
// console.log(allPermissions)
|
// console.log(allPermissions)
|
||||||
let canNext = false
|
let canNext = false;
|
||||||
|
|
||||||
// handle permission strings
|
// handle permission strings
|
||||||
if (Array.isArray(payloadPermission)) {
|
if (Array.isArray(payloadPermission)) {
|
||||||
// 只要有一个权限满足即可
|
// 只要有一个权限满足即可
|
||||||
canNext = payloadPermission.every(i => allPermissions.includes(i))
|
canNext = payloadPermission.every(i => allPermissions.includes(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof payloadPermission === 'string')
|
if (typeof payloadPermission === 'string') canNext = allPermissions.includes(payloadPermission);
|
||||||
canNext = allPermissions.includes(payloadPermission)
|
|
||||||
|
|
||||||
if (!canNext)
|
if (!canNext) throw new BusinessException(ErrorEnum.NO_PERMISSION);
|
||||||
throw new BusinessException(ErrorEnum.NO_PERMISSION)
|
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,72 +1,67 @@
|
||||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
|
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||||
import { Reflector } from '@nestjs/core'
|
import { Reflector } from '@nestjs/core';
|
||||||
import { FastifyRequest } from 'fastify'
|
import { FastifyRequest } from 'fastify';
|
||||||
|
|
||||||
import { isArray, isEmpty, isNil } from 'lodash'
|
import { isArray, isEmpty, isNil } from 'lodash';
|
||||||
|
|
||||||
import { DataSource, In, Repository } from 'typeorm'
|
import { DataSource, In, Repository } from 'typeorm';
|
||||||
|
|
||||||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
import { BusinessException } from '~/common/exceptions/biz.exception';
|
||||||
|
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
|
|
||||||
import { PUBLIC_KEY, RESOURCE_KEY, Roles } from '../auth.constant'
|
import { PUBLIC_KEY, RESOURCE_KEY, Roles } from '../auth.constant';
|
||||||
import { ResourceObject } from '../decorators/resource.decorator'
|
import { ResourceObject } from '../decorators/resource.decorator';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ResourceGuard implements CanActivate {
|
export class ResourceGuard implements CanActivate {
|
||||||
constructor(
|
constructor(
|
||||||
private reflector: Reflector,
|
private reflector: Reflector,
|
||||||
private dataSource: DataSource,
|
private dataSource: DataSource
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async canActivate(context: ExecutionContext): Promise<any> {
|
async canActivate(context: ExecutionContext): Promise<any> {
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_KEY, [
|
const isPublic = this.reflector.getAllAndOverride<boolean>(PUBLIC_KEY, [
|
||||||
context.getHandler(),
|
context.getHandler(),
|
||||||
context.getClass(),
|
context.getClass(),
|
||||||
])
|
]);
|
||||||
|
|
||||||
const request = context.switchToHttp().getRequest<FastifyRequest>()
|
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||||
const isSse = request.headers.accept === 'text/event-stream'
|
const isSse = request.headers.accept === 'text/event-stream';
|
||||||
// 忽略 sse 请求
|
// 忽略 sse 请求
|
||||||
if (isPublic || isSse)
|
if (isPublic || isSse) return true;
|
||||||
return true
|
|
||||||
|
|
||||||
const { user } = request
|
const { user } = request;
|
||||||
|
|
||||||
if (!user)
|
if (!user) return false;
|
||||||
return false
|
|
||||||
|
|
||||||
// 如果是检查资源所属,且不是超级管理员,还需要进一步判断是否是自己的数据
|
// 如果是检查资源所属,且不是超级管理员,还需要进一步判断是否是自己的数据
|
||||||
const { entity, condition } = this.reflector.get<ResourceObject>(
|
const { entity, condition } = this.reflector.get<ResourceObject>(
|
||||||
RESOURCE_KEY,
|
RESOURCE_KEY,
|
||||||
context.getHandler(),
|
context.getHandler()
|
||||||
) ?? { entity: null, condition: null }
|
) ?? { entity: null, condition: null };
|
||||||
|
|
||||||
if (entity && !user.roles.includes(Roles.ADMIN)) {
|
if (entity && !user.roles.includes(Roles.ADMIN)) {
|
||||||
const repo: Repository<any> = this.dataSource.getRepository(entity)
|
const repo: Repository<any> = this.dataSource.getRepository(entity);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取请求中的 items (ids) 验证数据拥有者
|
* 获取请求中的 items (ids) 验证数据拥有者
|
||||||
* @param request
|
* @param request
|
||||||
*/
|
*/
|
||||||
const getRequestItems = (request?: FastifyRequest): number[] => {
|
const getRequestItems = (request?: FastifyRequest): number[] => {
|
||||||
const { params = {}, body = {}, query = {} } = (request ?? {}) as any
|
const { params = {}, body = {}, query = {} } = (request ?? {}) as any;
|
||||||
const id = params.id ?? body.id ?? query.id
|
const id = params.id ?? body.id ?? query.id;
|
||||||
|
|
||||||
if (id)
|
if (id) return [id];
|
||||||
return [id]
|
|
||||||
|
|
||||||
const { items } = body
|
const { items } = body;
|
||||||
return !isNil(items) && isArray(items) ? items : []
|
return !isNil(items) && isArray(items) ? items : [];
|
||||||
}
|
};
|
||||||
|
|
||||||
const items = getRequestItems(request)
|
const items = getRequestItems(request);
|
||||||
if (isEmpty(items))
|
if (isEmpty(items)) throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND);
|
||||||
throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND)
|
|
||||||
|
|
||||||
if (condition)
|
if (condition) return condition(repo, items, user);
|
||||||
return condition(repo, items, user)
|
|
||||||
|
|
||||||
const recordQuery = {
|
const recordQuery = {
|
||||||
where: {
|
where: {
|
||||||
|
@ -74,14 +69,13 @@ export class ResourceGuard implements CanActivate {
|
||||||
user: { id: user.uid },
|
user: { id: user.uid },
|
||||||
},
|
},
|
||||||
relations: ['user'],
|
relations: ['user'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const records = await repo.find(recordQuery);
|
||||||
|
|
||||||
|
if (isEmpty(records)) throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await repo.find(recordQuery)
|
return true;
|
||||||
|
|
||||||
if (isEmpty(records))
|
|
||||||
throw new BusinessException(ErrorEnum.REQUESTED_RESOURCE_NOT_FOUND)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger'
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class ImageCaptcha {
|
export class ImageCaptcha {
|
||||||
@ApiProperty({ description: 'base64格式的svg图片' })
|
@ApiProperty({ description: 'base64格式的svg图片' })
|
||||||
img: string
|
img: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '验证码对应的唯一ID' })
|
@ApiProperty({ description: '验证码对应的唯一ID' })
|
||||||
id: string
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LoginToken {
|
export class LoginToken {
|
||||||
@ApiProperty({ description: 'JWT身份Token' })
|
@ApiProperty({ description: 'JWT身份Token' })
|
||||||
token: string
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,35 @@
|
||||||
import { InjectRedis } from '@liaoliaots/nestjs-redis'
|
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import Redis from 'ioredis'
|
import Redis from 'ioredis';
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
import { BusinessException } from '~/common/exceptions/biz.exception';
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
import { genCaptchaImgKey } from '~/helper/genRedisKey'
|
import { genCaptchaImgKey } from '~/helper/genRedisKey';
|
||||||
import { CaptchaLogService } from '~/modules/system/log/services/captcha-log.service'
|
import { CaptchaLogService } from '~/modules/system/log/services/captcha-log.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CaptchaService {
|
export class CaptchaService {
|
||||||
constructor(
|
constructor(
|
||||||
@InjectRedis() private redis: Redis,
|
@InjectRedis() private redis: Redis,
|
||||||
|
|
||||||
private captchaLogService: CaptchaLogService,
|
private captchaLogService: CaptchaLogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 校验图片验证码
|
* 校验图片验证码
|
||||||
*/
|
*/
|
||||||
async checkImgCaptcha(id: string, code: string): Promise<void> {
|
async checkImgCaptcha(id: string, code: string): Promise<void> {
|
||||||
const result = await this.redis.get(genCaptchaImgKey(id))
|
const result = await this.redis.get(genCaptchaImgKey(id));
|
||||||
if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase())
|
if (isEmpty(result) || code.toLowerCase() !== result.toLowerCase())
|
||||||
throw new BusinessException(ErrorEnum.INVALID_VERIFICATION_CODE)
|
throw new BusinessException(ErrorEnum.INVALID_VERIFICATION_CODE);
|
||||||
|
|
||||||
// 校验成功后移除验证码
|
// 校验成功后移除验证码
|
||||||
await this.redis.del(genCaptchaImgKey(id))
|
await this.redis.del(genCaptchaImgKey(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
async log(
|
async log(account: string, code: string, provider: 'sms' | 'email', uid?: number): Promise<void> {
|
||||||
account: string,
|
await this.captchaLogService.create(account, code, provider, uid);
|
||||||
code: string,
|
|
||||||
provider: 'sms' | 'email',
|
|
||||||
uid?: number,
|
|
||||||
): Promise<void> {
|
|
||||||
await this.captchaLogService.create(account, code, provider, uid)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common'
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { JwtService } from '@nestjs/jwt'
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { ISecurityConfig, SecurityConfig } from '~/config'
|
import { ISecurityConfig, SecurityConfig } from '~/config';
|
||||||
import { RoleService } from '~/modules/system/role/role.service'
|
import { RoleService } from '~/modules/system/role/role.service';
|
||||||
import { UserEntity } from '~/modules/user/user.entity'
|
import { UserEntity } from '~/modules/user/user.entity';
|
||||||
import { generateUUID } from '~/utils'
|
import { generateUUID } from '~/utils';
|
||||||
|
|
||||||
import { AccessTokenEntity } from '../entities/access-token.entity'
|
import { AccessTokenEntity } from '../entities/access-token.entity';
|
||||||
import { RefreshTokenEntity } from '../entities/refresh-token.entity'
|
import { RefreshTokenEntity } from '../entities/refresh-token.entity';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 令牌服务
|
* 令牌服务
|
||||||
|
@ -18,7 +18,7 @@ export class TokenService {
|
||||||
constructor(
|
constructor(
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
private roleService: RoleService,
|
private roleService: RoleService,
|
||||||
@Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig,
|
@Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,30 +27,29 @@ export class TokenService {
|
||||||
* @param response
|
* @param response
|
||||||
*/
|
*/
|
||||||
async refreshToken(accessToken: AccessTokenEntity) {
|
async refreshToken(accessToken: AccessTokenEntity) {
|
||||||
const { user, refreshToken } = accessToken
|
const { user, refreshToken } = accessToken;
|
||||||
|
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
const now = dayjs()
|
const now = dayjs();
|
||||||
// 判断refreshToken是否过期
|
// 判断refreshToken是否过期
|
||||||
if (now.isAfter(refreshToken.expired_at))
|
if (now.isAfter(refreshToken.expired_at)) return null;
|
||||||
return null
|
|
||||||
|
|
||||||
const roleIds = await this.roleService.getRoleIdsByUser(user.id)
|
const roleIds = await this.roleService.getRoleIdsByUser(user.id);
|
||||||
const roleValues = await this.roleService.getRoleValues(roleIds)
|
const roleValues = await this.roleService.getRoleValues(roleIds);
|
||||||
|
|
||||||
// 如果没过期则生成新的access_token和refresh_token
|
// 如果没过期则生成新的access_token和refresh_token
|
||||||
const token = await this.generateAccessToken(user.id, roleValues)
|
const token = await this.generateAccessToken(user.id, roleValues);
|
||||||
|
|
||||||
await accessToken.remove()
|
await accessToken.remove();
|
||||||
return token
|
return token;
|
||||||
}
|
}
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateJwtSign(payload: any) {
|
generateJwtSign(payload: any) {
|
||||||
const jwtSign = this.jwtService.sign(payload)
|
const jwtSign = this.jwtService.sign(payload);
|
||||||
|
|
||||||
return jwtSign
|
return jwtSign;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateAccessToken(uid: number, roles: string[] = []) {
|
async generateAccessToken(uid: number, roles: string[] = []) {
|
||||||
|
@ -58,27 +57,25 @@ export class TokenService {
|
||||||
uid,
|
uid,
|
||||||
pv: 1,
|
pv: 1,
|
||||||
roles,
|
roles,
|
||||||
}
|
};
|
||||||
|
|
||||||
const jwtSign = this.jwtService.sign(payload)
|
const jwtSign = this.jwtService.sign(payload);
|
||||||
|
|
||||||
// 生成accessToken
|
// 生成accessToken
|
||||||
const accessToken = new AccessTokenEntity()
|
const accessToken = new AccessTokenEntity();
|
||||||
accessToken.value = jwtSign
|
accessToken.value = jwtSign;
|
||||||
accessToken.user = { id: uid } as UserEntity
|
accessToken.user = { id: uid } as UserEntity;
|
||||||
accessToken.expired_at = dayjs()
|
accessToken.expired_at = dayjs().add(this.securityConfig.jwtExprire, 'second').toDate();
|
||||||
.add(this.securityConfig.jwtExprire, 'second')
|
|
||||||
.toDate()
|
|
||||||
|
|
||||||
await accessToken.save()
|
await accessToken.save();
|
||||||
|
|
||||||
// 生成refreshToken
|
// 生成refreshToken
|
||||||
const refreshToken = await this.generateRefreshToken(accessToken, dayjs())
|
const refreshToken = await this.generateRefreshToken(accessToken, dayjs());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken: jwtSign,
|
accessToken: jwtSign,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -86,28 +83,23 @@ export class TokenService {
|
||||||
* @param accessToken
|
* @param accessToken
|
||||||
* @param now
|
* @param now
|
||||||
*/
|
*/
|
||||||
async generateRefreshToken(
|
async generateRefreshToken(accessToken: AccessTokenEntity, now: dayjs.Dayjs): Promise<string> {
|
||||||
accessToken: AccessTokenEntity,
|
|
||||||
now: dayjs.Dayjs,
|
|
||||||
): Promise<string> {
|
|
||||||
const refreshTokenPayload = {
|
const refreshTokenPayload = {
|
||||||
uuid: generateUUID(),
|
uuid: generateUUID(),
|
||||||
}
|
};
|
||||||
|
|
||||||
const refreshTokenSign = this.jwtService.sign(refreshTokenPayload, {
|
const refreshTokenSign = this.jwtService.sign(refreshTokenPayload, {
|
||||||
secret: this.securityConfig.refreshSecret,
|
secret: this.securityConfig.refreshSecret,
|
||||||
})
|
});
|
||||||
|
|
||||||
const refreshToken = new RefreshTokenEntity()
|
const refreshToken = new RefreshTokenEntity();
|
||||||
refreshToken.value = refreshTokenSign
|
refreshToken.value = refreshTokenSign;
|
||||||
refreshToken.expired_at = now
|
refreshToken.expired_at = now.add(this.securityConfig.refreshExpire, 'second').toDate();
|
||||||
.add(this.securityConfig.refreshExpire, 'second')
|
refreshToken.accessToken = accessToken;
|
||||||
.toDate()
|
|
||||||
refreshToken.accessToken = accessToken
|
|
||||||
|
|
||||||
await refreshToken.save()
|
await refreshToken.save();
|
||||||
|
|
||||||
return refreshTokenSign
|
return refreshTokenSign;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -119,7 +111,7 @@ export class TokenService {
|
||||||
where: { value },
|
where: { value },
|
||||||
relations: ['user', 'refreshToken'],
|
relations: ['user', 'refreshToken'],
|
||||||
cache: true,
|
cache: true,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -129,9 +121,8 @@ export class TokenService {
|
||||||
async removeAccessToken(value: string) {
|
async removeAccessToken(value: string) {
|
||||||
const accessToken = await AccessTokenEntity.findOne({
|
const accessToken = await AccessTokenEntity.findOne({
|
||||||
where: { value },
|
where: { value },
|
||||||
})
|
});
|
||||||
if (accessToken)
|
if (accessToken) await accessToken.remove();
|
||||||
await accessToken.remove()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -142,11 +133,10 @@ export class TokenService {
|
||||||
const refreshToken = await RefreshTokenEntity.findOne({
|
const refreshToken = await RefreshTokenEntity.findOne({
|
||||||
where: { value },
|
where: { value },
|
||||||
relations: ['accessToken'],
|
relations: ['accessToken'],
|
||||||
})
|
});
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
if (refreshToken.accessToken)
|
if (refreshToken.accessToken) await refreshToken.accessToken.remove();
|
||||||
await refreshToken.accessToken.remove()
|
await refreshToken.remove();
|
||||||
await refreshToken.remove()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,6 +145,6 @@ export class TokenService {
|
||||||
* @param token
|
* @param token
|
||||||
*/
|
*/
|
||||||
async verifyAccessToken(token: string): Promise<IAuthUser> {
|
async verifyAccessToken(token: string): Promise<IAuthUser> {
|
||||||
return this.jwtService.verify(token)
|
return this.jwtService.verify(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import { Inject, Injectable } from '@nestjs/common'
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { ExtractJwt, Strategy } from 'passport-jwt'
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
|
||||||
import { ISecurityConfig, SecurityConfig } from '~/config'
|
import { ISecurityConfig, SecurityConfig } from '~/config';
|
||||||
|
|
||||||
import { AuthStrategy } from '../auth.constant'
|
import { AuthStrategy } from '../auth.constant';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy, AuthStrategy.JWT) {
|
export class JwtStrategy extends PassportStrategy(Strategy, AuthStrategy.JWT) {
|
||||||
constructor(
|
constructor(@Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig) {
|
||||||
@Inject(SecurityConfig.KEY) private securityConfig: ISecurityConfig,
|
|
||||||
) {
|
|
||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: securityConfig.jwtSecret,
|
secretOrKey: securityConfig.jwtSecret,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: IAuthUser) {
|
async validate(payload: IAuthUser) {
|
||||||
return payload
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common';
|
||||||
import { PassportStrategy } from '@nestjs/passport'
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
import { Strategy } from 'passport-local'
|
import { Strategy } from 'passport-local';
|
||||||
|
|
||||||
import { AuthStrategy } from '../auth.constant'
|
import { AuthStrategy } from '../auth.constant';
|
||||||
import { AuthService } from '../auth.service'
|
import { AuthService } from '../auth.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalStrategy extends PassportStrategy(
|
export class LocalStrategy extends PassportStrategy(Strategy, AuthStrategy.LOCAL) {
|
||||||
Strategy,
|
|
||||||
AuthStrategy.LOCAL,
|
|
||||||
) {
|
|
||||||
constructor(private authService: AuthService) {
|
constructor(private authService: AuthService) {
|
||||||
super({
|
super({
|
||||||
usernameField: 'credential',
|
usernameField: 'credential',
|
||||||
passwordField: 'password',
|
passwordField: 'password',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(username: string, password: string): Promise<any> {
|
async validate(username: string, password: string): Promise<any> {
|
||||||
const user = await this.authService.validateUser(username, password)
|
const user = await this.authService.validateUser(username, password);
|
||||||
return user
|
return user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
import { definePermission } from '../auth/decorators/permission.decorator';
|
||||||
|
import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator';
|
||||||
|
import { ContractService } from './contract.service';
|
||||||
|
import { ApiResult } from '~/common/decorators/api-result.decorator';
|
||||||
|
export const permissions = definePermission('app:contract', {
|
||||||
|
LIST: 'list',
|
||||||
|
CREATE: 'create',
|
||||||
|
READ: 'read',
|
||||||
|
UPDATE: 'update',
|
||||||
|
DELETE: 'delete',
|
||||||
|
} as const);
|
||||||
|
|
||||||
|
@ApiTags('Contract - 合同')
|
||||||
|
@ApiSecurityAuth()
|
||||||
|
@Controller('contract')
|
||||||
|
export class ContractController {
|
||||||
|
constructor(private menuService: ContractService) {}
|
||||||
|
|
||||||
|
// @Get()
|
||||||
|
// @ApiOperation({ summary: '获取合同列表' })
|
||||||
|
// @ApiResult({ type: [RoleEntity], isPage: true })
|
||||||
|
// @Perm(permissions.LIST)
|
||||||
|
// async list(@Query() dto: RoleQueryDto) {
|
||||||
|
// return this.roleService.findAll(dto)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Get(':id')
|
||||||
|
// @ApiOperation({ summary: '获取角色信息' })
|
||||||
|
// @ApiResult({ type: RoleInfo })
|
||||||
|
// @Perm(permissions.READ)
|
||||||
|
// async info(@IdParam() id: number) {
|
||||||
|
// return this.roleService.info(id)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Post()
|
||||||
|
// @ApiOperation({ summary: '新增角色' })
|
||||||
|
// @Perm(permissions.CREATE)
|
||||||
|
// async create(@Body() dto: RoleDto): Promise<void> {
|
||||||
|
// await this.roleService.create(dto)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Put(':id')
|
||||||
|
// @ApiOperation({ summary: '更新角色' })
|
||||||
|
// @Perm(permissions.UPDATE)
|
||||||
|
// async update(
|
||||||
|
// @IdParam() id: number, @Body() dto: RoleUpdateDto): Promise<void> {
|
||||||
|
// await this.roleService.update(id, dto)
|
||||||
|
// await this.menuService.refreshOnlineUserPerms()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Delete(':id')
|
||||||
|
// @ApiOperation({ summary: '删除角色' })
|
||||||
|
// @Perm(permissions.DELETE)
|
||||||
|
// async delete(@IdParam() id: number): Promise<void> {
|
||||||
|
// if (await this.roleService.checkUserByRoleId(id))
|
||||||
|
// throw new BadRequestException('该角色存在关联用户,无法删除')
|
||||||
|
|
||||||
|
// await this.roleService.delete(id)
|
||||||
|
// await this.menuService.refreshOnlineUserPerms()
|
||||||
|
// }
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { ApiHideProperty, ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { Column, Entity, JoinTable, ManyToMany, Relation } from 'typeorm';
|
||||||
|
import { CommonEntity } from '~/common/entity/common.entity';
|
||||||
|
|
||||||
|
@Entity({ name: 'contract' })
|
||||||
|
export class RoleEntity extends CommonEntity {
|
||||||
|
@Column({ length: 100, unique: true, name: 'contract_number' })
|
||||||
|
@ApiProperty({ description: '合同编号' })
|
||||||
|
contractNumber: string;
|
||||||
|
|
||||||
|
@Column({ length: 255, name: 'title', type: 'varchar' })
|
||||||
|
@ApiProperty({ description: '合同标题' })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
@ApiProperty({ description: '合同类型', type: 'varchar' })
|
||||||
|
type: number;
|
||||||
|
|
||||||
|
@Column({ name: 'partyA', length: 255, type: 'varchar' })
|
||||||
|
@ApiProperty({ description: '甲方' })
|
||||||
|
partyA: string;
|
||||||
|
|
||||||
|
// @Column({ type: 'tinyint', nullable: true, default: 1 })
|
||||||
|
// @ApiProperty({ description: '状态:1启用,0禁用' })
|
||||||
|
// status: number
|
||||||
|
|
||||||
|
// @Column({ nullable: true })
|
||||||
|
// @ApiProperty({ description: '是否默认用户' })
|
||||||
|
// default: boolean
|
||||||
|
|
||||||
|
// @ApiHideProperty()
|
||||||
|
// @ManyToMany(() => MenuEntity, menu => menu.roles, {})
|
||||||
|
// @JoinTable({
|
||||||
|
// name: 'sys_role_menus',
|
||||||
|
// joinColumn: { name: 'role_id', referencedColumnName: 'id' },
|
||||||
|
// inverseJoinColumn: { name: 'menu_id', referencedColumnName: 'id' },
|
||||||
|
// })
|
||||||
|
// menus: Relation<MenuEntity[]>
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ContractController } from './contract.controller';
|
||||||
|
import { ContractService } from './contract.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ContractController],
|
||||||
|
providers: [ContractService],
|
||||||
|
})
|
||||||
|
export class ContractModule {}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ContractService {}
|
|
@ -1,14 +1,14 @@
|
||||||
import { Controller, Get } from '@nestjs/common'
|
import { Controller, Get } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger'
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import {
|
import {
|
||||||
DiskHealthIndicator,
|
DiskHealthIndicator,
|
||||||
HealthCheck,
|
HealthCheck,
|
||||||
HttpHealthIndicator,
|
HttpHealthIndicator,
|
||||||
MemoryHealthIndicator,
|
MemoryHealthIndicator,
|
||||||
TypeOrmHealthIndicator,
|
TypeOrmHealthIndicator,
|
||||||
} from '@nestjs/terminus'
|
} from '@nestjs/terminus';
|
||||||
|
|
||||||
import { Perm, definePermission } from '../auth/decorators/permission.decorator'
|
import { Perm, definePermission } from '../auth/decorators/permission.decorator';
|
||||||
|
|
||||||
export const PermissionHealth = definePermission('app:health', {
|
export const PermissionHealth = definePermission('app:health', {
|
||||||
NETWORK: 'network',
|
NETWORK: 'network',
|
||||||
|
@ -16,7 +16,7 @@ export const PermissionHealth = definePermission('app:health', {
|
||||||
MH: 'memory-heap',
|
MH: 'memory-heap',
|
||||||
MR: 'memory-rss',
|
MR: 'memory-rss',
|
||||||
DISK: 'disk',
|
DISK: 'disk',
|
||||||
} as const)
|
} as const);
|
||||||
|
|
||||||
@ApiTags('Health - 健康检查')
|
@ApiTags('Health - 健康检查')
|
||||||
@Controller('health')
|
@Controller('health')
|
||||||
|
@ -25,21 +25,21 @@ export class HealthController {
|
||||||
private http: HttpHealthIndicator,
|
private http: HttpHealthIndicator,
|
||||||
private db: TypeOrmHealthIndicator,
|
private db: TypeOrmHealthIndicator,
|
||||||
private memory: MemoryHealthIndicator,
|
private memory: MemoryHealthIndicator,
|
||||||
private disk: DiskHealthIndicator,
|
private disk: DiskHealthIndicator
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('network')
|
@Get('network')
|
||||||
@HealthCheck()
|
@HealthCheck()
|
||||||
@Perm(PermissionHealth.NETWORK)
|
@Perm(PermissionHealth.NETWORK)
|
||||||
async checkNetwork() {
|
async checkNetwork() {
|
||||||
return this.http.pingCheck('buqiyuan', 'https://buqiyuan.gitee.io/')
|
return this.http.pingCheck('louis', 'https://gitee.com/lu-zixun');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('database')
|
@Get('database')
|
||||||
@HealthCheck()
|
@HealthCheck()
|
||||||
@Perm(PermissionHealth.DB)
|
@Perm(PermissionHealth.DB)
|
||||||
async checkDatabase() {
|
async checkDatabase() {
|
||||||
return this.db.pingCheck('database')
|
return this.db.pingCheck('database');
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('memory-heap')
|
@Get('memory-heap')
|
||||||
|
@ -47,7 +47,7 @@ export class HealthController {
|
||||||
@Perm(PermissionHealth.MH)
|
@Perm(PermissionHealth.MH)
|
||||||
async checkMemoryHeap() {
|
async checkMemoryHeap() {
|
||||||
// the process should not use more than 200MB memory
|
// the process should not use more than 200MB memory
|
||||||
return this.memory.checkHeap('memory-heap', 200 * 1024 * 1024)
|
return this.memory.checkHeap('memory-heap', 200 * 1024 * 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('memory-rss')
|
@Get('memory-rss')
|
||||||
|
@ -55,7 +55,7 @@ export class HealthController {
|
||||||
@Perm(PermissionHealth.MR)
|
@Perm(PermissionHealth.MR)
|
||||||
async checkMemoryRSS() {
|
async checkMemoryRSS() {
|
||||||
// the process should not have more than 200MB RSS memory allocated
|
// the process should not have more than 200MB RSS memory allocated
|
||||||
return this.memory.checkRSS('memory-rss', 200 * 1024 * 1024)
|
return this.memory.checkRSS('memory-rss', 200 * 1024 * 1024);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('disk')
|
@Get('disk')
|
||||||
|
@ -66,6 +66,6 @@ export class HealthController {
|
||||||
// The used disk storage should not exceed 75% of the full disk size
|
// The used disk storage should not exceed 75% of the full disk size
|
||||||
thresholdPercent: 0.75,
|
thresholdPercent: 0.75,
|
||||||
path: '/',
|
path: '/',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { HttpModule } from '@nestjs/axios'
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
import { TerminusModule } from '@nestjs/terminus'
|
import { TerminusModule } from '@nestjs/terminus';
|
||||||
|
|
||||||
import { HealthController } from './health.controller'
|
import { HealthController } from './health.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TerminusModule, HttpModule],
|
imports: [TerminusModule, HttpModule],
|
||||||
|
|
|
@ -49,8 +49,7 @@ export class SFileInfoDetail {
|
||||||
mimeType: string;
|
mimeType: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description:
|
description: '文件存储类型,2 表示归档存储,1 表示低频存储,0表示普通存储。',
|
||||||
'文件存储类型,2 表示归档存储,1 表示低频存储,0表示普通存储。',
|
|
||||||
})
|
})
|
||||||
type: number;
|
type: number;
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,15 @@
|
||||||
import { Body, Controller, Get, Post, Query } from '@nestjs/common'
|
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
|
||||||
import {
|
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
ApiOkResponse,
|
|
||||||
ApiOperation,
|
|
||||||
ApiTags,
|
|
||||||
} from '@nestjs/swagger'
|
|
||||||
|
|
||||||
import { BusinessException } from '~/common/exceptions/biz.exception'
|
import { BusinessException } from '~/common/exceptions/biz.exception';
|
||||||
import { ErrorEnum } from '~/constants/error-code.constant'
|
import { ErrorEnum } from '~/constants/error-code.constant';
|
||||||
import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator'
|
import { AuthUser } from '~/modules/auth/decorators/auth-user.decorator';
|
||||||
|
|
||||||
import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'
|
import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator';
|
||||||
|
|
||||||
import { checkIsDemoMode } from '~/utils'
|
import { checkIsDemoMode } from '~/utils';
|
||||||
|
|
||||||
import { SFileInfoDetail, SFileList, UploadToken } from './manage.class'
|
import { SFileInfoDetail, SFileList, UploadToken } from './manage.class';
|
||||||
import {
|
import {
|
||||||
DeleteDto,
|
DeleteDto,
|
||||||
FileInfoDto,
|
FileInfoDto,
|
||||||
|
@ -22,8 +18,8 @@ import {
|
||||||
MKDirDto,
|
MKDirDto,
|
||||||
MarkFileDto,
|
MarkFileDto,
|
||||||
RenameDto,
|
RenameDto,
|
||||||
} from './manage.dto'
|
} from './manage.dto';
|
||||||
import { NetDiskManageService } from './manage.service'
|
import { NetDiskManageService } from './manage.service';
|
||||||
|
|
||||||
export const permissions = definePermission('netdisk:manage', {
|
export const permissions = definePermission('netdisk:manage', {
|
||||||
LIST: 'list',
|
LIST: 'list',
|
||||||
|
@ -38,7 +34,7 @@ export const permissions = definePermission('netdisk:manage', {
|
||||||
RENAME: 'rename',
|
RENAME: 'rename',
|
||||||
CUT: 'cut',
|
CUT: 'cut',
|
||||||
COPY: 'copy',
|
COPY: 'copy',
|
||||||
} as const)
|
} as const);
|
||||||
|
|
||||||
@ApiTags('NetDiskManage - 网盘管理模块')
|
@ApiTags('NetDiskManage - 网盘管理模块')
|
||||||
@Controller('manage')
|
@Controller('manage')
|
||||||
|
@ -50,20 +46,17 @@ export class NetDiskManageController {
|
||||||
@ApiOkResponse({ type: SFileList })
|
@ApiOkResponse({ type: SFileList })
|
||||||
@Perm(permissions.LIST)
|
@Perm(permissions.LIST)
|
||||||
async list(@Query() dto: GetFileListDto): Promise<SFileList> {
|
async list(@Query() dto: GetFileListDto): Promise<SFileList> {
|
||||||
return await this.manageService.getFileList(dto.path, dto.marker, dto.key)
|
return await this.manageService.getFileList(dto.path, dto.marker, dto.key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('mkdir')
|
@Post('mkdir')
|
||||||
@ApiOperation({ summary: '创建文件夹,支持多级' })
|
@ApiOperation({ summary: '创建文件夹,支持多级' })
|
||||||
@Perm(permissions.MKDIR)
|
@Perm(permissions.MKDIR)
|
||||||
async mkdir(@Body() dto: MKDirDto): Promise<void> {
|
async mkdir(@Body() dto: MKDirDto): Promise<void> {
|
||||||
const result = await this.manageService.checkFileExist(
|
const result = await this.manageService.checkFileExist(`${dto.path}${dto.dirName}/`);
|
||||||
`${dto.path}${dto.dirName}/`,
|
if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST);
|
||||||
)
|
|
||||||
if (result)
|
|
||||||
throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST)
|
|
||||||
|
|
||||||
await this.manageService.createDir(`${dto.path}${dto.dirName}`)
|
await this.manageService.createDir(`${dto.path}${dto.dirName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('token')
|
@Get('token')
|
||||||
|
@ -71,11 +64,11 @@ export class NetDiskManageController {
|
||||||
@ApiOkResponse({ type: UploadToken })
|
@ApiOkResponse({ type: UploadToken })
|
||||||
@Perm(permissions.TOKEN)
|
@Perm(permissions.TOKEN)
|
||||||
async token(@AuthUser() user: IAuthUser): Promise<UploadToken> {
|
async token(@AuthUser() user: IAuthUser): Promise<UploadToken> {
|
||||||
checkIsDemoMode()
|
checkIsDemoMode();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token: this.manageService.createUploadToken(`${user.uid}`),
|
token: this.manageService.createUploadToken(`${user.uid}`),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('info')
|
@Get('info')
|
||||||
|
@ -83,7 +76,7 @@ export class NetDiskManageController {
|
||||||
@ApiOkResponse({ type: SFileInfoDetail })
|
@ApiOkResponse({ type: SFileInfoDetail })
|
||||||
@Perm(permissions.INFO)
|
@Perm(permissions.INFO)
|
||||||
async info(@Query() dto: FileInfoDto): Promise<SFileInfoDetail> {
|
async info(@Query() dto: FileInfoDto): Promise<SFileInfoDetail> {
|
||||||
return await this.manageService.getFileInfo(dto.name, dto.path)
|
return await this.manageService.getFileInfo(dto.name, dto.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('mark')
|
@Post('mark')
|
||||||
|
@ -92,7 +85,7 @@ export class NetDiskManageController {
|
||||||
async mark(@Body() dto: MarkFileDto): Promise<void> {
|
async mark(@Body() dto: MarkFileDto): Promise<void> {
|
||||||
await this.manageService.changeFileHeaders(dto.name, dto.path, {
|
await this.manageService.changeFileHeaders(dto.name, dto.path, {
|
||||||
mark: dto.mark,
|
mark: dto.mark,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('download')
|
@Get('download')
|
||||||
|
@ -100,7 +93,7 @@ export class NetDiskManageController {
|
||||||
@ApiOkResponse({ type: String })
|
@ApiOkResponse({ type: String })
|
||||||
@Perm(permissions.DOWNLOAD)
|
@Perm(permissions.DOWNLOAD)
|
||||||
async download(@Query() dto: FileInfoDto): Promise<string> {
|
async download(@Query() dto: FileInfoDto): Promise<string> {
|
||||||
return this.manageService.getDownloadLink(`${dto.path}${dto.name}`)
|
return this.manageService.getDownloadLink(`${dto.path}${dto.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('rename')
|
@Post('rename')
|
||||||
|
@ -108,22 +101,19 @@ export class NetDiskManageController {
|
||||||
@Perm(permissions.RENAME)
|
@Perm(permissions.RENAME)
|
||||||
async rename(@Body() dto: RenameDto): Promise<void> {
|
async rename(@Body() dto: RenameDto): Promise<void> {
|
||||||
const result = await this.manageService.checkFileExist(
|
const result = await this.manageService.checkFileExist(
|
||||||
`${dto.path}${dto.toName}${dto.type === 'dir' ? '/' : ''}`,
|
`${dto.path}${dto.toName}${dto.type === 'dir' ? '/' : ''}`
|
||||||
)
|
);
|
||||||
if (result)
|
if (result) throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST);
|
||||||
throw new BusinessException(ErrorEnum.OSS_FILE_OR_DIR_EXIST)
|
|
||||||
|
|
||||||
if (dto.type === 'file')
|
if (dto.type === 'file') await this.manageService.renameFile(dto.path, dto.name, dto.toName);
|
||||||
await this.manageService.renameFile(dto.path, dto.name, dto.toName)
|
else await this.manageService.renameDir(dto.path, dto.name, dto.toName);
|
||||||
else
|
|
||||||
await this.manageService.renameDir(dto.path, dto.name, dto.toName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('delete')
|
@Post('delete')
|
||||||
@ApiOperation({ summary: '删除文件或文件夹' })
|
@ApiOperation({ summary: '删除文件或文件夹' })
|
||||||
@Perm(permissions.DELETE)
|
@Perm(permissions.DELETE)
|
||||||
async delete(@Body() dto: DeleteDto): Promise<void> {
|
async delete(@Body() dto: DeleteDto): Promise<void> {
|
||||||
await this.manageService.deleteMultiFileOrDir(dto.files, dto.path)
|
await this.manageService.deleteMultiFileOrDir(dto.files, dto.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('cut')
|
@Post('cut')
|
||||||
|
@ -131,23 +121,15 @@ export class NetDiskManageController {
|
||||||
@Perm(permissions.CUT)
|
@Perm(permissions.CUT)
|
||||||
async cut(@Body() dto: FileOpDto): Promise<void> {
|
async cut(@Body() dto: FileOpDto): Promise<void> {
|
||||||
if (dto.originPath === dto.toPath)
|
if (dto.originPath === dto.toPath)
|
||||||
throw new BusinessException(ErrorEnum.OSS_NO_OPERATION_REQUIRED)
|
throw new BusinessException(ErrorEnum.OSS_NO_OPERATION_REQUIRED);
|
||||||
|
|
||||||
await this.manageService.moveMultiFileOrDir(
|
await this.manageService.moveMultiFileOrDir(dto.files, dto.originPath, dto.toPath);
|
||||||
dto.files,
|
|
||||||
dto.originPath,
|
|
||||||
dto.toPath,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('copy')
|
@Post('copy')
|
||||||
@ApiOperation({ summary: '复制文件或文件夹,支持批量' })
|
@ApiOperation({ summary: '复制文件或文件夹,支持批量' })
|
||||||
@Perm(permissions.COPY)
|
@Perm(permissions.COPY)
|
||||||
async copy(@Body() dto: FileOpDto): Promise<void> {
|
async copy(@Body() dto: FileOpDto): Promise<void> {
|
||||||
await this.manageService.copyMultiFileOrDir(
|
await this.manageService.copyMultiFileOrDir(dto.files, dto.originPath, dto.toPath);
|
||||||
dto.files,
|
|
||||||
dto.originPath,
|
|
||||||
dto.toPath,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer'
|
import { Type } from 'class-transformer';
|
||||||
import {
|
import {
|
||||||
ArrayMaxSize,
|
ArrayMaxSize,
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
|
@ -12,31 +12,28 @@ import {
|
||||||
ValidationArguments,
|
ValidationArguments,
|
||||||
ValidatorConstraint,
|
ValidatorConstraint,
|
||||||
ValidatorConstraintInterface,
|
ValidatorConstraintInterface,
|
||||||
} from 'class-validator'
|
} from 'class-validator';
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash';
|
||||||
|
|
||||||
import { NETDISK_HANDLE_MAX_ITEM } from '~/constants/oss.constant'
|
import { NETDISK_HANDLE_MAX_ITEM } from '~/constants/oss.constant';
|
||||||
|
|
||||||
@ValidatorConstraint({ name: 'IsLegalNameExpression', async: false })
|
@ValidatorConstraint({ name: 'IsLegalNameExpression', async: false })
|
||||||
export class IsLegalNameExpression implements ValidatorConstraintInterface {
|
export class IsLegalNameExpression implements ValidatorConstraintInterface {
|
||||||
validate(value: string, args: ValidationArguments) {
|
validate(value: string, args: ValidationArguments) {
|
||||||
try {
|
try {
|
||||||
if (isEmpty(value))
|
if (isEmpty(value)) throw new Error('dir name is empty');
|
||||||
throw new Error('dir name is empty')
|
|
||||||
|
|
||||||
if (value.includes('/'))
|
if (value.includes('/')) throw new Error('dir name not allow /');
|
||||||
throw new Error('dir name not allow /')
|
|
||||||
|
|
||||||
return true
|
return true;
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) {
|
return false;
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultMessage(_args: ValidationArguments) {
|
defaultMessage(_args: ValidationArguments) {
|
||||||
// here you can provide default error message if validation failed
|
// here you can provide default error message if validation failed
|
||||||
return 'file or dir name invalid'
|
return 'file or dir name invalid';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,30 +41,30 @@ export class FileOpItem {
|
||||||
@ApiProperty({ description: '文件类型', enum: ['file', 'dir'] })
|
@ApiProperty({ description: '文件类型', enum: ['file', 'dir'] })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches(/(^file$)|(^dir$)/)
|
@Matches(/(^file$)|(^dir$)/)
|
||||||
type: string
|
type: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '文件名称' })
|
@ApiProperty({ description: '文件名称' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Validate(IsLegalNameExpression)
|
@Validate(IsLegalNameExpression)
|
||||||
name: string
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GetFileListDto {
|
export class GetFileListDto {
|
||||||
@ApiProperty({ description: '分页标识' })
|
@ApiProperty({ description: '分页标识' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
marker: string
|
marker: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '当前路径' })
|
@ApiProperty({ description: '当前路径' })
|
||||||
@IsString()
|
@IsString()
|
||||||
path: string
|
path: string;
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: '搜索关键字' })
|
@ApiPropertyOptional({ description: '搜索关键字' })
|
||||||
@Validate(IsLegalNameExpression)
|
@Validate(IsLegalNameExpression)
|
||||||
@ValidateIf(o => !isEmpty(o.key))
|
@ValidateIf(o => !isEmpty(o.key))
|
||||||
@IsString()
|
@IsString()
|
||||||
key: string
|
key: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MKDirDto {
|
export class MKDirDto {
|
||||||
|
@ -75,34 +72,34 @@ export class MKDirDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@Validate(IsLegalNameExpression)
|
@Validate(IsLegalNameExpression)
|
||||||
dirName: string
|
dirName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '所属路径' })
|
@ApiProperty({ description: '所属路径' })
|
||||||
@IsString()
|
@IsString()
|
||||||
path: string
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RenameDto {
|
export class RenameDto {
|
||||||
@ApiProperty({ description: '文件类型' })
|
@ApiProperty({ description: '文件类型' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@Matches(/(^file$)|(^dir$)/)
|
@Matches(/(^file$)|(^dir$)/)
|
||||||
type: string
|
type: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '更改的名称' })
|
@ApiProperty({ description: '更改的名称' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Validate(IsLegalNameExpression)
|
@Validate(IsLegalNameExpression)
|
||||||
toName: string
|
toName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '原来的名称' })
|
@ApiProperty({ description: '原来的名称' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Validate(IsLegalNameExpression)
|
@Validate(IsLegalNameExpression)
|
||||||
name: string
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '路径' })
|
@ApiProperty({ description: '路径' })
|
||||||
@IsString()
|
@IsString()
|
||||||
path: string
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileInfoDto {
|
export class FileInfoDto {
|
||||||
|
@ -110,11 +107,11 @@ export class FileInfoDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Validate(IsLegalNameExpression)
|
@Validate(IsLegalNameExpression)
|
||||||
name: string
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '文件所在路径' })
|
@ApiProperty({ description: '文件所在路径' })
|
||||||
@IsString()
|
@IsString()
|
||||||
path: string
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DeleteDto {
|
export class DeleteDto {
|
||||||
|
@ -122,11 +119,11 @@ export class DeleteDto {
|
||||||
@Type(() => FileOpItem)
|
@Type(() => FileOpItem)
|
||||||
@ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)
|
@ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
files: FileOpItem[]
|
files: FileOpItem[];
|
||||||
|
|
||||||
@ApiProperty({ description: '所在目录' })
|
@ApiProperty({ description: '所在目录' })
|
||||||
@IsString()
|
@IsString()
|
||||||
path: string
|
path: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MarkFileDto {
|
export class MarkFileDto {
|
||||||
|
@ -134,15 +131,15 @@ export class MarkFileDto {
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Validate(IsLegalNameExpression)
|
@Validate(IsLegalNameExpression)
|
||||||
name: string
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '文件所在路径' })
|
@ApiProperty({ description: '文件所在路径' })
|
||||||
@IsString()
|
@IsString()
|
||||||
path: string
|
path: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '备注信息' })
|
@ApiProperty({ description: '备注信息' })
|
||||||
@IsString()
|
@IsString()
|
||||||
mark: string
|
mark: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FileOpDto {
|
export class FileOpDto {
|
||||||
|
@ -150,13 +147,13 @@ export class FileOpDto {
|
||||||
@Type(() => FileOpItem)
|
@Type(() => FileOpItem)
|
||||||
@ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)
|
@ArrayMaxSize(NETDISK_HANDLE_MAX_ITEM)
|
||||||
@ValidateNested({ each: true })
|
@ValidateNested({ each: true })
|
||||||
files: FileOpItem[]
|
files: FileOpItem[];
|
||||||
|
|
||||||
@ApiProperty({ description: '操作前的目录' })
|
@ApiProperty({ description: '操作前的目录' })
|
||||||
@IsString()
|
@IsString()
|
||||||
originPath: string
|
originPath: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '操作后的目录' })
|
@ApiProperty({ description: '操作后的目录' })
|
||||||
@IsString()
|
@IsString()
|
||||||
toPath: string
|
toPath: string;
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,21 +1,24 @@
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { RouterModule } from '@nestjs/core'
|
import { RouterModule } from '@nestjs/core';
|
||||||
|
|
||||||
import { UserModule } from '../user/user.module'
|
import { UserModule } from '../user/user.module';
|
||||||
|
|
||||||
import { NetDiskManageController } from './manager/manage.controller'
|
import { NetDiskManageController } from './manager/manage.controller';
|
||||||
import { NetDiskManageService } from './manager/manage.service'
|
import { NetDiskManageService } from './manager/manage.service';
|
||||||
import { NetDiskOverviewController } from './overview/overview.controller'
|
import { NetDiskOverviewController } from './overview/overview.controller';
|
||||||
import { NetDiskOverviewService } from './overview/overview.service'
|
import { NetDiskOverviewService } from './overview/overview.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [UserModule, RouterModule.register([
|
imports: [
|
||||||
|
UserModule,
|
||||||
|
RouterModule.register([
|
||||||
{
|
{
|
||||||
path: 'netdisk',
|
path: 'netdisk',
|
||||||
module: NetdiskModule,
|
module: NetdiskModule,
|
||||||
},
|
},
|
||||||
])],
|
]),
|
||||||
|
],
|
||||||
controllers: [NetDiskManageController, NetDiskOverviewController],
|
controllers: [NetDiskManageController, NetDiskOverviewController],
|
||||||
providers: [NetDiskManageService, NetDiskOverviewService],
|
providers: [NetDiskManageService, NetDiskOverviewService],
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,23 +1,15 @@
|
||||||
import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager'
|
import { CacheInterceptor, CacheKey, CacheTTL } from '@nestjs/cache-manager';
|
||||||
import {
|
import { Controller, Get, UseInterceptors } from '@nestjs/common';
|
||||||
Controller,
|
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
Get,
|
|
||||||
UseInterceptors,
|
|
||||||
} from '@nestjs/common'
|
|
||||||
import {
|
|
||||||
ApiOkResponse,
|
|
||||||
ApiOperation,
|
|
||||||
ApiTags,
|
|
||||||
} from '@nestjs/swagger'
|
|
||||||
|
|
||||||
import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator'
|
import { Perm, definePermission } from '~/modules/auth/decorators/permission.decorator';
|
||||||
|
|
||||||
import { OverviewSpaceInfo } from './overview.dto'
|
import { OverviewSpaceInfo } from './overview.dto';
|
||||||
import { NetDiskOverviewService } from './overview.service'
|
import { NetDiskOverviewService } from './overview.service';
|
||||||
|
|
||||||
export const permissions = definePermission('netdisk:overview', {
|
export const permissions = definePermission('netdisk:overview', {
|
||||||
DESC: 'desc',
|
DESC: 'desc',
|
||||||
} as const)
|
} as const);
|
||||||
|
|
||||||
@ApiTags('NetDiskOverview - 网盘概览模块')
|
@ApiTags('NetDiskOverview - 网盘概览模块')
|
||||||
@Controller('overview')
|
@Controller('overview')
|
||||||
|
@ -32,11 +24,11 @@ export class NetDiskOverviewController {
|
||||||
@ApiOkResponse({ type: OverviewSpaceInfo })
|
@ApiOkResponse({ type: OverviewSpaceInfo })
|
||||||
@Perm(permissions.DESC)
|
@Perm(permissions.DESC)
|
||||||
async space(): Promise<OverviewSpaceInfo> {
|
async space(): Promise<OverviewSpaceInfo> {
|
||||||
const date = this.overviewService.getZeroHourAnd1Day(new Date())
|
const date = this.overviewService.getZeroHourAnd1Day(new Date());
|
||||||
const hit = await this.overviewService.getHit(date)
|
const hit = await this.overviewService.getHit(date);
|
||||||
const flow = await this.overviewService.getFlow(date)
|
const flow = await this.overviewService.getFlow(date);
|
||||||
const space = await this.overviewService.getSpace(date)
|
const space = await this.overviewService.getSpace(date);
|
||||||
const count = await this.overviewService.getCount(date)
|
const count = await this.overviewService.getCount(date);
|
||||||
return {
|
return {
|
||||||
fileSize: count.datas[count.datas.length - 1],
|
fileSize: count.datas[count.datas.length - 1],
|
||||||
flowSize: flow.datas[flow.datas.length - 1],
|
flowSize: flow.datas[flow.datas.length - 1],
|
||||||
|
@ -44,6 +36,6 @@ export class NetDiskOverviewController {
|
||||||
spaceSize: space.datas[space.datas.length - 1],
|
spaceSize: space.datas[space.datas.length - 1],
|
||||||
flowTrend: flow,
|
flowTrend: flow,
|
||||||
sizeTrend: space,
|
sizeTrend: space,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,32 @@
|
||||||
import { HttpService } from '@nestjs/axios'
|
import { HttpService } from '@nestjs/axios';
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config'
|
import { ConfigService } from '@nestjs/config';
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs';
|
||||||
import * as qiniu from 'qiniu'
|
import * as qiniu from 'qiniu';
|
||||||
|
|
||||||
import { ConfigKeyPaths } from '~/config'
|
import { ConfigKeyPaths } from '~/config';
|
||||||
import { OSS_API } from '~/constants/oss.constant'
|
import { OSS_API } from '~/constants/oss.constant';
|
||||||
|
|
||||||
import { CountInfo, FlowInfo, HitInfo, SpaceInfo } from './overview.dto'
|
import { CountInfo, FlowInfo, HitInfo, SpaceInfo } from './overview.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class NetDiskOverviewService {
|
export class NetDiskOverviewService {
|
||||||
private mac: qiniu.auth.digest.Mac
|
private mac: qiniu.auth.digest.Mac;
|
||||||
private readonly FORMAT = 'YYYYMMDDHHmmss'
|
private readonly FORMAT = 'YYYYMMDDHHmmss';
|
||||||
private get qiniuConfig() {
|
private get qiniuConfig() {
|
||||||
return this.configService.get('oss', { infer: true })
|
return this.configService.get('oss', { infer: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private configService: ConfigService<ConfigKeyPaths>,
|
private configService: ConfigService<ConfigKeyPaths>,
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService
|
||||||
) {
|
) {
|
||||||
this.mac = new qiniu.auth.digest.Mac(
|
this.mac = new qiniu.auth.digest.Mac(this.qiniuConfig.accessKey, this.qiniuConfig.secretKey);
|
||||||
this.qiniuConfig.accessKey,
|
|
||||||
this.qiniuConfig.secretKey,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取格式化后的起始和结束时间 */
|
/** 获取格式化后的起始和结束时间 */
|
||||||
getStartAndEndDate(start: Date, end = new Date()) {
|
getStartAndEndDate(start: Date, end = new Date()) {
|
||||||
return [dayjs(start).format(this.FORMAT), dayjs(end).format(this.FORMAT)]
|
return [dayjs(start).format(this.FORMAT), dayjs(end).format(this.FORMAT)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -40,9 +37,9 @@ export class NetDiskOverviewService {
|
||||||
const defaultParams = {
|
const defaultParams = {
|
||||||
$bucket: this.qiniuConfig.bucket,
|
$bucket: this.qiniuConfig.bucket,
|
||||||
g: 'day',
|
g: 'day',
|
||||||
}
|
};
|
||||||
const searchParams = new URLSearchParams({ ...defaultParams, ...queryParams })
|
const searchParams = new URLSearchParams({ ...defaultParams, ...queryParams });
|
||||||
return decodeURIComponent(`${OSS_API}/v6/${type}?${searchParams}`)
|
return decodeURIComponent(`${OSS_API}/v6/${type}?${searchParams}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取统计数据 */
|
/** 获取统计数据 */
|
||||||
|
@ -51,33 +48,33 @@ export class NetDiskOverviewService {
|
||||||
this.mac,
|
this.mac,
|
||||||
url,
|
url,
|
||||||
'GET',
|
'GET',
|
||||||
'application/x-www-form-urlencoded',
|
'application/x-www-form-urlencoded'
|
||||||
)
|
);
|
||||||
return this.httpService.axiosRef.get(url, {
|
return this.httpService.axiosRef.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
'Authorization': `${accessToken}`,
|
Authorization: `${accessToken}`,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当天零时
|
* 获取当天零时
|
||||||
*/
|
*/
|
||||||
getZeroHourToDay(current: Date): Date {
|
getZeroHourToDay(current: Date): Date {
|
||||||
const year = dayjs(current).year()
|
const year = dayjs(current).year();
|
||||||
const month = dayjs(current).month()
|
const month = dayjs(current).month();
|
||||||
const date = dayjs(current).date()
|
const date = dayjs(current).date();
|
||||||
return new Date(year, month, date, 0)
|
return new Date(year, month, date, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当月1号零时
|
* 获取当月1号零时
|
||||||
*/
|
*/
|
||||||
getZeroHourAnd1Day(current: Date): Date {
|
getZeroHourAnd1Day(current: Date): Date {
|
||||||
const year = dayjs(current).year()
|
const year = dayjs(current).year();
|
||||||
const month = dayjs(current).month()
|
const month = dayjs(current).month();
|
||||||
return new Date(year, month, 1, 0)
|
return new Date(year, month, 1, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -85,15 +82,15 @@ export class NetDiskOverviewService {
|
||||||
* https://developer.qiniu.com/kodo/3908/statistic-space
|
* https://developer.qiniu.com/kodo/3908/statistic-space
|
||||||
*/
|
*/
|
||||||
async getSpace(beginDate: Date, endDate = new Date()): Promise<SpaceInfo> {
|
async getSpace(beginDate: Date, endDate = new Date()): Promise<SpaceInfo> {
|
||||||
const [begin, end] = this.getStartAndEndDate(beginDate, endDate)
|
const [begin, end] = this.getStartAndEndDate(beginDate, endDate);
|
||||||
const url = this.getStatisticUrl('space', { begin, end })
|
const url = this.getStatisticUrl('space', { begin, end });
|
||||||
const { data } = await this.getStatisticData(url)
|
const { data } = await this.getStatisticData(url);
|
||||||
return {
|
return {
|
||||||
datas: data.datas,
|
datas: data.datas,
|
||||||
times: data.times.map((e) => {
|
times: data.times.map(e => {
|
||||||
return dayjs.unix(e).date()
|
return dayjs.unix(e).date();
|
||||||
}),
|
}),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -101,15 +98,15 @@ export class NetDiskOverviewService {
|
||||||
* https://developer.qiniu.com/kodo/3914/count
|
* https://developer.qiniu.com/kodo/3914/count
|
||||||
*/
|
*/
|
||||||
async getCount(beginDate: Date, endDate = new Date()): Promise<CountInfo> {
|
async getCount(beginDate: Date, endDate = new Date()): Promise<CountInfo> {
|
||||||
const [begin, end] = this.getStartAndEndDate(beginDate, endDate)
|
const [begin, end] = this.getStartAndEndDate(beginDate, endDate);
|
||||||
const url = this.getStatisticUrl('count', { begin, end })
|
const url = this.getStatisticUrl('count', { begin, end });
|
||||||
const { data } = await this.getStatisticData(url)
|
const { data } = await this.getStatisticData(url);
|
||||||
return {
|
return {
|
||||||
times: data.times.map((e) => {
|
times: data.times.map(e => {
|
||||||
return dayjs.unix(e).date()
|
return dayjs.unix(e).date();
|
||||||
}),
|
}),
|
||||||
datas: data.datas,
|
datas: data.datas,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -118,19 +115,25 @@ export class NetDiskOverviewService {
|
||||||
* https://developer.qiniu.com/kodo/3820/blob-io
|
* https://developer.qiniu.com/kodo/3820/blob-io
|
||||||
*/
|
*/
|
||||||
async getFlow(beginDate: Date, endDate = new Date()): Promise<FlowInfo> {
|
async getFlow(beginDate: Date, endDate = new Date()): Promise<FlowInfo> {
|
||||||
const [begin, end] = this.getStartAndEndDate(beginDate, endDate)
|
const [begin, end] = this.getStartAndEndDate(beginDate, endDate);
|
||||||
const url = this.getStatisticUrl('blob_io', { begin, end, $ftype: 0, $src: 'origin', select: 'flow' })
|
const url = this.getStatisticUrl('blob_io', {
|
||||||
const { data } = await this.getStatisticData(url)
|
begin,
|
||||||
const times = []
|
end,
|
||||||
const datas = []
|
$ftype: 0,
|
||||||
data.forEach((e) => {
|
$src: 'origin',
|
||||||
times.push(dayjs(e.time).date())
|
select: 'flow',
|
||||||
datas.push(e.values.flow)
|
});
|
||||||
})
|
const { data } = await this.getStatisticData(url);
|
||||||
|
const times = [];
|
||||||
|
const datas = [];
|
||||||
|
data.forEach(e => {
|
||||||
|
times.push(dayjs(e.time).date());
|
||||||
|
datas.push(e.values.flow);
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
times,
|
times,
|
||||||
datas,
|
datas,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -139,18 +142,24 @@ export class NetDiskOverviewService {
|
||||||
* https://developer.qiniu.com/kodo/3820/blob-io
|
* https://developer.qiniu.com/kodo/3820/blob-io
|
||||||
*/
|
*/
|
||||||
async getHit(beginDate: Date, endDate = new Date()): Promise<HitInfo> {
|
async getHit(beginDate: Date, endDate = new Date()): Promise<HitInfo> {
|
||||||
const [begin, end] = this.getStartAndEndDate(beginDate, endDate)
|
const [begin, end] = this.getStartAndEndDate(beginDate, endDate);
|
||||||
const url = this.getStatisticUrl('blob_io', { begin, end, $ftype: 0, $src: 'inner', select: 'hit' })
|
const url = this.getStatisticUrl('blob_io', {
|
||||||
const { data } = await this.getStatisticData(url)
|
begin,
|
||||||
const times = []
|
end,
|
||||||
const datas = []
|
$ftype: 0,
|
||||||
data.forEach((e) => {
|
$src: 'inner',
|
||||||
times.push(dayjs(e.time).date())
|
select: 'hit',
|
||||||
datas.push(e.values.hit)
|
});
|
||||||
})
|
const { data } = await this.getStatisticData(url);
|
||||||
|
const times = [];
|
||||||
|
const datas = [];
|
||||||
|
data.forEach(e => {
|
||||||
|
times.push(dayjs(e.time).date());
|
||||||
|
datas.push(e.values.hit);
|
||||||
|
});
|
||||||
return {
|
return {
|
||||||
times,
|
times,
|
||||||
datas,
|
datas,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,25 @@
|
||||||
import { BeforeApplicationShutdown, Controller, Param, ParseIntPipe, Req, Res, Sse } from '@nestjs/common'
|
import {
|
||||||
import { ApiTags } from '@nestjs/swagger'
|
BeforeApplicationShutdown,
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify'
|
Controller,
|
||||||
import { Observable, interval } from 'rxjs'
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
|
Req,
|
||||||
|
Res,
|
||||||
|
Sse,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
|
import { Observable, interval } from 'rxjs';
|
||||||
|
|
||||||
import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator'
|
import { ApiSecurityAuth } from '~/common/decorators/swagger.decorator';
|
||||||
|
|
||||||
import { MessageEvent, SseService } from './sse.service'
|
import { MessageEvent, SseService } from './sse.service';
|
||||||
|
|
||||||
@ApiTags('System - sse模块')
|
@ApiTags('System - sse模块')
|
||||||
@ApiSecurityAuth()
|
@ApiSecurityAuth()
|
||||||
@Controller('sse')
|
@Controller('sse')
|
||||||
export class SseController implements BeforeApplicationShutdown {
|
export class SseController implements BeforeApplicationShutdown {
|
||||||
private replyMap: Map<number, FastifyReply> = new Map()
|
private replyMap: Map<number, FastifyReply> = new Map();
|
||||||
|
|
||||||
constructor(private readonly sseService: SseService) {}
|
constructor(private readonly sseService: SseService) {}
|
||||||
|
|
||||||
|
@ -19,36 +27,40 @@ export class SseController implements BeforeApplicationShutdown {
|
||||||
this.sseService.sendToAll({
|
this.sseService.sendToAll({
|
||||||
type: 'close',
|
type: 'close',
|
||||||
data: 'bye~',
|
data: 'bye~',
|
||||||
})
|
});
|
||||||
this.replyMap.forEach((reply) => {
|
this.replyMap.forEach(reply => {
|
||||||
reply.raw.end().destroy()
|
reply.raw.end().destroy();
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过控制台关闭程序时触发
|
// 通过控制台关闭程序时触发
|
||||||
beforeApplicationShutdown() {
|
beforeApplicationShutdown() {
|
||||||
// console.log('beforeApplicationShutdown')
|
// console.log('beforeApplicationShutdown')
|
||||||
this.closeAllConnect()
|
this.closeAllConnect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Sse(':uid')
|
@Sse(':uid')
|
||||||
sse(@Param('uid', ParseIntPipe) uid: number, @Req() req: FastifyRequest, @Res() res: FastifyReply): Observable<MessageEvent> {
|
sse(
|
||||||
this.replyMap.set(uid, res)
|
@Param('uid', ParseIntPipe) uid: number,
|
||||||
|
@Req() req: FastifyRequest,
|
||||||
|
@Res() res: FastifyReply
|
||||||
|
): Observable<MessageEvent> {
|
||||||
|
this.replyMap.set(uid, res);
|
||||||
|
|
||||||
const subscription = interval(10000).subscribe(() => {
|
const subscription = interval(10000).subscribe(() => {
|
||||||
this.sseService.sendToClient(uid, { type: 'ping' })
|
this.sseService.sendToClient(uid, { type: 'ping' });
|
||||||
})
|
});
|
||||||
|
|
||||||
// 当客户端断开连接时
|
// 当客户端断开连接时
|
||||||
req.raw.on('close', () => {
|
req.raw.on('close', () => {
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe();
|
||||||
this.sseService.removeClient(uid)
|
this.sseService.removeClient(uid);
|
||||||
this.replyMap.delete(uid)
|
this.replyMap.delete(uid);
|
||||||
// console.log(`user-${uid}已关闭`)
|
// console.log(`user-${uid}已关闭`)
|
||||||
})
|
});
|
||||||
|
|
||||||
return new Observable((subscriber) => {
|
return new Observable(subscriber => {
|
||||||
this.sseService.addClient(uid, subscriber)
|
this.sseService.addClient(uid, subscriber);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Module } from '@nestjs/common'
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { SseController } from './sse.controller'
|
import { SseController } from './sse.controller';
|
||||||
import { SseService } from './sse.service'
|
import { SseService } from './sse.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [],
|
||||||
|
|
|
@ -1,42 +1,42 @@
|
||||||
import { Injectable } from '@nestjs/common'
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Subscriber } from 'rxjs'
|
import { Subscriber } from 'rxjs';
|
||||||
import { In } from 'typeorm'
|
import { In } from 'typeorm';
|
||||||
|
|
||||||
import { ROOT_ROLE_ID } from '~/constants/system.constant'
|
import { ROOT_ROLE_ID } from '~/constants/system.constant';
|
||||||
|
|
||||||
import { RoleEntity } from '~/modules/system/role/role.entity'
|
import { RoleEntity } from '~/modules/system/role/role.entity';
|
||||||
import { UserEntity } from '~/modules/user/user.entity'
|
import { UserEntity } from '~/modules/user/user.entity';
|
||||||
|
|
||||||
export interface MessageEvent {
|
export interface MessageEvent {
|
||||||
data?: string | object
|
data?: string | object;
|
||||||
id?: string
|
id?: string;
|
||||||
type?: 'ping' | 'close' | 'updatePermsAndMenus'
|
type?: 'ping' | 'close' | 'updatePermsAndMenus';
|
||||||
retry?: number
|
retry?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientMap: Map<number, Subscriber<MessageEvent>> = new Map()
|
const clientMap: Map<number, Subscriber<MessageEvent>> = new Map();
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SseService {
|
export class SseService {
|
||||||
addClient(uid: number, subscriber: Subscriber<MessageEvent>) {
|
addClient(uid: number, subscriber: Subscriber<MessageEvent>) {
|
||||||
clientMap.set(uid, subscriber)
|
clientMap.set(uid, subscriber);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeClient(uid: number): void {
|
removeClient(uid: number): void {
|
||||||
const client = clientMap.get(uid)
|
const client = clientMap.get(uid);
|
||||||
client?.complete()
|
client?.complete();
|
||||||
clientMap.delete(uid)
|
clientMap.delete(uid);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToClient(uid: number, data: MessageEvent): void {
|
sendToClient(uid: number, data: MessageEvent): void {
|
||||||
const client = clientMap.get(uid)
|
const client = clientMap.get(uid);
|
||||||
client?.next?.(data)
|
client?.next?.(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToAll(data: MessageEvent): void {
|
sendToAll(data: MessageEvent): void {
|
||||||
clientMap.forEach((client) => {
|
clientMap.forEach(client => {
|
||||||
client.next(data)
|
client.next(data);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,10 +45,10 @@ export class SseService {
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
async noticeClientToUpdateMenusByUserIds(uid: number | number[]) {
|
async noticeClientToUpdateMenusByUserIds(uid: number | number[]) {
|
||||||
const userIds = [].concat(uid) as number[]
|
const userIds = [].concat(uid) as number[];
|
||||||
userIds.forEach((uid) => {
|
userIds.forEach(uid => {
|
||||||
this.sendToClient(uid, { type: 'updatePermsAndMenus' })
|
this.sendToClient(uid, { type: 'updatePermsAndMenus' });
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,9 +61,9 @@ export class SseService {
|
||||||
id: In(menuIds),
|
id: In(menuIds),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
const roleIds = roleMenus.map(n => n.id).concat(ROOT_ROLE_ID)
|
const roleIds = roleMenus.map(n => n.id).concat(ROOT_ROLE_ID);
|
||||||
await this.noticeClientToUpdateMenusByRoleIds(roleIds)
|
await this.noticeClientToUpdateMenusByRoleIds(roleIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,10 +76,10 @@ export class SseService {
|
||||||
id: In(roleIds),
|
id: In(roleIds),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
if (users) {
|
if (users) {
|
||||||
const userIds = users.map(n => n.id)
|
const userIds = users.map(n => n.id);
|
||||||
await this.noticeClientToUpdateMenusByUserIds(userIds)
|
await this.noticeClientToUpdateMenusByUserIds(userIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue