refactor: format

This commit is contained in:
louis 2024-02-28 17:02:46 +08:00
parent aeb25ac816
commit a98e122fbc
228 changed files with 4787 additions and 4615 deletions

7
.eslintignore Normal file
View File

@ -0,0 +1,7 @@
node_modules
dist/
config
build/
.eslintrc.js
package.json
tsconfig**.json

22
.eslintrc.cjs Normal file
View File

@ -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,
},
};

10
.prettierignore Normal file
View File

@ -0,0 +1,10 @@
/dist/*
.local
.output.js
/node_modules/**
**/*.svg
**/*.sh
/public/*
test/**/*

11
.prettierrc.cjs Normal file
View File

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

View File

@ -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 '签订日期',

View File

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

View File

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

View File

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

View File

@ -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();
}) });

View File

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

View File

@ -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>>({
}, },
], ],
}, },
}), })
) );
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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 pathurl params * request pathurl 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;
}) });

View File

@ -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 格式不正确');
},
})
);
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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[];
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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`}`);
}, })
), );
)
} }
} }

View File

@ -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);
}), })
) );
} }
} }

View File

@ -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);
}), })
) );
} }
} }

View File

@ -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>[];
} }

View File

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

View File

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

View File

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

View File

@ -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,
} };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:';

View File

@ -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 = '的副本';

View File

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

View File

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

View File

@ -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`)
}
})
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
) {} ) {}
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -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],
}), }),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}, });
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
// }
}

View File

@ -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[]>
}

View File

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

View File

@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class ContractService {}

View File

@ -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: '/',
}) });
} }
} }

View File

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

View File

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

View File

@ -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,
)
} }
} }

View File

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

View File

@ -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],
}) })

View File

@ -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,
} };
} }
} }

View File

@ -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,
} };
} }
} }

View File

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

View File

@ -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: [],

View File

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