feat(api):
1. 注册接口 2. 邮箱验证 3. 发送验证码
This commit is contained in:
parent
9afe6d756f
commit
804d14d8fd
@ -28,6 +28,7 @@ export default tseslint.config(
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||
},
|
||||
},
|
||||
|
418
star-tune/package-lock.json
generated
418
star-tune/package-lock.json
generated
@ -9,12 +9,21 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-fastify": "^11.1.2",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"drizzle-orm": "^0.44.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.14.1",
|
||||
"nest-winston": "^1.10.2",
|
||||
"redis": "^5.1.1",
|
||||
@ -1745,6 +1754,22 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/accept-negotiator": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz",
|
||||
"integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@fastify/ajv-compiler": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz",
|
||||
@ -1929,6 +1954,65 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/send": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/send/-/send-4.0.0.tgz",
|
||||
"integrity": "sha512-eJjKDxyBnZ1iMHcmwYWG5wSA/yzVY/yrBy3Upd2+hc0omcK13tWeXRcbF28zEcbl+Z2kXEgMzJ5Rb/gXGWx9Rg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@lukeed/ms": "^2.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"fast-decode-uri-component": "^1.0.1",
|
||||
"http-errors": "^2.0.0",
|
||||
"mime": "^3"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/send/node_modules/mime": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/mime/-/mime-3.0.0.tgz",
|
||||
"integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@fastify/static": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@fastify/static/-/static-8.2.0.tgz",
|
||||
"integrity": "sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fastify"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fastify"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fastify/accept-negotiator": "^2.0.0",
|
||||
"@fastify/send": "^4.0.0",
|
||||
"content-disposition": "^0.5.4",
|
||||
"fastify-plugin": "^5.0.0",
|
||||
"fastq": "^1.17.1",
|
||||
"glob": "^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -2317,7 +2401,6 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
@ -2335,7 +2418,6 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@ -2348,14 +2430,12 @@
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
@ -2373,7 +2453,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
@ -2937,6 +3016,21 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/ms": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@lukeed/ms/-/ms-2.0.2.tgz",
|
||||
"integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@microsoft/tsdoc": {
|
||||
"version": "0.15.1",
|
||||
"resolved": "https://registry.npmmirror.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz",
|
||||
"integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@napi-rs/nice": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/@napi-rs/nice/-/nice-1.0.1.tgz",
|
||||
@ -3544,6 +3638,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
|
||||
"integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"class-transformer": "^0.4.0 || ^0.5.0",
|
||||
"class-validator": "^0.13.0 || ^0.14.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/platform-express": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@nestjs/platform-express/-/platform-express-11.1.2.tgz",
|
||||
@ -3699,6 +3813,39 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/swagger": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@nestjs/swagger/-/swagger-11.2.0.tgz",
|
||||
"integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@microsoft/tsdoc": "0.15.1",
|
||||
"@nestjs/mapped-types": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"lodash": "4.17.21",
|
||||
"path-to-regexp": "8.2.0",
|
||||
"swagger-ui-dist": "5.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fastify/static": "^8.0.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"class-transformer": "*",
|
||||
"class-validator": "*",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@fastify/static": {
|
||||
"optional": true
|
||||
},
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/testing": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@nestjs/testing/-/testing-11.1.2.tgz",
|
||||
@ -3877,6 +4024,13 @@
|
||||
"@redis/client": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@sec-ant/readable-stream": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
|
||||
@ -4429,6 +4583,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jsonwebtoken": {
|
||||
"version": "9.0.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz",
|
||||
"integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/ms": "*",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/@types/methods/-/methods-1.1.4.tgz",
|
||||
@ -4436,11 +4600,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ms": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz",
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.24",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.24.tgz",
|
||||
"integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
@ -4483,6 +4652,12 @@
|
||||
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/validator": {
|
||||
"version": "13.15.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.1.tgz",
|
||||
"integrity": "sha512-9gG6ogYcoI2mCMLdcO0NYI0AYrbxIjv0MDmy/5Ywo6CpWWrqYayc+mmgxRsCgtcGJm9BSbXkMsmxGah1iGHAAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.33",
|
||||
"resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.33.tgz",
|
||||
@ -5726,7 +5901,6 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
@ -5739,7 +5913,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@ -5828,7 +6001,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true,
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-timsort": {
|
||||
@ -6022,7 +6194,6 @@
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
@ -6054,6 +6225,20 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/bin-version": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/bin-version/-/bin-version-6.0.0.tgz",
|
||||
@ -6253,6 +6438,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@ -6468,6 +6659,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-transformer": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmmirror.com/class-transformer/-/class-transformer-0.5.1.tgz",
|
||||
"integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/class-validator": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmmirror.com/class-validator/-/class-validator-0.14.2.tgz",
|
||||
"integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/validator": "^13.11.8",
|
||||
"libphonenumber-js": "^1.11.1",
|
||||
"validator": "^13.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
@ -6627,7 +6835,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@ -6764,7 +6971,6 @@
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
@ -6930,6 +7136,12 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
|
||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz",
|
||||
@ -7054,8 +7266,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -7295,9 +7505,17 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
|
||||
@ -7346,7 +7564,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/enabled": {
|
||||
@ -7512,9 +7729,7 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
@ -8374,7 +8589,6 @@
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
@ -8664,7 +8878,6 @@
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/glob/-/glob-11.0.1.tgz",
|
||||
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
@ -8708,7 +8921,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@ -8718,7 +8930,6 @@
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
@ -8877,8 +9088,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
@ -9079,7 +9288,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -9284,7 +9492,6 @@
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
@ -9987,7 +10194,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
@ -10089,6 +10295,49 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.2.tgz",
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
|
||||
@ -10149,6 +10398,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.12.8",
|
||||
"resolved": "https://registry.npmmirror.com/libphonenumber-js/-/libphonenumber-js-1.12.8.tgz",
|
||||
"integrity": "sha512-f1KakiQJa9tdc7w1phC2ST+DyxWimy9c3g3yeF+84QtEanJr2K77wAmBPP22riU05xldniHsvXuflnLZ4oysqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/light-my-request": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz",
|
||||
@ -10253,6 +10508,42 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.memoize": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||
@ -10267,6 +10558,12 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/log-symbols": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz",
|
||||
@ -10584,7 +10881,6 @@
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
@ -10783,6 +11079,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-8.3.1.tgz",
|
||||
"integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "1.11.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-emoji/-/node-emoji-1.11.0.tgz",
|
||||
@ -10793,6 +11098,17 @@
|
||||
"lodash": "^4.17.21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@ -11065,7 +11381,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
@ -11151,7 +11466,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.0.tgz",
|
||||
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
@ -11168,7 +11482,6 @@
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
@ -11913,7 +12226,6 @@
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"devOptional": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@ -12130,9 +12442,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
@ -12235,7 +12545,6 @@
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@ -12403,8 +12712,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -12489,7 +12796,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@ -12505,7 +12811,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@ -12520,7 +12825,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -12530,7 +12834,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@ -12543,7 +12846,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -12553,7 +12855,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@ -12566,7 +12867,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
@ -12583,7 +12883,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@ -12596,7 +12895,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -12724,6 +13022,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz",
|
||||
"integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-observable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/symbol-observable/-/symbol-observable-4.0.0.tgz",
|
||||
@ -13055,8 +13362,6 @@
|
||||
"resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
@ -13421,7 +13726,6 @@
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
@ -13514,6 +13818,15 @@
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.15.15",
|
||||
"resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.15.tgz",
|
||||
"integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
|
||||
@ -13908,7 +14221,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@ -13926,7 +14238,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -13936,7 +14247,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
|
@ -28,12 +28,21 @@
|
||||
"sqlV": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/mapped-types": "*",
|
||||
"@nestjs/platform-fastify": "^11.1.2",
|
||||
"@nestjs/swagger": "^11.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"bcrypt": "^6.0.0",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"drizzle-orm": "^0.44.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.14.1",
|
||||
"nest-winston": "^1.10.2",
|
||||
"redis": "^5.1.1",
|
||||
|
@ -1,22 +0,0 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(private readonly appService: AppService) {}
|
||||
|
||||
@Get()
|
||||
async getHello(): Promise<object> {
|
||||
return await this.appService.getHello();
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
async getHello(): Promise<object> {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
let str = 'Hello World!';
|
||||
str = str.repeat(1000);
|
||||
return {str};
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import configuration from '@/config/configuration';
|
||||
import configuration from '@/common/config/configuration';
|
||||
|
||||
@Module({
|
||||
imports: [
|
@ -25,8 +25,18 @@ export default () => {
|
||||
ttl: parseInt(process.env.REDIS_TTL || '3600', 10),
|
||||
},
|
||||
jwt: {
|
||||
secret: process.env.JWT_SECRET || 'your-secret-key',
|
||||
expiresIn: process.env.JWT_EXPIRATION || '1d',
|
||||
secret: process.env.JWT_SECRET || 'star-tune-salt',
|
||||
accessExpiresIn: process.env.JWT_ACCESS_EXPIRES || '20m',
|
||||
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES || '14d',
|
||||
whitelist: [
|
||||
'/user/login',
|
||||
'/user/register',
|
||||
'/user/refreshToken',
|
||||
'/',
|
||||
// '/module',
|
||||
'/docs*',
|
||||
'/docs/json',
|
||||
],
|
||||
},
|
||||
logging: {
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
@ -36,11 +46,22 @@ export default () => {
|
||||
console: (process.env.LOG_CONSOLE || 'true') === 'true',
|
||||
},
|
||||
password: {
|
||||
userDefaultPassword: (process.env.USER_DEFAULT_PASSWORD || 'StarTune123') as string,
|
||||
salt: (process.env.PASSWORD_SALT || 'star-tune-salt') as string,
|
||||
iterations: parseInt(process.env.PASSWORD_ITERATIONS || '10000', 10),
|
||||
userDefaultPassword:
|
||||
process.env.USER_DEFAULT_PASSWORD || 'StarTune123',
|
||||
salt: process.env.PASSWORD_SALT || 'star-tune-salt',
|
||||
iterations: parseInt(
|
||||
process.env.PASSWORD_ITERATIONS || '10000',
|
||||
10,
|
||||
),
|
||||
keylen: parseInt(process.env.PASSWORD_KEYLEN || '64', 10),
|
||||
digest: process.env.PASSWORD_DIGEST || 'sha512',
|
||||
},
|
||||
// 邮箱
|
||||
email: {
|
||||
// 验证码过期时间
|
||||
codeEX: 300,
|
||||
// 验证码冷却时间
|
||||
codeEP: 60,
|
||||
},
|
||||
};
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InitService } from './init.service';
|
||||
import { DatabaseModule } from '../database/database.module';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
import { UtilsModule } from '../utils/utils.module';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
@Module({
|
||||
imports: [
|
||||
DatabaseModule,
|
||||
LoggerModule,
|
||||
UtilsModule,
|
||||
ConfigModule,
|
||||
],
|
||||
providers: [InitService],
|
||||
exports: [InitService],
|
||||
})
|
||||
export class InitModule {}
|
@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as winston from 'winston';
|
||||
import * as DailyRotateFile from 'winston-daily-rotate-file';
|
||||
import { join } from 'path';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { allColors } from '@/type/enum';
|
||||
|
||||
// 定义配置接口
|
||||
interface LogConfig {
|
||||
@ -50,12 +51,12 @@ const createCustomFormat = (environment: Environment) => {
|
||||
message: string | Error;
|
||||
context?: string;
|
||||
}) => {
|
||||
const { timestamp, level, message, context} = info;
|
||||
|
||||
const { timestamp, level, message, context } = info;
|
||||
|
||||
// 处理错误信息
|
||||
let errorMessage = message;
|
||||
let errorStack: string | undefined;
|
||||
|
||||
|
||||
// 如果消息是错误对象或包含堆栈信息
|
||||
if (message instanceof Error) {
|
||||
errorMessage = message.message;
|
||||
@ -98,13 +99,18 @@ const createCustomFormat = (environment: Environment) => {
|
||||
: contextContent.padEnd(FORMAT_CONFIG.CONTEXT_LENGTH, ' ');
|
||||
|
||||
// 处理错误堆栈信息
|
||||
const stackInfo = errorStack ? `\n${levelColor}${errorStack}${reset}` : '';
|
||||
const stackInfo = errorStack
|
||||
? `\n${levelColor}${errorStack}${reset}`
|
||||
: '';
|
||||
|
||||
// 返回格式化的日志字符串
|
||||
let paddedContext = formattedContext;
|
||||
if(formattedContext.trim() === 'RequestLogger') {
|
||||
paddedContext = `[${levelColors['debug']}${bold}${formattedContext}${reset}]`;
|
||||
}else{
|
||||
if (formattedContext.trim() === 'RequestLogger') {
|
||||
paddedContext = `[${levelColors['info']}${bold}${formattedContext}${reset}]`;
|
||||
} else if (formattedContext.trim() === 'SQL_Query') {
|
||||
paddedContext = `[${allColors.text.magenta}${allColors.bg.bgWhite}${bold}${formattedContext}${reset}]`;
|
||||
errorMessage = `${allColors.text.magenta}${errorMessage}`;
|
||||
} else {
|
||||
paddedContext = `[${formattedContext}]`;
|
||||
}
|
||||
return `${envColor}[${envTag}] ${getPaddedPid()}${reset} - ${timestamp} ${levelColor}${upperLevel} ${reset}${paddedContext} ${levelColor}${errorMessage}${reset}${stackInfo}`;
|
||||
@ -185,6 +191,7 @@ const createLoggerOptions = (
|
||||
};
|
||||
};
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
WinstonModule.forRootAsync({
|
||||
|
@ -28,4 +28,4 @@ export class CustomLogger implements LoggerService {
|
||||
info(message: string, context?: string) {
|
||||
this.log(message, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UtilsServer } from '@/common/utils/utils.server';
|
||||
|
||||
interface ResponseWithHeader {
|
||||
_header?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RequestLoggerMiddleware implements NestMiddleware {
|
||||
constructor(
|
||||
private readonly logger: CustomLogger,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly utilsServer: UtilsServer,
|
||||
) {
|
||||
const environment = this.configService.get<string>('environment');
|
||||
// 只在开发环境下记录请求日志
|
||||
if (environment === 'development') {
|
||||
this.use = this.useBack;
|
||||
}
|
||||
}
|
||||
|
||||
use(req: FastifyRequest, res: FastifyReply, next: () => void) {
|
||||
}
|
||||
|
||||
useBack(req: FastifyRequest & FastifyRequest['raw'], res: FastifyReply & FastifyReply['raw'], next: () => void) {
|
||||
const requestStart = process.hrtime();
|
||||
const ip = req.id?.slice(4).padEnd(6).toUpperCase();
|
||||
this.logger.debug(`<=== ${ip} | ${req.method} ${req.url} ${req.ip}`, 'RequestLogger');
|
||||
res.on('finish', () => {
|
||||
const [seconds, nanoseconds] = process.hrtime(requestStart);
|
||||
const duration = seconds * 1000 + nanoseconds / 1000000;
|
||||
const header = (res as unknown as ResponseWithHeader)._header;
|
||||
const contentLength = header?.match(/content-length: (\d+)/i)?.[1];
|
||||
const size = contentLength ? this.utilsServer.formatFileSize(parseInt(contentLength, 10)) : '0 B';
|
||||
const contentType = header?.match(/content-type: (\w+\/\w+)/i)?.[1];
|
||||
|
||||
this.logger.debug(
|
||||
`===> ${ip} | ${req.method} ${req.url} ${res.statusCode} ${duration.toFixed(2)}ms [${size}] [${contentType}]`,
|
||||
'RequestLogger'
|
||||
);
|
||||
})
|
||||
next()
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { RedisService } from './redis.service';
|
||||
import { LoggerModule } from '../logger/logger.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [LoggerModule],
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
@ -1,84 +0,0 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
import { CustomLogger } from '../logger/logger.service';
|
||||
|
||||
@Injectable()
|
||||
export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||||
public client: RedisClientType;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private logger: CustomLogger,
|
||||
) {
|
||||
const config = this.configService.get('redis');
|
||||
this.client = createClient({
|
||||
name: config.connectName,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
url: `redis://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`,
|
||||
});
|
||||
this.client.on('connect', async () => {
|
||||
this.logger.log(await this.client.set('SI HI', this.configService.get('redis.connectName') as string) || '', 'InitRedis');
|
||||
});
|
||||
|
||||
this.client.on('error', (err) => {
|
||||
this.logger.error(err.message, err.stack, 'InitRedis');
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.client.connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.client.quit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值对
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||
const expireTime = ttl || this.configService.get('redis.ttl');
|
||||
await this.client.set(key, value, { EX: expireTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
* @param key 键
|
||||
* @returns 值
|
||||
*/
|
||||
async get(key: string): Promise<string | null> {
|
||||
return await this.client.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
* @param key 键
|
||||
*/
|
||||
async del(key: string): Promise<void> {
|
||||
await this.client.del(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
* @param key 键
|
||||
* @returns 是否存在
|
||||
*/
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return (await this.client.exists(key)) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键的过期时间
|
||||
* @param key 键
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
await this.client.expire(key, ttl);
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { Module, Global } from '@nestjs/common';
|
||||
import { UtilsServer } from './utils.server';
|
||||
import { UtilsService } from './utils.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [UtilsServer],
|
||||
exports: [UtilsServer],
|
||||
providers: [UtilsService],
|
||||
exports: [UtilsService],
|
||||
})
|
||||
export class UtilsModule {}
|
||||
export class UtilsModule {}
|
||||
|
@ -1,57 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
@Injectable()
|
||||
export class UtilsServer {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
* @param password 原始密码
|
||||
* @returns 加密后的密码
|
||||
*/
|
||||
async hashPassword(password: string): Promise<string> {
|
||||
const salt = this.configService.get<string>('password.salt');
|
||||
const iterations = this.configService.get<number>('password.iterations');
|
||||
// 我们设置了 keylen: 128,这表示 128 字节
|
||||
// 每个字节转换为 2 个十六进制字符
|
||||
// 所以最终输出长度是 128 * 2 = 256 个字符
|
||||
const keylen = this.configService.get<number>('password.keylen');
|
||||
const digest = this.configService.get<string>('password.digest');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.pbkdf2(password, salt as string, iterations as number, keylen as number, digest as string, (err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
resolve(derivedKey.toString('hex'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* @param password 原始密码
|
||||
* @param hashedPassword 加密后的密码
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
async verifyPassword(password: string, hashedPassword: string): Promise<boolean> {
|
||||
const hashedInput = await this.hashPassword(password);
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(hashedInput, 'hex'),
|
||||
Buffer.from(hashedPassword, 'hex')
|
||||
);
|
||||
}
|
||||
|
||||
formatFileSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
}
|
122
star-tune/src/common/utils/utils.service.ts
Normal file
122
star-tune/src/common/utils/utils.service.ts
Normal file
@ -0,0 +1,122 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import * as crypto from 'crypto';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { sign, verify } from 'jsonwebtoken';
|
||||
import { type StringValue } from 'ms';
|
||||
import { UserGuard } from '@/type/userGuard';
|
||||
|
||||
export type SendEmailOptions = {
|
||||
to: string;
|
||||
subject: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UtilsService {
|
||||
constructor(
|
||||
private readonly configService: ConfigService,
|
||||
private readonly logger: CustomLogger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
* @param password 原始密码
|
||||
* @returns 加密后的密码
|
||||
*/
|
||||
async hashPassword(password: string): Promise<string> {
|
||||
const salt = this.configService.get<string>('password.salt');
|
||||
const iterations = this.configService.get<number>(
|
||||
'password.iterations',
|
||||
);
|
||||
// 我们设置了 keylen: 128,这表示 128 字节
|
||||
// 每个字节转换为 2 个十六进制字符
|
||||
// 所以最终输出长度是 128 * 2 = 256 个字符
|
||||
const keylen = this.configService.get<number>('password.keylen');
|
||||
const digest = this.configService.get<string>('password.digest');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
crypto.pbkdf2(
|
||||
password,
|
||||
salt as string,
|
||||
iterations as number,
|
||||
keylen as number,
|
||||
digest as string,
|
||||
(err, derivedKey) => {
|
||||
if (err) reject(err);
|
||||
resolve(derivedKey.toString('hex'));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码
|
||||
* @param password 原始密码
|
||||
* @param hashedPassword 加密后的密码
|
||||
* @returns 是否匹配
|
||||
*/
|
||||
async verifyPassword(
|
||||
password: string,
|
||||
hashedPassword: string,
|
||||
): Promise<boolean> {
|
||||
const hashedInput = await this.hashPassword(password);
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(hashedInput, 'hex'),
|
||||
Buffer.from(hashedPassword, 'hex'),
|
||||
);
|
||||
}
|
||||
|
||||
formatFileSize(bytes: number): string {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
// 发送邮箱验证码
|
||||
async sendEmail(sendEmailOptions: SendEmailOptions) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
this.logger.log(
|
||||
`发送邮箱验证码: ${sendEmailOptions.to} ${sendEmailOptions.subject} ${sendEmailOptions.text}`,
|
||||
'UtilsService',
|
||||
);
|
||||
}
|
||||
|
||||
// 生成访问令牌
|
||||
generateAccessToken(userGuard: UserGuard): string {
|
||||
// 盐
|
||||
const secret =
|
||||
this.configService.get<string>('jwt.secret') || 'star-tune-salt';
|
||||
// 过期时间
|
||||
const expiresIn =
|
||||
this.configService.get<StringValue>('jwt.accessExpiresIn') || '20m';
|
||||
return sign(userGuard, secret, { expiresIn });
|
||||
}
|
||||
// 生成刷新令牌
|
||||
generateRefreshToken(userGuard: UserGuard): string {
|
||||
const secret =
|
||||
this.configService.get<string>('jwt.secret') || 'star-tune-salt';
|
||||
const expiresIn =
|
||||
this.configService.get<StringValue>('jwt.refreshExpiresIn') ||
|
||||
'14d';
|
||||
return sign(userGuard, secret, { expiresIn });
|
||||
}
|
||||
|
||||
// 验证访问令牌
|
||||
verifyToken(token: string): UserGuard {
|
||||
const secret =
|
||||
this.configService.get<string>('jwt.secret') || 'star-tune-salt';
|
||||
try {
|
||||
return verify(token, secret) as UserGuard;
|
||||
} catch {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
}
|
||||
}
|
40
star-tune/src/decorators/transform.decorator.ts
Normal file
40
star-tune/src/decorators/transform.decorator.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
/**
|
||||
* 转换为大写字母
|
||||
* @returns PropertyDecorator
|
||||
*/
|
||||
export function ToUpperCase(): PropertyDecorator {
|
||||
return Transform(({ value }) => {
|
||||
if (value && typeof value === 'string') {
|
||||
return value.toUpperCase();
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为小写字母
|
||||
* @returns PropertyDecorator
|
||||
*/
|
||||
export function ToLowerCase(): PropertyDecorator {
|
||||
return Transform(({ value }) => {
|
||||
if (value && typeof value === 'string') {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除字符串两端空格
|
||||
* @returns PropertyDecorator
|
||||
*/
|
||||
export function Trim(): PropertyDecorator {
|
||||
return Transform(({ value }) => {
|
||||
if (value && typeof value === 'string') {
|
||||
return value.trim();
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
10
star-tune/src/decorators/user.decorator.ts
Normal file
10
star-tune/src/decorators/user.decorator.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
|
||||
export const User = createParamDecorator(
|
||||
(data: string, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
59
star-tune/src/decorators/validation.decorator.ts
Normal file
59
star-tune/src/decorators/validation.decorator.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import {
|
||||
registerDecorator,
|
||||
ValidationOptions,
|
||||
ValidationArguments,
|
||||
} from 'class-validator';
|
||||
|
||||
/**
|
||||
* 验证字符串是否只包含字母和数字
|
||||
* @param validationOptions 验证选项
|
||||
* @returns PropertyDecorator
|
||||
*/
|
||||
export function IsAlphanumeric(validationOptions?: ValidationOptions) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isAlphanumeric',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any) {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
/^[a-zA-Z0-9]+$/.test(value)
|
||||
);
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${args.property} 只能包含字母和数字`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证字符串是否不包含特殊字符
|
||||
* @param validationOptions 验证选项
|
||||
* @returns PropertyDecorator
|
||||
*/
|
||||
export function NoSpecialCharacters(validationOptions?: ValidationOptions) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'noSpecialCharacters',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any) {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
/^[a-zA-Z0-9\u4e00-\u9fa5]+$/.test(value)
|
||||
);
|
||||
},
|
||||
defaultMessage(args: ValidationArguments) {
|
||||
return `${args.property} 不能包含特殊字符`;
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
62
star-tune/src/drizzle/0000_perpetual_terrax.sql
Normal file
62
star-tune/src/drizzle/0000_perpetual_terrax.sql
Normal file
@ -0,0 +1,62 @@
|
||||
-- Current sql file was generated after introspecting the database
|
||||
-- If you want to run this migration please uncomment this code before executing migrations
|
||||
/*
|
||||
CREATE TABLE `user` (
|
||||
`user_id` bigint unsigned NOT NULL,
|
||||
`status` tinyint unsigned NOT NULL DEFAULT 0,
|
||||
`user_type` tinyint unsigned NOT NULL DEFAULT 0,
|
||||
`user_role` int unsigned NOT NULL DEFAULT 0,
|
||||
`username` varchar(50) NOT NULL,
|
||||
`email` varchar(320) NOT NULL,
|
||||
`nickname` varchar(50) NOT NULL DEFAULT '',
|
||||
`gender` tinyint unsigned NOT NULL DEFAULT 0,
|
||||
`birthdate` date,
|
||||
`address` varchar(255) DEFAULT '',
|
||||
`avatar` varchar(255) DEFAULT '',
|
||||
`background_image` varchar(255) DEFAULT '',
|
||||
`created_by` enum('SELF','ADMIN') NOT NULL,
|
||||
`created_at` datetime(3) NOT NULL DEFAULT (now()),
|
||||
`updated_at` datetime(3) NOT NULL DEFAULT (now()),
|
||||
`username_updated_at` datetime(3),
|
||||
`is_deleted` tinyint unsigned NOT NULL DEFAULT 0,
|
||||
`deleted_at` datetime(3),
|
||||
CONSTRAINT `user_user_id` PRIMARY KEY(`user_id`),
|
||||
CONSTRAINT `idx_unique_username` UNIQUE(`username`),
|
||||
CONSTRAINT `idx_unique_active_email` UNIQUE(`email`,`is_deleted`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_password` (
|
||||
`user_id` bigint unsigned NOT NULL,
|
||||
`password_hash` varchar(128) NOT NULL,
|
||||
`previous_password_hash` varchar(128),
|
||||
`last_updated_at` datetime(3) NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `user_password_user_id` PRIMARY KEY(`user_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_profile` (
|
||||
`user_id` bigint unsigned NOT NULL,
|
||||
`profile` mediumtext NOT NULL,
|
||||
`created_at` datetime(3) NOT NULL DEFAULT (now()),
|
||||
`updated_at` datetime(3) NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `user_profile_user_id` PRIMARY KEY(`user_id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `user_signature_history` (
|
||||
`id` bigint unsigned AUTO_INCREMENT NOT NULL,
|
||||
`user_id` bigint unsigned NOT NULL,
|
||||
`signature` varchar(100) NOT NULL,
|
||||
`signature_tag` varchar(100) NOT NULL,
|
||||
`created_at` datetime(3) NOT NULL DEFAULT (now()),
|
||||
CONSTRAINT `user_signature_history_id` PRIMARY KEY(`id`)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX `idx_created_at` ON `user` (`created_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_email` ON `user` (`email`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_status` ON `user` (`status`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_type` ON `user` (`user_type`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_username_updated` ON `user` (`username_updated_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_id` ON `user_password` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_id` ON `user_profile` (`user_id`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_created` ON `user_signature_history` (`user_id`,`created_at`);--> statement-breakpoint
|
||||
CREATE INDEX `idx_user_id` ON `user_signature_history` (`user_id`);
|
||||
*/
|
435
star-tune/src/drizzle/meta/0000_snapshot.json
Normal file
435
star-tune/src/drizzle/meta/0000_snapshot.json
Normal file
@ -0,0 +1,435 @@
|
||||
{
|
||||
"id": "00000000-0000-0000-0000-000000000000",
|
||||
"prevId": "",
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"tables": {
|
||||
"user": {
|
||||
"name": "user",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"autoincrement": false,
|
||||
"name": "user_id",
|
||||
"type": "bigint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"status": {
|
||||
"default": 0,
|
||||
"autoincrement": false,
|
||||
"name": "status",
|
||||
"type": "tinyint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_type": {
|
||||
"default": 0,
|
||||
"autoincrement": false,
|
||||
"name": "user_type",
|
||||
"type": "tinyint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_role": {
|
||||
"default": 0,
|
||||
"autoincrement": false,
|
||||
"name": "user_role",
|
||||
"type": "int unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"username": {
|
||||
"autoincrement": false,
|
||||
"name": "username",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"autoincrement": false,
|
||||
"name": "email",
|
||||
"type": "varchar(320)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"nickname": {
|
||||
"default": "''",
|
||||
"autoincrement": false,
|
||||
"name": "nickname",
|
||||
"type": "varchar(50)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"gender": {
|
||||
"default": 0,
|
||||
"autoincrement": false,
|
||||
"name": "gender",
|
||||
"type": "tinyint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"birthdate": {
|
||||
"autoincrement": false,
|
||||
"name": "birthdate",
|
||||
"type": "date",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"address": {
|
||||
"default": "''",
|
||||
"autoincrement": false,
|
||||
"name": "address",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"avatar": {
|
||||
"default": "''",
|
||||
"autoincrement": false,
|
||||
"name": "avatar",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"background_image": {
|
||||
"default": "''",
|
||||
"autoincrement": false,
|
||||
"name": "background_image",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_by": {
|
||||
"autoincrement": false,
|
||||
"name": "created_by",
|
||||
"type": "enum('SELF','ADMIN')",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"default": "(now())",
|
||||
"autoincrement": false,
|
||||
"name": "created_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"default": "(now())",
|
||||
"autoincrement": false,
|
||||
"name": "updated_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"username_updated_at": {
|
||||
"autoincrement": false,
|
||||
"name": "username_updated_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"is_deleted": {
|
||||
"default": 0,
|
||||
"autoincrement": false,
|
||||
"name": "is_deleted",
|
||||
"type": "tinyint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"deleted_at": {
|
||||
"autoincrement": false,
|
||||
"name": "deleted_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_user_id": {
|
||||
"name": "user_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_created_at": {
|
||||
"name": "idx_created_at",
|
||||
"columns": [
|
||||
"created_at"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_email": {
|
||||
"name": "idx_email",
|
||||
"columns": [
|
||||
"email"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_status": {
|
||||
"name": "idx_status",
|
||||
"columns": [
|
||||
"status"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_user_type": {
|
||||
"name": "idx_user_type",
|
||||
"columns": [
|
||||
"user_type"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_username_updated": {
|
||||
"name": "idx_username_updated",
|
||||
"columns": [
|
||||
"username_updated_at"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"idx_unique_username": {
|
||||
"name": "idx_unique_username",
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
},
|
||||
"idx_unique_active_email": {
|
||||
"name": "idx_unique_active_email",
|
||||
"columns": [
|
||||
"email",
|
||||
"is_deleted"
|
||||
]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user_password": {
|
||||
"name": "user_password",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"autoincrement": false,
|
||||
"name": "user_id",
|
||||
"type": "bigint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"password_hash": {
|
||||
"autoincrement": false,
|
||||
"name": "password_hash",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"previous_password_hash": {
|
||||
"autoincrement": false,
|
||||
"name": "previous_password_hash",
|
||||
"type": "varchar(128)",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"last_updated_at": {
|
||||
"default": "(now())",
|
||||
"autoincrement": false,
|
||||
"name": "last_updated_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_password_user_id": {
|
||||
"name": "user_password_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_user_id": {
|
||||
"name": "idx_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user_profile": {
|
||||
"name": "user_profile",
|
||||
"columns": {
|
||||
"user_id": {
|
||||
"autoincrement": false,
|
||||
"name": "user_id",
|
||||
"type": "bigint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"profile": {
|
||||
"autoincrement": false,
|
||||
"name": "profile",
|
||||
"type": "mediumtext",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"default": "(now())",
|
||||
"autoincrement": false,
|
||||
"name": "created_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"default": "(now())",
|
||||
"autoincrement": false,
|
||||
"name": "updated_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_profile_user_id": {
|
||||
"name": "user_profile_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_user_id": {
|
||||
"name": "idx_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
},
|
||||
"user_signature_history": {
|
||||
"name": "user_signature_history",
|
||||
"columns": {
|
||||
"id": {
|
||||
"autoincrement": true,
|
||||
"name": "id",
|
||||
"type": "bigint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"autoincrement": false,
|
||||
"name": "user_id",
|
||||
"type": "bigint unsigned",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"signature": {
|
||||
"autoincrement": false,
|
||||
"name": "signature",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"signature_tag": {
|
||||
"autoincrement": false,
|
||||
"name": "signature_tag",
|
||||
"type": "varchar(100)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"default": "(now())",
|
||||
"autoincrement": false,
|
||||
"name": "created_at",
|
||||
"type": "datetime(3)",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {
|
||||
"user_signature_history_id": {
|
||||
"name": "user_signature_history_id",
|
||||
"columns": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"idx_user_created": {
|
||||
"name": "idx_user_created",
|
||||
"columns": [
|
||||
"user_id",
|
||||
"created_at"
|
||||
],
|
||||
"isUnique": false
|
||||
},
|
||||
"idx_user_id": {
|
||||
"name": "idx_user_id",
|
||||
"columns": [
|
||||
"user_id"
|
||||
],
|
||||
"isUnique": false
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {
|
||||
"user": {
|
||||
"columns": {
|
||||
"created_at": {
|
||||
"isDefaultAnExpression": true
|
||||
},
|
||||
"updated_at": {
|
||||
"isDefaultAnExpression": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_password": {
|
||||
"columns": {
|
||||
"last_updated_at": {
|
||||
"isDefaultAnExpression": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_profile": {
|
||||
"columns": {
|
||||
"created_at": {
|
||||
"isDefaultAnExpression": true
|
||||
},
|
||||
"updated_at": {
|
||||
"isDefaultAnExpression": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signature_history": {
|
||||
"columns": {
|
||||
"created_at": {
|
||||
"isDefaultAnExpression": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
13
star-tune/src/drizzle/meta/_journal.json
Normal file
13
star-tune/src/drizzle/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "mysql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1748607208823,
|
||||
"tag": "0000_perpetual_terrax",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
2
star-tune/src/drizzle/relations.ts
Normal file
2
star-tune/src/drizzle/relations.ts
Normal file
@ -0,0 +1,2 @@
|
||||
import { relations } from 'drizzle-orm/relations';
|
||||
import {} from './schema';
|
119
star-tune/src/drizzle/schema.ts
Normal file
119
star-tune/src/drizzle/schema.ts
Normal file
@ -0,0 +1,119 @@
|
||||
import {
|
||||
mysqlTable,
|
||||
mysqlSchema,
|
||||
AnyMySqlColumn,
|
||||
index,
|
||||
primaryKey,
|
||||
unique,
|
||||
bigint,
|
||||
tinyint,
|
||||
int,
|
||||
varchar,
|
||||
date,
|
||||
mysqlEnum,
|
||||
datetime,
|
||||
mediumtext,
|
||||
} from 'drizzle-orm/mysql-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const user = mysqlTable(
|
||||
'user',
|
||||
{
|
||||
userId: bigint('user_id', { mode: 'number', unsigned: true }).notNull(),
|
||||
status: tinyint({ unsigned: true }).default(0).notNull(),
|
||||
userType: tinyint('user_type', { unsigned: true }).default(0).notNull(),
|
||||
userRole: int('user_role', { unsigned: true }).default(0).notNull(),
|
||||
username: varchar({ length: 50 }).notNull(),
|
||||
email: varchar({ length: 320 }).notNull(),
|
||||
nickname: varchar({ length: 50 }).default('').notNull(),
|
||||
gender: tinyint({ unsigned: true }).default(0).notNull(),
|
||||
// you can use { mode: 'date' }, if you want to have Date as type for this column
|
||||
birthdate: date({ mode: 'string' }),
|
||||
address: varchar({ length: 255 }).default(''),
|
||||
avatar: varchar({ length: 255 }).default(''),
|
||||
backgroundImage: varchar('background_image', { length: 255 }).default(
|
||||
'',
|
||||
),
|
||||
createdBy: mysqlEnum('created_by', ['SELF', 'ADMIN']).notNull(),
|
||||
createdAt: datetime('created_at', { mode: 'string', fsp: 3 })
|
||||
.default(sql`(now())`)
|
||||
.notNull(),
|
||||
updatedAt: datetime('updated_at', { mode: 'string', fsp: 3 })
|
||||
.default(sql`(now())`)
|
||||
.notNull(),
|
||||
usernameUpdatedAt: datetime('username_updated_at', {
|
||||
mode: 'string',
|
||||
fsp: 3,
|
||||
}),
|
||||
isDeleted: tinyint('is_deleted', { unsigned: true })
|
||||
.default(0)
|
||||
.notNull(),
|
||||
deletedAt: datetime('deleted_at', { mode: 'string', fsp: 3 }),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_created_at').on(table.createdAt),
|
||||
index('idx_email').on(table.email),
|
||||
index('idx_status').on(table.status),
|
||||
index('idx_user_type').on(table.userType),
|
||||
index('idx_username_updated').on(table.usernameUpdatedAt),
|
||||
primaryKey({ columns: [table.userId], name: 'user_user_id' }),
|
||||
unique('idx_unique_username').on(table.username),
|
||||
unique('idx_unique_active_email').on(table.email, table.isDeleted),
|
||||
],
|
||||
);
|
||||
|
||||
export const userPassword = mysqlTable(
|
||||
'user_password',
|
||||
{
|
||||
userId: bigint('user_id', { mode: 'number', unsigned: true }).notNull(),
|
||||
passwordHash: varchar('password_hash', { length: 128 }).notNull(),
|
||||
previousPasswordHash: varchar('previous_password_hash', {
|
||||
length: 128,
|
||||
}),
|
||||
lastUpdatedAt: datetime('last_updated_at', { mode: 'string', fsp: 3 })
|
||||
.default(sql`(now())`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.userId], name: 'user_password_user_id' }),
|
||||
],
|
||||
);
|
||||
|
||||
export const userProfile = mysqlTable(
|
||||
'user_profile',
|
||||
{
|
||||
userId: bigint('user_id', { mode: 'number', unsigned: true }).notNull(),
|
||||
profile: mediumtext().notNull(),
|
||||
createdAt: datetime('created_at', { mode: 'string', fsp: 3 })
|
||||
.default(sql`(now())`)
|
||||
.notNull(),
|
||||
updatedAt: datetime('updated_at', { mode: 'string', fsp: 3 })
|
||||
.default(sql`(now())`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.userId], name: 'user_profile_user_id' }),
|
||||
],
|
||||
);
|
||||
|
||||
export const userSignatureHistory = mysqlTable(
|
||||
'user_signature_history',
|
||||
{
|
||||
id: bigint({ mode: 'number', unsigned: true })
|
||||
.autoincrement()
|
||||
.notNull(),
|
||||
userId: bigint('user_id', { mode: 'number', unsigned: true }).notNull(),
|
||||
signature: varchar({ length: 100 }).notNull(),
|
||||
signatureTag: varchar('signature_tag', { length: 100 }).notNull(),
|
||||
createdAt: datetime('created_at', { mode: 'string', fsp: 3 })
|
||||
.default(sql`(now())`)
|
||||
.notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('idx_user_created').on(table.userId, table.createdAt),
|
||||
index('idx_user_id').on(table.userId),
|
||||
primaryKey({ columns: [table.id], name: 'user_signature_history_id' }),
|
||||
],
|
||||
);
|
57
star-tune/src/guards/auth.guard.ts
Normal file
57
star-tune/src/guards/auth.guard.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { RedisService } from '@/service/redis/redis.service';
|
||||
import { UtilsService } from '@/common/utils/utils.service';
|
||||
import { UserGuard } from '@/type/userGuard';
|
||||
import { FastifyRequest } from 'fastify';
|
||||
|
||||
// 扩展 FastifyRequest 类型以包含 user 属性
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
user?: UserGuard;
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private readonly config: ConfigService,
|
||||
private readonly redis: RedisService,
|
||||
private readonly utils: UtilsService,
|
||||
) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const request = context.switchToHttp().getRequest<FastifyRequest>();
|
||||
const path = request.url;
|
||||
|
||||
// 检查白名单路由
|
||||
const whitelist = this.config.get<string[]>('jwt.whitelist') || [];
|
||||
if (whitelist.some((route) => path.startsWith(route))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 获取并验证 token
|
||||
const authHeader = request.headers.authorization;
|
||||
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||
throw new UnauthorizedException('缺少访问令牌');
|
||||
}
|
||||
|
||||
const token = authHeader.split(' ')[1];
|
||||
let userGuard: UserGuard;
|
||||
|
||||
try {
|
||||
userGuard = this.utils.verifyToken(token);
|
||||
} catch {
|
||||
throw new UnauthorizedException('无效的访问令牌');
|
||||
}
|
||||
|
||||
// 将用户信息附加到请求对象
|
||||
request.user = userGuard;
|
||||
return true;
|
||||
}
|
||||
}
|
8
star-tune/src/init/init.module.ts
Normal file
8
star-tune/src/init/init.module.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { InitService } from '@/init/init.service';
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [InitService],
|
||||
exports: [InitService],
|
||||
})
|
||||
export class InitModule {}
|
@ -1,16 +1,16 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { DatabaseService } from '@/common/database/database.service';
|
||||
import { DatabaseService } from '@/service/database/database.service';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { user, userPassword, userProfile } from '@/drizzle/schema';
|
||||
import { UtilsServer } from '@/common/utils/utils.server';
|
||||
import { UtilsService } from '@/common/utils/utils.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
@Injectable()
|
||||
export class InitService implements OnModuleInit {
|
||||
constructor(
|
||||
private readonly dbService: DatabaseService,
|
||||
private readonly logger: CustomLogger,
|
||||
private readonly utilsServer: UtilsServer,
|
||||
private readonly utilsService: UtilsService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@ -24,7 +24,7 @@ export class InitService implements OnModuleInit {
|
||||
*/
|
||||
private async initRootUser() {
|
||||
const db = this.dbService.getDb();
|
||||
|
||||
|
||||
try {
|
||||
// 检查 root 用户是否存在
|
||||
const rootUser = await db.query.user.findFirst({
|
||||
@ -33,11 +33,18 @@ export class InitService implements OnModuleInit {
|
||||
|
||||
if (!rootUser) {
|
||||
this.logger.log('正在创建 root 用户...', 'InitService');
|
||||
|
||||
|
||||
// 生成密码哈希
|
||||
const rootPassword = this.configService.get<string>('password.userDefaultPassword'); // 默认密码
|
||||
const passwordHash = await this.utilsServer.hashPassword(rootPassword as string);
|
||||
const now = new Date().toISOString().slice(0, 23).replace('T', ' ');
|
||||
const rootPassword = this.configService.get<string>(
|
||||
'password.userDefaultPassword',
|
||||
); // 默认密码
|
||||
const passwordHash = await this.utilsService.hashPassword(
|
||||
rootPassword as string,
|
||||
);
|
||||
const now = new Date()
|
||||
.toISOString()
|
||||
.slice(0, 23)
|
||||
.replace('T', ' ');
|
||||
|
||||
// 创建 root 用户
|
||||
await db.transaction(async (tx) => {
|
||||
@ -78,8 +85,12 @@ export class InitService implements OnModuleInit {
|
||||
this.logger.log('root 用户已存在', 'InitService');
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('初始化 root 用户失败', error, 'InitService');
|
||||
this.logger.error(
|
||||
'初始化 root 用户失败',
|
||||
(error as Error).message,
|
||||
'InitService',
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -4,8 +4,10 @@ import {
|
||||
NestFastifyApplication,
|
||||
} from '@nestjs/platform-fastify';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AppModule } from '@/app.module';
|
||||
import { AppModule } from '@/module/app.module';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
|
||||
interface ServerConfig {
|
||||
port: number;
|
||||
@ -16,7 +18,7 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create<NestFastifyApplication>(
|
||||
AppModule,
|
||||
new FastifyAdapter({
|
||||
logger: false
|
||||
logger: false,
|
||||
}),
|
||||
);
|
||||
|
||||
@ -33,6 +35,32 @@ async function bootstrap() {
|
||||
const logger = app.get(CustomLogger);
|
||||
app.useLogger(logger);
|
||||
|
||||
// 设置全局路由前缀
|
||||
app.setGlobalPrefix('/api');
|
||||
|
||||
// 配置全局验证管道
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true, // 去除在验证类中不存在的属性
|
||||
transform: true, // 自动转换类型
|
||||
forbidNonWhitelisted: true, // 禁止在验证类中不存在的属性
|
||||
disableErrorMessages: false, // 在生产环境中你可能想要设置为 true
|
||||
}),
|
||||
);
|
||||
|
||||
// 配置 Swagger
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Star Tune API')
|
||||
.setDescription('Star Tune 项目的 API 文档')
|
||||
.setVersion('1.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
SwaggerModule.setup('api/docs', app, document);
|
||||
logger.debug(
|
||||
`Swagger 配置完成: http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}/api/docs`,
|
||||
);
|
||||
|
||||
// 测试不同级别的日志输出
|
||||
logger.error('这是一条错误日志:服务启动出现严重问题', 'Bootstrap');
|
||||
logger.warn('这是一条警告日志:配置项未完全设置', 'Bootstrap');
|
||||
@ -43,11 +71,9 @@ async function bootstrap() {
|
||||
await app.listen(port, host);
|
||||
const environment = configService.get<string>('environment');
|
||||
logger.info(
|
||||
`应用程序正在运行: http://${host==='0.0.0.0'?'127.0.0.1':host}:${port}, 环境: ${environment}`,
|
||||
`应用程序正在运行: http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}, 环境: ${environment}`,
|
||||
'Bootstrap',
|
||||
);
|
||||
}
|
||||
|
||||
// 调用引导函数启动应用程序
|
||||
// 使用 void 运算符来明确表示我们知道这是一个未处理的 Promise,这是一种最佳实践
|
||||
void bootstrap();
|
||||
|
72
star-tune/src/middleware/request-logger.middleware.ts
Normal file
72
star-tune/src/middleware/request-logger.middleware.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UtilsService } from '@/common/utils/utils.service';
|
||||
import { allColors } from '@/type/enum';
|
||||
|
||||
interface ResponseWithHeader {
|
||||
_header?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RequestLoggerMiddleware implements NestMiddleware {
|
||||
public use: (
|
||||
req: FastifyRequest & FastifyRequest['raw'],
|
||||
res: FastifyReply & FastifyReply['raw'],
|
||||
next: () => void,
|
||||
) => void;
|
||||
private number = 0;
|
||||
private color: string[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly logger: CustomLogger,
|
||||
private readonly configService: ConfigService,
|
||||
private readonly utilsService: UtilsService,
|
||||
) {
|
||||
const environment = this.configService.get<string>('environment');
|
||||
// 只在开发环境下记录请求日志
|
||||
if (environment === 'development') {
|
||||
this.use = this.useBack;
|
||||
for (let i of Object.keys(allColors.text)) {
|
||||
for (let j of Object.keys(allColors.bg)) {
|
||||
this.color.push(`${allColors.text[i]}${allColors.bg[j]}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useBack = (
|
||||
req: FastifyRequest & FastifyRequest['raw'],
|
||||
res: FastifyReply & FastifyReply['raw'],
|
||||
next: () => void,
|
||||
) => {
|
||||
const requestStart = process.hrtime();
|
||||
const id = req.id?.slice(4).padEnd(6).toUpperCase();
|
||||
const color = this.getColor();
|
||||
this.logger.debug(
|
||||
`${color}<=== ${id}${allColors.reset} | ${req.method} ${req.originalUrl} ${req.ip}`,
|
||||
'RequestLogger',
|
||||
);
|
||||
res.on('finish', () => {
|
||||
const [seconds, nanoseconds] = process.hrtime(requestStart);
|
||||
const duration = seconds * 1000 + nanoseconds / 1000000;
|
||||
const header = (res as unknown as ResponseWithHeader)._header;
|
||||
const contentLength = header?.match(/content-length: (\d+)/i)?.[1];
|
||||
const size = contentLength
|
||||
? this.utilsService.formatFileSize(parseInt(contentLength, 10))
|
||||
: '0 B';
|
||||
const contentType = header?.match(/content-type: (\w+\/\w+)/i)?.[1];
|
||||
|
||||
this.logger.debug(
|
||||
`${color}===> ${id}${allColors.reset} | ${req.method} ${req.originalUrl} ${res.statusCode} ${duration.toFixed(2)}ms [${size}] [${contentType || ''}]`,
|
||||
'RequestLogger',
|
||||
);
|
||||
});
|
||||
next();
|
||||
};
|
||||
getColor() {
|
||||
this.number++;
|
||||
return this.color[this.number % this.color.length];
|
||||
}
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { InitModule } from '@/common/init/init.module';
|
||||
import { DatabaseModule } from '@/common/database/database.module';
|
||||
import { InitModule } from '@/init/init.module';
|
||||
import { DatabaseModule } from '@/service/database/database.module';
|
||||
import { LoggerModule } from '@/common/logger/logger.module';
|
||||
import { UtilsModule } from '@/common/utils/utils.module';
|
||||
import { RedisModule } from '@/common/redis/redis.module';
|
||||
import configuration from '@/config/configuration';
|
||||
import { AppController } from '@/app.controller';
|
||||
import { AppService } from '@/app.service';
|
||||
import { RequestLoggerMiddleware } from '@/common/middleware/request-logger.middleware';
|
||||
import { RedisModule } from '@/service/redis/redis.module';
|
||||
import configuration from '@/common/config/configuration';
|
||||
import { RequestLoggerMiddleware } from '@/middleware/request-logger.middleware';
|
||||
import { UserModule } from '@/module/user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@ -27,12 +26,13 @@ import { RequestLoggerMiddleware } from '@/common/middleware/request-logger.midd
|
||||
InitModule,
|
||||
// 工具模块
|
||||
UtilsModule,
|
||||
UserModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
controllers: [],
|
||||
providers: [],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer) {
|
||||
consumer.apply(RequestLoggerMiddleware).forRoutes('*');
|
||||
consumer.apply(RequestLoggerMiddleware).forRoutes('*path');
|
||||
}
|
||||
}
|
15
star-tune/src/module/user/dto/check-email.dto.ts
Normal file
15
star-tune/src/module/user/dto/check-email.dto.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { IsEmail, Length } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ToLowerCase, Trim } from '@/decorators/transform.decorator';
|
||||
|
||||
export class CheckEmailDto {
|
||||
@ApiProperty({
|
||||
description: '需要检查的邮箱地址',
|
||||
example: 'user@example.com',
|
||||
})
|
||||
@Length(8, 128, { message: '请将邮箱长度控制在8到128位之间!' })
|
||||
@IsEmail({}, { message: '邮箱格式错误!' })
|
||||
@Trim()
|
||||
@ToLowerCase()
|
||||
email: string;
|
||||
}
|
1
star-tune/src/module/user/dto/create-user.dto.ts
Normal file
1
star-tune/src/module/user/dto/create-user.dto.ts
Normal file
@ -0,0 +1 @@
|
||||
export class CreateUserDto {}
|
21
star-tune/src/module/user/dto/email-login.dto.ts
Normal file
21
star-tune/src/module/user/dto/email-login.dto.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { IsEmail, IsString, Length } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class EmailLoginDto {
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'user@example.com',
|
||||
})
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '验证码',
|
||||
example: '123456',
|
||||
minLength: 6,
|
||||
maxLength: 6,
|
||||
})
|
||||
@IsString()
|
||||
@Length(6, 6)
|
||||
code: string;
|
||||
}
|
35
star-tune/src/module/user/dto/login.dto.ts
Normal file
35
star-tune/src/module/user/dto/login.dto.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { IsEmail, IsString, Length, ValidateIf } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty({
|
||||
description: '邮箱地址(邮箱和用户名必须填写一个)',
|
||||
example: 'user@example.com',
|
||||
required: false,
|
||||
})
|
||||
@ValidateIf((o) => !o.username)
|
||||
@IsEmail()
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户名(邮箱和用户名必须填写一个)',
|
||||
example: 'johndoe',
|
||||
minLength: 3,
|
||||
maxLength: 50,
|
||||
required: false,
|
||||
})
|
||||
@ValidateIf((o) => !o.email)
|
||||
@IsString()
|
||||
@Length(3, 50)
|
||||
username?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '密码',
|
||||
example: 'password123',
|
||||
minLength: 6,
|
||||
maxLength: 20,
|
||||
})
|
||||
@IsString()
|
||||
@Length(6, 20)
|
||||
password: string;
|
||||
}
|
69
star-tune/src/module/user/dto/register.dto.ts
Normal file
69
star-tune/src/module/user/dto/register.dto.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { IsEmail, IsString, Length, Matches } from 'class-validator';
|
||||
import { Trim, ToLowerCase } from '../../../decorators/transform.decorator';
|
||||
import {
|
||||
IsAlphanumeric,
|
||||
NoSpecialCharacters,
|
||||
} from '../../../decorators/validation.decorator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 用户注册数据传输对象
|
||||
*/
|
||||
export class RegisterDto {
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'user@example.com',
|
||||
})
|
||||
@Trim()
|
||||
@ToLowerCase()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '验证码',
|
||||
example: '123456',
|
||||
minLength: 6,
|
||||
maxLength: 6,
|
||||
})
|
||||
@Trim()
|
||||
@IsString()
|
||||
@Length(6, 6, { message: '验证码必须是6位' })
|
||||
@IsAlphanumeric({ message: '验证码只能包含数字和字母' })
|
||||
code: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户名',
|
||||
example: 'johndoe',
|
||||
minLength: 3,
|
||||
maxLength: 50,
|
||||
})
|
||||
@Trim()
|
||||
@IsString()
|
||||
@Length(3, 50, { message: '用户名长度必须在3-50个字符之间' })
|
||||
@Matches(/^[a-zA-Z0-9_-]+$/, {
|
||||
message: '用户名只能包含字母、数字、下划线和连字符',
|
||||
})
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '密码',
|
||||
example: 'password123',
|
||||
minLength: 6,
|
||||
maxLength: 20,
|
||||
})
|
||||
@IsString()
|
||||
@Length(6, 20, { message: '密码长度必须在6-20个字符之间' })
|
||||
password: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '昵称',
|
||||
example: 'John Doe',
|
||||
minLength: 1,
|
||||
maxLength: 50,
|
||||
})
|
||||
@Trim()
|
||||
@IsString()
|
||||
@Length(1, 50, { message: '昵称长度必须在1-50个字符之间' })
|
||||
@NoSpecialCharacters({ message: '昵称不能包含特殊字符' })
|
||||
nickname: string;
|
||||
}
|
26
star-tune/src/module/user/dto/send-email-code.dto.ts
Normal file
26
star-tune/src/module/user/dto/send-email-code.dto.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { IsEmail, IsEnum, Length } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ToLowerCase, Trim } from '@/decorators/transform.decorator';
|
||||
import { EmailCodeType } from '@/type/enum';
|
||||
|
||||
export class SendEmailCodeDto {
|
||||
@ApiProperty({
|
||||
description: '接收验证码的邮箱地址',
|
||||
example: 'user@example.com',
|
||||
})
|
||||
@Length(8, 128, { message: '请将邮箱长度控制在8到128位之间!' })
|
||||
@IsEmail({}, { message: '邮箱格式错误!' })
|
||||
@Trim()
|
||||
@ToLowerCase()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '验证码类型',
|
||||
enum: EmailCodeType,
|
||||
example: EmailCodeType.REGISTER,
|
||||
})
|
||||
@IsEnum(EmailCodeType)
|
||||
@Trim()
|
||||
@ToLowerCase()
|
||||
type: EmailCodeType;
|
||||
}
|
13
star-tune/src/module/user/dto/update-profile.dto.ts
Normal file
13
star-tune/src/module/user/dto/update-profile.dto.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { IsString, MaxLength } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateProfileDto {
|
||||
@ApiProperty({
|
||||
description: '个人简介内容',
|
||||
example: '这是我的个人简介,介绍一下自己...',
|
||||
maxLength: 16777215,
|
||||
})
|
||||
@IsString()
|
||||
@MaxLength(16777215) // mediumtext 最大长度
|
||||
profile: string;
|
||||
}
|
24
star-tune/src/module/user/dto/update-signature.dto.ts
Normal file
24
star-tune/src/module/user/dto/update-signature.dto.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { IsString, Length } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateSignatureDto {
|
||||
@ApiProperty({
|
||||
description: '个性签名内容',
|
||||
example: '生活就像一杯茶',
|
||||
minLength: 1,
|
||||
maxLength: 100,
|
||||
})
|
||||
@IsString()
|
||||
@Length(1, 100)
|
||||
signature: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '个性签名标签',
|
||||
example: '生活感悟',
|
||||
minLength: 1,
|
||||
maxLength: 100,
|
||||
})
|
||||
@IsString()
|
||||
@Length(1, 100)
|
||||
signatureTag: string;
|
||||
}
|
74
star-tune/src/module/user/dto/update-user.dto.ts
Normal file
74
star-tune/src/module/user/dto/update-user.dto.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
IsOptional,
|
||||
IsString,
|
||||
Length,
|
||||
IsEnum,
|
||||
IsDateString,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class UpdateUserDto {
|
||||
@ApiProperty({
|
||||
description: '昵称',
|
||||
example: 'John Doe',
|
||||
minLength: 1,
|
||||
maxLength: 50,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(1, 50)
|
||||
nickname?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '性别(0-未知,1-男,2-女)',
|
||||
enum: ['0', '1', '2'],
|
||||
example: '1',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEnum(['0', '1', '2'])
|
||||
gender?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '出生日期',
|
||||
example: '1990-01-01',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
birthdate?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '地址',
|
||||
example: '北京市朝阳区',
|
||||
maxLength: 255,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(0, 255)
|
||||
address?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '头像URL',
|
||||
example: 'https://example.com/avatar.jpg',
|
||||
maxLength: 255,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(0, 255)
|
||||
avatar?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '背景图片URL',
|
||||
example: 'https://example.com/background.jpg',
|
||||
maxLength: 255,
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Length(0, 255)
|
||||
backgroundImage?: string;
|
||||
}
|
1
star-tune/src/module/user/entities/user.entity.ts
Normal file
1
star-tune/src/module/user/entities/user.entity.ts
Normal file
@ -0,0 +1 @@
|
||||
export class User {}
|
20
star-tune/src/module/user/user.controller.spec.ts
Normal file
20
star-tune/src/module/user/user.controller.spec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserController } from './user.controller';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserController', () => {
|
||||
let controller: UserController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<UserController>(UserController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(controller).toBeDefined();
|
||||
});
|
||||
});
|
153
star-tune/src/module/user/user.controller.ts
Normal file
153
star-tune/src/module/user/user.controller.ts
Normal file
@ -0,0 +1,153 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Delete,
|
||||
UseGuards,
|
||||
Headers,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { CheckEmailDto } from './dto/check-email.dto';
|
||||
import { SendEmailCodeDto } from './dto/send-email-code.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { EmailLoginDto } from './dto/email-login.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { UpdateSignatureDto } from './dto/update-signature.dto';
|
||||
import { AuthGuard } from '../../guards/auth.guard';
|
||||
import { User } from '../../decorators/user.decorator';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiBearerAuth,
|
||||
ApiResponse,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
@ApiTags('用户管理')
|
||||
@Controller('user')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
// 实现功能注意
|
||||
// 数据库采用drizzle,已全局注入,使用时直接注入即可
|
||||
// 日志采用nestjs的logger,已全局注入,使用时直接注入即可
|
||||
// 缓存采用redis,已全局注入,使用时直接注入即可
|
||||
// 工具类采用utils,已全局注入,使用时直接注入即可
|
||||
// 配置采用config,已全局注入,使用时直接注入即可
|
||||
// 数据实体在src/drizzle/schema.ts中,使用时直接引入即可
|
||||
|
||||
// 1. 邮箱注册功能
|
||||
// - 检查邮箱是否可用接口
|
||||
// - 发送验证码接口,通过配置系统邮箱发送给目标邮箱邮件,60秒内不允许再发,5分钟内复用上次验证码,采用redis
|
||||
// - 注册接口,完善用户信息,通过验证码注册,注册后自动登录,清除邮箱验证码,注意加分布式锁,防止重复注册
|
||||
|
||||
// 2. 普通登录功能
|
||||
// - 登录接口,通过邮箱/用户名和密码登录,登录后返回token(ReflushToken和AccessToken),token采用jwt,采用redis缓存,缓存时间AccessToken10分钟,ReflushToken14天,
|
||||
// - 退出登录接口,清除两个token,清除redis缓存
|
||||
// - 刷新token接口,通过ReflushToken刷新AccessToken,刷新后返回新的token,刷新后清除旧的token,清除redis缓存
|
||||
|
||||
// 3. 邮箱登录功能
|
||||
// - 登录接口,通过邮箱和验证码登录,验证码逻辑同邮箱注册功能,但是要区分
|
||||
// - token逻辑和普通登录功能一致
|
||||
|
||||
// 4. 个人用户信息功能
|
||||
// - 获取用户信息接口,通过token获取用户信息
|
||||
// - 更新用户信息接口,通过token更新用户信息,更新时注意,基础用户表为一个接口,个人简介和个性签名均为单独接口
|
||||
// - 删除用户信息接口,通过token删除用户信息
|
||||
|
||||
@ApiOperation({ summary: '检查邮箱是否可用' })
|
||||
@ApiResponse({ status: 200, description: '邮箱检查结果' })
|
||||
@Post('check-email')
|
||||
checkEmail(@Body() dto: CheckEmailDto) {
|
||||
return this.userService.checkEmail(dto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '发送邮箱验证码' })
|
||||
@ApiResponse({ status: 200, description: '验证码发送成功' })
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: '发送失败,可能是因为发送过于频繁',
|
||||
})
|
||||
@Post('send-code')
|
||||
sendEmailCode(@Body() dto: SendEmailCodeDto) {
|
||||
return this.userService.sendEmailCode(dto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: '用户注册' })
|
||||
@ApiResponse({ status: 201, description: '注册成功并返回用户信息' })
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: '注册失败,可能是验证码错误或用户已存在',
|
||||
})
|
||||
@Post('register')
|
||||
register(@Body() dto: RegisterDto) {
|
||||
return this.userService.register(dto);
|
||||
}
|
||||
|
||||
// @Post('login')
|
||||
// login(@Body() dto: LoginDto) {
|
||||
// return this.userService.login(dto);
|
||||
// }
|
||||
|
||||
// @Post('email-login')
|
||||
// emailLogin(@Body() dto: EmailLoginDto) {
|
||||
// return this.userService.emailLogin(dto);
|
||||
// }
|
||||
|
||||
// @Post('refresh-token')
|
||||
// refreshToken(@Headers('refresh-token') refreshToken: string) {
|
||||
// if (!refreshToken) {
|
||||
// throw new UnauthorizedException('缺少刷新令牌');
|
||||
// }
|
||||
// return this.userService.refreshToken(refreshToken);
|
||||
// }
|
||||
|
||||
// @Post('logout')
|
||||
// @UseGuards(AuthGuard)
|
||||
// logout(@User('userId') userId: number) {
|
||||
// return this.userService.logout(userId);
|
||||
// }
|
||||
|
||||
// @Get('info')
|
||||
// @UseGuards(AuthGuard)
|
||||
// getUserInfo(@User('userId') userId: number) {
|
||||
// return this.userService.getUserInfo(userId);
|
||||
// }
|
||||
|
||||
// @Patch()
|
||||
// @UseGuards(AuthGuard)
|
||||
// updateUser(
|
||||
// @User('userId') userId: number,
|
||||
// @Body() dto: UpdateUserDto,
|
||||
// ) {
|
||||
// return this.userService.updateUser(userId, dto);
|
||||
// }
|
||||
|
||||
// @Patch('profile')
|
||||
// @UseGuards(AuthGuard)
|
||||
// updateProfile(
|
||||
// @User('userId') userId: number,
|
||||
// @Body() dto: UpdateProfileDto,
|
||||
// ) {
|
||||
// return this.userService.updateProfile(userId, dto);
|
||||
// }
|
||||
|
||||
// @Patch('signature')
|
||||
// @UseGuards(AuthGuard)
|
||||
// updateSignature(
|
||||
// @User('userId') userId: number,
|
||||
// @Body() dto: UpdateSignatureDto,
|
||||
// ) {
|
||||
// return this.userService.updateSignature(userId, dto);
|
||||
// }
|
||||
|
||||
// @Delete()
|
||||
// @UseGuards(AuthGuard)
|
||||
// deleteUser(@User('userId') userId: number) {
|
||||
// return this.userService.deleteUser(userId);
|
||||
// }
|
||||
}
|
11
star-tune/src/module/user/user.module.ts
Normal file
11
star-tune/src/module/user/user.module.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UserService } from './user.service';
|
||||
import { UserController } from './user.controller';
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
controllers: [UserController],
|
||||
providers: [UserService],
|
||||
exports: [UserService],
|
||||
})
|
||||
export class UserModule {}
|
18
star-tune/src/module/user/user.service.spec.ts
Normal file
18
star-tune/src/module/user/user.service.spec.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [UserService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<UserService>(UserService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
361
star-tune/src/module/user/user.service.ts
Normal file
361
star-tune/src/module/user/user.service.ts
Normal file
@ -0,0 +1,361 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { RedisService } from '@/service/redis/redis.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { UtilsService } from '@/common/utils/utils.service';
|
||||
import { eq, and, or } from 'drizzle-orm';
|
||||
import { CheckEmailDto } from './dto/check-email.dto';
|
||||
import { SendEmailCodeDto } from './dto/send-email-code.dto';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { EmailLoginDto } from './dto/email-login.dto';
|
||||
import { UpdateUserDto } from './dto/update-user.dto';
|
||||
import { UpdateProfileDto } from './dto/update-profile.dto';
|
||||
import { UpdateSignatureDto } from './dto/update-signature.dto';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { DatabaseService } from '@/service/database/database.service';
|
||||
import {
|
||||
user,
|
||||
userPassword,
|
||||
userProfile,
|
||||
userSignatureHistory,
|
||||
} from '@/drizzle/schema';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { EmailCodeType } from '@/type/enum';
|
||||
import * as dayjs from 'dayjs';
|
||||
import { UserGuard } from '@/type/userGuard';
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
constructor(
|
||||
private readonly logger: CustomLogger,
|
||||
private readonly redis: RedisService,
|
||||
private readonly config: ConfigService,
|
||||
private readonly utils: UtilsService,
|
||||
private readonly database: DatabaseService,
|
||||
) {}
|
||||
|
||||
// 检查邮箱是否可用
|
||||
async checkEmail(dto: CheckEmailDto) {
|
||||
const exists = await this.database.db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(and(eq(user.email, dto.email), eq(user.isDeleted, 0)))
|
||||
.execute();
|
||||
|
||||
return { available: exists.length === 0 };
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
async sendEmailCode(dto: SendEmailCodeDto) {
|
||||
// 邮箱验证码key
|
||||
const codeKey = `${EmailCodeType[dto.type]}:${dto.email}`;
|
||||
// 判断Key是否存在
|
||||
const keyExists = await this.redis.exists(codeKey);
|
||||
// 验证码过期时间
|
||||
const codeEX = this.config.get<number>('email.codeEX') || 300;
|
||||
// 验证码冷却时间
|
||||
const codeEP = this.config.get<number>('email.codeEP') || 60;
|
||||
// 判断是否存在验证码
|
||||
if (keyExists) {
|
||||
// 获取Key的过期时间
|
||||
const ttl = await this.redis.ttl(codeKey);
|
||||
if (ttl > codeEX - codeEP) {
|
||||
// 再等等吧
|
||||
throw new BadRequestException(`请等待${ttl}秒后再试`);
|
||||
} else {
|
||||
// 续杯
|
||||
await this.redis.expire(codeKey, codeEX);
|
||||
// 获取验证码
|
||||
const code = await this.redis.get(codeKey);
|
||||
// todo重新发送验证码
|
||||
this.logger.debug(`重新发送验证码: ${code}`);
|
||||
await this.utils.sendEmail({
|
||||
to: dto.email,
|
||||
subject: '账户注册验证码',
|
||||
text: `您的验证码是:${code},5分钟内有效`,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// 生成验证码
|
||||
const code = Math.random().toString().slice(2, 8);
|
||||
// todo发送验证码
|
||||
this.logger.debug(`发送验证码: ${code}`);
|
||||
await this.utils.sendEmail({
|
||||
to: dto.email,
|
||||
subject: '账户注册验证码',
|
||||
text: `您的验证码是:${code},5分钟内有效`,
|
||||
});
|
||||
// 存储验证码
|
||||
await this.redis.set(codeKey, code, codeEX);
|
||||
}
|
||||
return { message: '验证码已发送' };
|
||||
}
|
||||
|
||||
// 注册
|
||||
async register(dto: RegisterDto) {
|
||||
// 邮箱验证码key
|
||||
const codeKey = `${EmailCodeType.register}:${dto.email}`;
|
||||
this.logger.debug(
|
||||
`邮箱验证码key: ${EmailCodeType.register}:${dto.email} ${codeKey}`,
|
||||
);
|
||||
// 获取验证码
|
||||
const code = await this.redis.get(codeKey);
|
||||
// 验证码错误或已过期
|
||||
if (!code || code !== dto.code) {
|
||||
throw new BadRequestException('验证码错误或已过期');
|
||||
}
|
||||
|
||||
// 加锁
|
||||
const lock = await this.redis.lock(
|
||||
`${EmailCodeType.REGISTER}:${dto.email}`,
|
||||
);
|
||||
// 加锁失败
|
||||
if (!lock) {
|
||||
throw new BadRequestException('请稍后再试');
|
||||
}
|
||||
|
||||
try {
|
||||
// 判断用户是否存在
|
||||
const userExists = await this.database.db
|
||||
.select({
|
||||
userId: user.userId,
|
||||
})
|
||||
.from(user)
|
||||
.where(
|
||||
or(
|
||||
eq(user.email, dto.email),
|
||||
eq(user.username, dto.username),
|
||||
),
|
||||
)
|
||||
.execute();
|
||||
if (userExists.length > 0) {
|
||||
throw new BadRequestException('用户已存在');
|
||||
}
|
||||
// 生成用户ID
|
||||
const userId = new Date().getTime();
|
||||
// 加密密码
|
||||
const passwordHash = await this.utils.hashPassword(dto.password);
|
||||
|
||||
await this.database.db
|
||||
.insert(user)
|
||||
.values({
|
||||
userId,
|
||||
username: dto.username,
|
||||
email: dto.email,
|
||||
nickname: dto.nickname,
|
||||
createdBy: 'SELF',
|
||||
})
|
||||
.execute();
|
||||
|
||||
await this.database.db
|
||||
.insert(userPassword)
|
||||
.values({
|
||||
userId,
|
||||
passwordHash,
|
||||
})
|
||||
.execute();
|
||||
// 生成用户Guard
|
||||
const userGuard: UserGuard = {
|
||||
userId,
|
||||
username: dto.username,
|
||||
email: dto.email,
|
||||
time: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
};
|
||||
await this.redis.del(codeKey);
|
||||
const accessToken = this.utils.generateAccessToken(userGuard);
|
||||
const refreshToken = this.utils.generateRefreshToken(userGuard);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
} finally {
|
||||
await this.redis.unlock(`register:${dto.email}`);
|
||||
}
|
||||
}
|
||||
|
||||
// // 普通登录
|
||||
// async login(dto: LoginDto) {
|
||||
// const result = await this.database.db
|
||||
// .select()
|
||||
// .from(user)
|
||||
// .where(
|
||||
// and(
|
||||
// dto.email
|
||||
// ? eq(user.email, dto.email)
|
||||
// : eq(user.username, dto.username),
|
||||
// eq(user.isDeleted, 0),
|
||||
// ),
|
||||
// )
|
||||
// .execute();
|
||||
|
||||
// if (result.length === 0) {
|
||||
// throw new UnauthorizedException('用户不存在');
|
||||
// }
|
||||
|
||||
// const userData = result[0];
|
||||
// const passwordData = await this.database.db
|
||||
// .select()
|
||||
// .from(userPassword)
|
||||
// .where(eq(userPassword.userId, userData.userId))
|
||||
// .execute();
|
||||
|
||||
// if (
|
||||
// !passwordData.length ||
|
||||
// !(await bcrypt.compare(dto.password, passwordData[0].passwordHash))
|
||||
// ) {
|
||||
// throw new UnauthorizedException('密码错误');
|
||||
// }
|
||||
|
||||
// return this.generateTokens(userData.userId);
|
||||
// }
|
||||
|
||||
// // 邮箱登录
|
||||
// async emailLogin(dto: EmailLoginDto) {
|
||||
// const redisKey = `email_code:${EmailCodeType.LOGIN}:${dto.email}`;
|
||||
// const code = await this.redis.get(redisKey);
|
||||
|
||||
// if (!code || code !== dto.code) {
|
||||
// throw new UnauthorizedException('验证码错误或已过期');
|
||||
// }
|
||||
|
||||
// const result = await this.database.db
|
||||
// .select()
|
||||
// .from(user)
|
||||
// .where(and(eq(user.email, dto.email), eq(user.isDeleted, 0)))
|
||||
// .execute();
|
||||
|
||||
// if (result.length === 0) {
|
||||
// throw new UnauthorizedException('用户不存在');
|
||||
// }
|
||||
|
||||
// await this.redis.del(redisKey);
|
||||
// return this.generateTokens(result[0].userId);
|
||||
// }
|
||||
|
||||
生成令牌;
|
||||
private async generateTokens(userGuard: UserGuard) {
|
||||
const accessToken = this.utils.generateAccessToken(userGuard);
|
||||
const refreshToken = this.utils.generateRefreshToken(userGuard);
|
||||
|
||||
await this.redis.set(
|
||||
`access_token:${userGuard.userId}`,
|
||||
accessToken,
|
||||
600,
|
||||
);
|
||||
await this.redis.set(
|
||||
`refresh_token:${userGuard.userId}`,
|
||||
refreshToken,
|
||||
1209600,
|
||||
);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
// // 刷新令牌
|
||||
// async refreshToken(refreshToken: string) {
|
||||
// try {
|
||||
// const decoded = jwt.verify(
|
||||
// refreshToken,
|
||||
// this.config.get('JWT_SECRET'),
|
||||
// ) as { userId: number };
|
||||
// const storedToken = await this.redis.get(
|
||||
// `refresh_token:${decoded.userId}`,
|
||||
// );
|
||||
|
||||
// if (!storedToken || storedToken !== refreshToken) {
|
||||
// throw new UnauthorizedException('无效的刷新令牌');
|
||||
// }
|
||||
|
||||
// await this.redis.del(`refresh_token:${decoded.userId}`);
|
||||
// await this.redis.del(`access_token:${decoded.userId}`);
|
||||
|
||||
// return this.generateTokens(decoded.userId);
|
||||
// } catch (error) {
|
||||
// throw new UnauthorizedException('无效的刷新令牌');
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 退出登录
|
||||
// async logout(userId: number) {
|
||||
// await this.redis.del(`access_token:${userId}`);
|
||||
// await this.redis.del(`refresh_token:${userId}`);
|
||||
// return { message: '退出成功' };
|
||||
// }
|
||||
|
||||
// // 获取用户信息
|
||||
// async getUserInfo(userId: number) {
|
||||
// const result = await this.database.db
|
||||
// .select()
|
||||
// .from(user)
|
||||
// .where(and(eq(user.userId, userId), eq(user.isDeleted, 0)))
|
||||
// .execute();
|
||||
|
||||
// if (result.length === 0) {
|
||||
// throw new UnauthorizedException('用户不存在');
|
||||
// }
|
||||
|
||||
// const { passwordHash, ...userData } = result[0];
|
||||
// return userData;
|
||||
// }
|
||||
|
||||
// // 更新用户信息
|
||||
// async updateUser(userId: number, dto: UpdateUserDto) {
|
||||
// await this.database.db
|
||||
// .update(user)
|
||||
// .set({
|
||||
// ...dto,
|
||||
// updatedAt: new Date().toISOString(),
|
||||
// })
|
||||
// .where(eq(user.userId, userId))
|
||||
// .execute();
|
||||
|
||||
// return { message: '更新成功' };
|
||||
// }
|
||||
|
||||
// // 更新用户简介
|
||||
// async updateProfile(userId: number, dto: UpdateProfileDto) {
|
||||
// await this.database.db
|
||||
// .insert(userProfile)
|
||||
// .values({
|
||||
// userId,
|
||||
// profile: dto.profile,
|
||||
// })
|
||||
// .onDuplicateKeyUpdate({
|
||||
// set: {
|
||||
// profile: dto.profile,
|
||||
// updatedAt: new Date().toISOString(),
|
||||
// },
|
||||
// })
|
||||
// .execute();
|
||||
|
||||
// return { message: '更新成功' };
|
||||
// }
|
||||
|
||||
// // 更新用户签名
|
||||
// async updateSignature(userId: number, dto: UpdateSignatureDto) {
|
||||
// await this.database.db
|
||||
// .insert(userSignatureHistory)
|
||||
// .values({
|
||||
// userId,
|
||||
// signature: dto.signature,
|
||||
// signatureTag: dto.signatureTag,
|
||||
// })
|
||||
// .execute();
|
||||
|
||||
// return { message: '更新成功' };
|
||||
// }
|
||||
|
||||
// // 删除用户
|
||||
// async deleteUser(userId: number) {
|
||||
// await this.database.db
|
||||
// .update(user)
|
||||
// .set({
|
||||
// isDeleted: 1,
|
||||
// deletedAt: new Date().toISOString(),
|
||||
// })
|
||||
// .where(eq(user.userId, userId))
|
||||
// .execute();
|
||||
|
||||
// await this.logout(userId);
|
||||
// return { message: '删除成功' };
|
||||
// }
|
||||
}
|
@ -1,19 +1,18 @@
|
||||
/**
|
||||
* 数据库模块
|
||||
*
|
||||
*
|
||||
* 这是一个全局模块,使用 @Global() 装饰器使其在整个应用中可用
|
||||
* 不需要在每个使用数据库的模块中重复导入
|
||||
*/
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { LoggerModule } from '@/common/logger/logger.module';
|
||||
import { DatabaseService } from '@/common/database/database.service';
|
||||
import { DatabaseService } from '@/service/database/database.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [LoggerModule],
|
||||
imports: [],
|
||||
// 提供数据库服务
|
||||
providers: [DatabaseService],
|
||||
// 导出数据库服务,使其可以被其他模块注入使用
|
||||
exports: [DatabaseService],
|
||||
})
|
||||
export class DatabaseModule {}
|
||||
export class DatabaseModule {}
|
@ -1,25 +1,36 @@
|
||||
/**
|
||||
* 数据库服务
|
||||
*
|
||||
*
|
||||
* 负责管理数据库连接和提供数据库操作接口
|
||||
* 实现了 OnModuleInit 接口,在模块初始化时自动建立数据库连接
|
||||
*/
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { drizzle, type MySql2Database } from 'drizzle-orm/mysql2';
|
||||
import { createPool, Pool} from 'mysql2/promise';
|
||||
import { createPool, Pool } from 'mysql2/promise';
|
||||
import * as schema from '@/drizzle/schema';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
import { type ResultSetHeader } from 'mysql2';
|
||||
|
||||
// 定义数据库配置接口
|
||||
interface DatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService implements OnModuleInit {
|
||||
// 数据库实例,使用 drizzle ORM
|
||||
private db!: MySql2Database<typeof schema>;
|
||||
public db!: MySql2Database<typeof schema>;
|
||||
// MySQL 连接池实例
|
||||
private pool!: Pool;
|
||||
|
||||
constructor(private configService: ConfigService, private logger: CustomLogger) {}
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private logger: CustomLogger,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 模块初始化时自动执行
|
||||
@ -27,8 +38,12 @@ export class DatabaseService implements OnModuleInit {
|
||||
*/
|
||||
async onModuleInit() {
|
||||
// 从配置服务获取数据库配置
|
||||
const dbConfig = this.configService.get('database');
|
||||
|
||||
const dbConfig = this.configService.get<DatabaseConfig>('database');
|
||||
|
||||
if (!dbConfig) {
|
||||
throw new Error('数据库配置未找到');
|
||||
}
|
||||
|
||||
// 先创建临时连接(不指定数据库名)
|
||||
const tempPool = createPool({
|
||||
host: dbConfig.host,
|
||||
@ -38,8 +53,10 @@ export class DatabaseService implements OnModuleInit {
|
||||
});
|
||||
|
||||
// 创建数据库(如果不存在)
|
||||
await tempPool.query(`CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\``) as [ResultSetHeader, any];
|
||||
|
||||
await tempPool.query(
|
||||
`CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\``,
|
||||
);
|
||||
|
||||
this.logger.log(`数据库 ${dbConfig.database} 初始化成功`, 'InitMySQL');
|
||||
await tempPool.end();
|
||||
|
||||
@ -53,7 +70,18 @@ export class DatabaseService implements OnModuleInit {
|
||||
});
|
||||
|
||||
// 初始化 drizzle ORM,使用默认模式
|
||||
this.db = drizzle(this.pool, { schema, mode: 'default' });
|
||||
this.db = drizzle(this.pool, {
|
||||
schema,
|
||||
mode: 'default',
|
||||
logger: {
|
||||
logQuery: (query, params) => {
|
||||
this.logger.debug(
|
||||
`SQL: ${query} - Params: ${JSON.stringify(params)}`,
|
||||
'SQL_Query',
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -71,5 +99,4 @@ export class DatabaseService implements OnModuleInit {
|
||||
getDb() {
|
||||
return this.db;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
9
star-tune/src/service/redis/redis.module.ts
Normal file
9
star-tune/src/service/redis/redis.module.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { RedisService } from '@/service/redis/redis.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [RedisService],
|
||||
exports: [RedisService],
|
||||
})
|
||||
export class RedisModule {}
|
166
star-tune/src/service/redis/redis.service.ts
Normal file
166
star-tune/src/service/redis/redis.service.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
import { CustomLogger } from '@/common/logger/logger.service';
|
||||
|
||||
// Redis 配置接口
|
||||
interface RedisConfig {
|
||||
connectName: string;
|
||||
username: string;
|
||||
password: string;
|
||||
database: number;
|
||||
host: string;
|
||||
port: number;
|
||||
ttl: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RedisService implements OnModuleInit, OnModuleDestroy {
|
||||
public client: RedisClientType;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private logger: CustomLogger,
|
||||
) {
|
||||
const config = this.configService.get<RedisConfig>('redis');
|
||||
if (!config) {
|
||||
throw new Error('Redis 配置未找到');
|
||||
}
|
||||
|
||||
this.client = createClient({
|
||||
name: config.connectName,
|
||||
username: config.username,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
url: `redis://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`,
|
||||
});
|
||||
|
||||
this.client.on('connect', () => {
|
||||
const connectName =
|
||||
this.configService.get<string>('redis.connectName');
|
||||
|
||||
void this.client.set('SI HI', connectName || '').then((result) => {
|
||||
this.logger.log(result || '', 'InitRedis');
|
||||
});
|
||||
});
|
||||
|
||||
this.client.on('error', (err: Error) => {
|
||||
this.logger.error(err.message, err.stack, 'InitRedis');
|
||||
});
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.client.connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.client.quit();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键值对
|
||||
* @param key 键
|
||||
* @param value 值
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
async set(key: string, value: string, ttl?: number): Promise<void> {
|
||||
const expireTime = ttl || this.configService.get('redis.ttl');
|
||||
await this.client.set(key, value, { EX: expireTime });
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键值
|
||||
* @param key 键
|
||||
* @returns 值
|
||||
*/
|
||||
async get(key: string): Promise<string | null> {
|
||||
return await this.client.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除键
|
||||
* @param key 键
|
||||
*/
|
||||
async del(key: string): Promise<void> {
|
||||
await this.client.del(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查键是否存在
|
||||
* @param key 键
|
||||
* @returns 是否存在
|
||||
*/
|
||||
async exists(key: string): Promise<boolean> {
|
||||
return (await this.client.exists(key)) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键的过期时间
|
||||
* @param key 键
|
||||
* @param ttl 过期时间(秒)
|
||||
*/
|
||||
async expire(key: string, ttl: number): Promise<void> {
|
||||
await this.client.expire(key, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试获取分布式锁
|
||||
* @param key 锁的键
|
||||
* @param ttl 锁的过期时间(秒)
|
||||
* @returns 是否成功获取锁
|
||||
*/
|
||||
async lock(key: string, ttl: number = 30): Promise<boolean> {
|
||||
const lockKey = `lock:${key}`;
|
||||
const lockValue = Date.now().toString();
|
||||
|
||||
// 使用 SET NX 命令尝试获取锁
|
||||
const result = await this.client.set(lockKey, lockValue, {
|
||||
NX: true, // 只有当key不存在时才设置
|
||||
EX: ttl, // 设置过期时间
|
||||
});
|
||||
|
||||
return result === 'OK';
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放分布式锁
|
||||
* @param key 锁的键
|
||||
*/
|
||||
async unlock(key: string): Promise<void> {
|
||||
const lockKey = `lock:${key}`;
|
||||
await this.client.del(lockKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键的剩余过期时间
|
||||
* @param key 键
|
||||
* @returns 剩余过期时间(秒),如果键不存在返回-2,如果键没有过期时间返回-1
|
||||
*/
|
||||
async ttl(key: string): Promise<number> {
|
||||
return await this.client.ttl(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用自动释放的分布式锁执行操作
|
||||
* @param key 锁的键
|
||||
* @param fn 需要在锁中执行的函数
|
||||
* @param ttl 锁的过期时间(秒)
|
||||
* @returns 函数执行的结果
|
||||
*/
|
||||
async withLock<T>(
|
||||
key: string,
|
||||
fn: () => Promise<T>,
|
||||
ttl: number = 30,
|
||||
): Promise<T> {
|
||||
const locked = await this.lock(key, ttl);
|
||||
if (!locked) {
|
||||
throw new Error('无法获取锁');
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
await this.unlock(key);
|
||||
}
|
||||
}
|
||||
}
|
60
star-tune/src/type/enum.ts
Normal file
60
star-tune/src/type/enum.ts
Normal file
@ -0,0 +1,60 @@
|
||||
// 邮箱验证码类型
|
||||
export enum EmailCodeType {
|
||||
REGISTER = 'register',
|
||||
LOGIN = 'login',
|
||||
register = 'REGISTER',
|
||||
login = 'LOGIN',
|
||||
}
|
||||
|
||||
export const allColors = {
|
||||
text: {
|
||||
black: '\x1b[30m',
|
||||
red: '\x1b[31m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
blue: '\x1b[34m',
|
||||
magenta: '\x1b[35m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
// 亮色版本
|
||||
brightBlack: '\x1b[90m',
|
||||
brightRed: '\x1b[91m',
|
||||
brightGreen: '\x1b[92m',
|
||||
brightYellow: '\x1b[93m',
|
||||
brightBlue: '\x1b[94m',
|
||||
brightMagenta: '\x1b[95m',
|
||||
brightCyan: '\x1b[96m',
|
||||
brightWhite: '\x1b[97m',
|
||||
},
|
||||
bg: {
|
||||
bgBlack: '\x1b[40m',
|
||||
bgRed: '\x1b[41m',
|
||||
bgGreen: '\x1b[42m',
|
||||
bgYellow: '\x1b[43m',
|
||||
bgBlue: '\x1b[44m',
|
||||
bgMagenta: '\x1b[45m',
|
||||
bgCyan: '\x1b[46m',
|
||||
bgWhite: '\x1b[47m',
|
||||
// 亮色背景
|
||||
bgBrightBlack: '\x1b[100m',
|
||||
bgBrightRed: '\x1b[101m',
|
||||
bgBrightGreen: '\x1b[102m',
|
||||
bgBrightYellow: '\x1b[103m',
|
||||
bgBrightBlue: '\x1b[104m',
|
||||
bgBrightMagenta: '\x1b[105m',
|
||||
bgBrightCyan: '\x1b[106m',
|
||||
bgBrightWhite: '\x1b[107m',
|
||||
},
|
||||
style: {
|
||||
reset: '\x1b[0m', // 重置所有样式
|
||||
bold: '\x1b[1m', // 粗体
|
||||
dim: '\x1b[2m', // 暗淡
|
||||
italic: '\x1b[3m', // 斜体
|
||||
underline: '\x1b[4m', // 下划线
|
||||
blink: '\x1b[5m', // 闪烁
|
||||
reverse: '\x1b[7m', // 反转前景色和背景色
|
||||
hidden: '\x1b[8m', // 隐藏
|
||||
strikethrough: '\x1b[9m', // 删除线
|
||||
},
|
||||
reset: '\x1b[0m',
|
||||
};
|
6
star-tune/src/type/userGuard.ts
Normal file
6
star-tune/src/type/userGuard.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export type UserGuard = {
|
||||
userId: number;
|
||||
username: string;
|
||||
email: string;
|
||||
time: string;
|
||||
};
|
@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import * as request from 'supertest';
|
||||
import { App } from 'supertest/types';
|
||||
import { AppModule } from './../src/app.module';
|
||||
import { AppModule } from '../src/module/app.module';
|
||||
|
||||
describe('AppController (e2e)', () => {
|
||||
let app: INestApplication<App>;
|
||||
|
Loading…
Reference in New Issue
Block a user