feat(api):

1. 注册接口
2. 邮箱验证
3. 发送验证码
This commit is contained in:
nie 2025-05-31 01:25:24 +08:00
parent 9afe6d756f
commit 804d14d8fd
53 changed files with 2609 additions and 377 deletions

View File

@ -28,6 +28,7 @@ export default tseslint.config(
rules: { rules: {
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'warn', '@typescript-eslint/no-floating-promises': 'warn',
"@typescript-eslint/no-unsafe-call": "off",
'@typescript-eslint/no-unsafe-argument': 'warn' '@typescript-eslint/no-unsafe-argument': 'warn'
}, },
}, },

View File

@ -9,12 +9,21 @@
"version": "0.0.1", "version": "0.0.1",
"license": "UNLICENSED", "license": "UNLICENSED",
"dependencies": { "dependencies": {
"@fastify/static": "^8.2.0",
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "*",
"@nestjs/platform-fastify": "^11.1.2", "@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", "cross-env": "^7.0.3",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.44.0", "drizzle-orm": "^0.44.0",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
"nest-winston": "^1.10.2", "nest-winston": "^1.10.2",
"redis": "^5.1.1", "redis": "^5.1.1",
@ -1745,6 +1754,22 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "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": { "node_modules/@fastify/ajv-compiler": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", "resolved": "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz",
@ -1929,6 +1954,65 @@
"node": ">= 10" "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": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
@ -2317,7 +2401,6 @@
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"string-width": "^5.1.2", "string-width": "^5.1.2",
@ -2335,7 +2418,6 @@
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz",
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -2348,14 +2430,12 @@
"version": "9.2.2", "version": "9.2.2",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@isaacs/cliui/node_modules/string-width": { "node_modules/@isaacs/cliui/node_modules/string-width": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"eastasianwidth": "^0.2.0", "eastasianwidth": "^0.2.0",
@ -2373,7 +2453,6 @@
"version": "8.1.0", "version": "8.1.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^6.1.0", "ansi-styles": "^6.1.0",
@ -2937,6 +3016,21 @@
"node": ">=8" "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": { "node_modules/@napi-rs/nice": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/@napi-rs/nice/-/nice-1.0.1.tgz", "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": { "node_modules/@nestjs/platform-express": {
"version": "11.1.2", "version": "11.1.2",
"resolved": "https://registry.npmmirror.com/@nestjs/platform-express/-/platform-express-11.1.2.tgz", "resolved": "https://registry.npmmirror.com/@nestjs/platform-express/-/platform-express-11.1.2.tgz",
@ -3699,6 +3813,39 @@
"tslib": "^2.1.0" "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": { "node_modules/@nestjs/testing": {
"version": "11.1.2", "version": "11.1.2",
"resolved": "https://registry.npmmirror.com/@nestjs/testing/-/testing-11.1.2.tgz", "resolved": "https://registry.npmmirror.com/@nestjs/testing/-/testing-11.1.2.tgz",
@ -3877,6 +4024,13 @@
"@redis/client": "^5.1.1" "@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": { "node_modules/@sec-ant/readable-stream": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", "resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
@ -4429,6 +4583,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmmirror.com/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmmirror.com/@types/methods/-/methods-1.1.4.tgz",
@ -4436,11 +4600,16 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@types/node": {
"version": "22.15.24", "version": "22.15.24",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.24.tgz", "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.24.tgz",
"integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==", "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -4483,6 +4652,12 @@
"integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
"license": "MIT" "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": { "node_modules/@types/yargs": {
"version": "17.0.33", "version": "17.0.33",
"resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.33.tgz", "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.33.tgz",
@ -5726,7 +5901,6 @@
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz",
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@ -5739,7 +5913,6 @@
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-convert": "^2.0.1" "color-convert": "^2.0.1"
@ -5828,7 +6001,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0" "license": "Python-2.0"
}, },
"node_modules/array-timsort": { "node_modules/array-timsort": {
@ -6022,7 +6194,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/bare-events": { "node_modules/bare-events": {
@ -6054,6 +6225,20 @@
], ],
"license": "MIT" "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": { "node_modules/bin-version": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmmirror.com/bin-version/-/bin-version-6.0.0.tgz", "resolved": "https://registry.npmmirror.com/bin-version/-/bin-version-6.0.0.tgz",
@ -6253,6 +6438,12 @@
"node": "*" "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": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
@ -6468,6 +6659,23 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/cli-cursor": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz",
@ -6627,7 +6835,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"color-name": "~1.1.4" "color-name": "~1.1.4"
@ -6764,7 +6971,6 @@
"version": "0.5.4", "version": "0.5.4",
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "5.2.1" "safe-buffer": "5.2.1"
@ -6930,6 +7136,12 @@
"node": ">= 8" "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": { "node_modules/debug": {
"version": "4.4.1", "version": "4.4.1",
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", "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", "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@ -7295,9 +7505,17 @@
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true,
"license": "MIT" "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": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz",
@ -7346,7 +7564,6 @@
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/enabled": { "node_modules/enabled": {
@ -7512,9 +7729,7 @@
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT", "license": "MIT"
"optional": true,
"peer": true
}, },
"node_modules/escape-string-regexp": { "node_modules/escape-string-regexp": {
"version": "4.0.0", "version": "4.0.0",
@ -8374,7 +8589,6 @@
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
@ -8664,7 +8878,6 @@
"version": "11.0.1", "version": "11.0.1",
"resolved": "https://registry.npmmirror.com/glob/-/glob-11.0.1.tgz", "resolved": "https://registry.npmmirror.com/glob/-/glob-11.0.1.tgz",
"integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
@ -8708,7 +8921,6 @@
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
@ -8718,7 +8930,6 @@
"version": "10.0.1", "version": "10.0.1",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.1.tgz", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
@ -8877,8 +9088,6 @@
"resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz",
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"dependencies": { "dependencies": {
"depd": "2.0.0", "depd": "2.0.0",
"inherits": "2.0.4", "inherits": "2.0.4",
@ -9079,7 +9288,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -9284,7 +9492,6 @@
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.1.1.tgz", "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"@isaacs/cliui": "^8.0.2" "@isaacs/cliui": "^8.0.2"
@ -9987,7 +10194,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
@ -10089,6 +10295,49 @@
"graceful-fs": "^4.1.6" "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": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
@ -10149,6 +10398,12 @@
"node": ">= 0.8.0" "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": { "node_modules/light-my-request": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz", "resolved": "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz",
@ -10253,6 +10508,42 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "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": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -10267,6 +10558,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz",
@ -10584,7 +10881,6 @@
"version": "7.1.2", "version": "7.1.2",
"resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
@ -10783,6 +11079,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/node-emoji": {
"version": "1.11.0", "version": "1.11.0",
"resolved": "https://registry.npmmirror.com/node-emoji/-/node-emoji-1.11.0.tgz", "resolved": "https://registry.npmmirror.com/node-emoji/-/node-emoji-1.11.0.tgz",
@ -10793,6 +11098,17 @@
"lodash": "^4.17.21" "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": { "node_modules/node-int64": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz", "resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz",
@ -11065,7 +11381,6 @@
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/parent-module": { "node_modules/parent-module": {
@ -11151,7 +11466,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.0.tgz", "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"license": "BlueOak-1.0.0", "license": "BlueOak-1.0.0",
"dependencies": { "dependencies": {
"lru-cache": "^11.0.0", "lru-cache": "^11.0.0",
@ -11168,7 +11482,6 @@
"version": "11.1.0", "version": "11.1.0",
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.1.0.tgz", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.1.0.tgz",
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": "20 || >=22" "node": "20 || >=22"
@ -11913,7 +12226,6 @@
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"devOptional": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -12130,9 +12442,7 @@
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC", "license": "ISC"
"optional": true,
"peer": true
}, },
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
@ -12235,7 +12545,6 @@
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"dev": true,
"license": "ISC", "license": "ISC",
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@ -12403,8 +12712,6 @@
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
@ -12489,7 +12796,6 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@ -12505,7 +12811,6 @@
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"emoji-regex": "^8.0.0", "emoji-regex": "^8.0.0",
@ -12520,7 +12825,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -12530,7 +12834,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@ -12543,7 +12846,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -12553,7 +12855,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@ -12566,7 +12867,6 @@
"version": "7.1.0", "version": "7.1.0",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz",
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^6.0.1" "ansi-regex": "^6.0.1"
@ -12583,7 +12883,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"
@ -12596,7 +12895,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -12724,6 +13022,15 @@
"url": "https://github.com/sponsors/ljharb" "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": { "node_modules/symbol-observable": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/symbol-observable/-/symbol-observable-4.0.0.tgz", "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", "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
"license": "MIT", "license": "MIT",
"optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
} }
@ -13421,7 +13726,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/universalify": { "node_modules/universalify": {
@ -13514,6 +13818,15 @@
"node": ">=10.12.0" "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": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz",
@ -13908,7 +14221,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
@ -13926,7 +14238,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -13936,7 +14247,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"

View File

@ -28,12 +28,21 @@
"sqlV": "drizzle-kit studio" "sqlV": "drizzle-kit studio"
}, },
"dependencies": { "dependencies": {
"@fastify/static": "^8.2.0",
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/mapped-types": "*",
"@nestjs/platform-fastify": "^11.1.2", "@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", "cross-env": "^7.0.3",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.44.0", "drizzle-orm": "^0.44.0",
"jsonwebtoken": "^9.0.2",
"mysql2": "^3.14.1", "mysql2": "^3.14.1",
"nest-winston": "^1.10.2", "nest-winston": "^1.10.2",
"redis": "^5.1.1", "redis": "^5.1.1",

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import configuration from '@/config/configuration'; import configuration from '@/common/config/configuration';
@Module({ @Module({
imports: [ imports: [

View File

@ -25,8 +25,18 @@ export default () => {
ttl: parseInt(process.env.REDIS_TTL || '3600', 10), ttl: parseInt(process.env.REDIS_TTL || '3600', 10),
}, },
jwt: { jwt: {
secret: process.env.JWT_SECRET || 'your-secret-key', secret: process.env.JWT_SECRET || 'star-tune-salt',
expiresIn: process.env.JWT_EXPIRATION || '1d', 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: { logging: {
level: process.env.LOG_LEVEL || 'info', level: process.env.LOG_LEVEL || 'info',
@ -36,11 +46,22 @@ export default () => {
console: (process.env.LOG_CONSOLE || 'true') === 'true', console: (process.env.LOG_CONSOLE || 'true') === 'true',
}, },
password: { password: {
userDefaultPassword: (process.env.USER_DEFAULT_PASSWORD || 'StarTune123') as string, userDefaultPassword:
salt: (process.env.PASSWORD_SALT || 'star-tune-salt') as string, process.env.USER_DEFAULT_PASSWORD || 'StarTune123',
iterations: parseInt(process.env.PASSWORD_ITERATIONS || '10000', 10), salt: process.env.PASSWORD_SALT || 'star-tune-salt',
iterations: parseInt(
process.env.PASSWORD_ITERATIONS || '10000',
10,
),
keylen: parseInt(process.env.PASSWORD_KEYLEN || '64', 10), keylen: parseInt(process.env.PASSWORD_KEYLEN || '64', 10),
digest: process.env.PASSWORD_DIGEST || 'sha512', digest: process.env.PASSWORD_DIGEST || 'sha512',
}, },
// 邮箱
email: {
// 验证码过期时间
codeEX: 300,
// 验证码冷却时间
codeEP: 60,
},
}; };
}; };

View File

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

View File

@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module, Global } from '@nestjs/common';
import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston'; import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import * as winston from 'winston'; import * as winston from 'winston';
import * as DailyRotateFile from 'winston-daily-rotate-file'; import * as DailyRotateFile from 'winston-daily-rotate-file';
import { join } from 'path'; import { join } from 'path';
import { CustomLogger } from '@/common/logger/logger.service'; import { CustomLogger } from '@/common/logger/logger.service';
import { allColors } from '@/type/enum';
// 定义配置接口 // 定义配置接口
interface LogConfig { interface LogConfig {
@ -50,7 +51,7 @@ const createCustomFormat = (environment: Environment) => {
message: string | Error; message: string | Error;
context?: string; context?: string;
}) => { }) => {
const { timestamp, level, message, context} = info; const { timestamp, level, message, context } = info;
// 处理错误信息 // 处理错误信息
let errorMessage = message; let errorMessage = message;
@ -98,13 +99,18 @@ const createCustomFormat = (environment: Environment) => {
: contextContent.padEnd(FORMAT_CONFIG.CONTEXT_LENGTH, ' '); : contextContent.padEnd(FORMAT_CONFIG.CONTEXT_LENGTH, ' ');
// 处理错误堆栈信息 // 处理错误堆栈信息
const stackInfo = errorStack ? `\n${levelColor}${errorStack}${reset}` : ''; const stackInfo = errorStack
? `\n${levelColor}${errorStack}${reset}`
: '';
// 返回格式化的日志字符串 // 返回格式化的日志字符串
let paddedContext = formattedContext; let paddedContext = formattedContext;
if(formattedContext.trim() === 'RequestLogger') { if (formattedContext.trim() === 'RequestLogger') {
paddedContext = `[${levelColors['debug']}${bold}${formattedContext}${reset}]`; paddedContext = `[${levelColors['info']}${bold}${formattedContext}${reset}]`;
}else{ } else if (formattedContext.trim() === 'SQL_Query') {
paddedContext = `[${allColors.text.magenta}${allColors.bg.bgWhite}${bold}${formattedContext}${reset}]`;
errorMessage = `${allColors.text.magenta}${errorMessage}`;
} else {
paddedContext = `[${formattedContext}]`; paddedContext = `[${formattedContext}]`;
} }
return `${envColor}[${envTag}] ${getPaddedPid()}${reset} - ${timestamp} ${levelColor}${upperLevel} ${reset}${paddedContext} ${levelColor}${errorMessage}${reset}${stackInfo}`; return `${envColor}[${envTag}] ${getPaddedPid()}${reset} - ${timestamp} ${levelColor}${upperLevel} ${reset}${paddedContext} ${levelColor}${errorMessage}${reset}${stackInfo}`;
@ -185,6 +191,7 @@ const createLoggerOptions = (
}; };
}; };
@Global()
@Module({ @Module({
imports: [ imports: [
WinstonModule.forRootAsync({ WinstonModule.forRootAsync({

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
import { Module, Global } from '@nestjs/common'; import { Module, Global } from '@nestjs/common';
import { UtilsServer } from './utils.server'; import { UtilsService } from './utils.service';
@Global() @Global()
@Module({ @Module({
providers: [UtilsServer], providers: [UtilsService],
exports: [UtilsServer], exports: [UtilsService],
}) })
export class UtilsModule {} export class UtilsModule {}

View File

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

View 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');
}
}
}

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

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

View 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} 不能包含特殊字符`;
},
},
});
};
}

View 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`);
*/

View 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": {}
}
}

View File

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "mysql",
"entries": [
{
"idx": 0,
"version": "5",
"when": 1748607208823,
"tag": "0000_perpetual_terrax",
"breakpoints": true
}
]
}

View File

@ -0,0 +1,2 @@
import { relations } from 'drizzle-orm/relations';
import {} from './schema';

View 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' }),
],
);

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

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

View File

@ -1,16 +1,16 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; 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 { CustomLogger } from '@/common/logger/logger.service';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { user, userPassword, userProfile } from '@/drizzle/schema'; 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'; import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class InitService implements OnModuleInit { export class InitService implements OnModuleInit {
constructor( constructor(
private readonly dbService: DatabaseService, private readonly dbService: DatabaseService,
private readonly logger: CustomLogger, private readonly logger: CustomLogger,
private readonly utilsServer: UtilsServer, private readonly utilsService: UtilsService,
private readonly configService: ConfigService, private readonly configService: ConfigService,
) {} ) {}
@ -35,9 +35,16 @@ export class InitService implements OnModuleInit {
this.logger.log('正在创建 root 用户...', 'InitService'); this.logger.log('正在创建 root 用户...', 'InitService');
// 生成密码哈希 // 生成密码哈希
const rootPassword = this.configService.get<string>('password.userDefaultPassword'); // 默认密码 const rootPassword = this.configService.get<string>(
const passwordHash = await this.utilsServer.hashPassword(rootPassword as string); 'password.userDefaultPassword',
const now = new Date().toISOString().slice(0, 23).replace('T', ' '); ); // 默认密码
const passwordHash = await this.utilsService.hashPassword(
rootPassword as string,
);
const now = new Date()
.toISOString()
.slice(0, 23)
.replace('T', ' ');
// 创建 root 用户 // 创建 root 用户
await db.transaction(async (tx) => { await db.transaction(async (tx) => {
@ -78,7 +85,11 @@ export class InitService implements OnModuleInit {
this.logger.log('root 用户已存在', 'InitService'); this.logger.log('root 用户已存在', 'InitService');
} }
} catch (error) { } catch (error) {
this.logger.error('初始化 root 用户失败', error, 'InitService'); this.logger.error(
'初始化 root 用户失败',
(error as Error).message,
'InitService',
);
throw error; throw error;
} }
} }

View File

@ -4,8 +4,10 @@ import {
NestFastifyApplication, NestFastifyApplication,
} from '@nestjs/platform-fastify'; } from '@nestjs/platform-fastify';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { AppModule } from '@/app.module'; import { AppModule } from '@/module/app.module';
import { CustomLogger } from '@/common/logger/logger.service'; import { CustomLogger } from '@/common/logger/logger.service';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
interface ServerConfig { interface ServerConfig {
port: number; port: number;
@ -16,7 +18,7 @@ async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>( const app = await NestFactory.create<NestFastifyApplication>(
AppModule, AppModule,
new FastifyAdapter({ new FastifyAdapter({
logger: false logger: false,
}), }),
); );
@ -33,6 +35,32 @@ async function bootstrap() {
const logger = app.get(CustomLogger); const logger = app.get(CustomLogger);
app.useLogger(logger); 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.error('这是一条错误日志:服务启动出现严重问题', 'Bootstrap');
logger.warn('这是一条警告日志:配置项未完全设置', 'Bootstrap'); logger.warn('这是一条警告日志:配置项未完全设置', 'Bootstrap');
@ -43,11 +71,9 @@ async function bootstrap() {
await app.listen(port, host); await app.listen(port, host);
const environment = configService.get<string>('environment'); const environment = configService.get<string>('environment');
logger.info( 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', 'Bootstrap',
); );
} }
// 调用引导函数启动应用程序
// 使用 void 运算符来明确表示我们知道这是一个未处理的 Promise这是一种最佳实践
void bootstrap(); void bootstrap();

View 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];
}
}

View File

@ -1,14 +1,13 @@
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config'; import { ConfigModule } from '@nestjs/config';
import { InitModule } from '@/common/init/init.module'; import { InitModule } from '@/init/init.module';
import { DatabaseModule } from '@/common/database/database.module'; import { DatabaseModule } from '@/service/database/database.module';
import { LoggerModule } from '@/common/logger/logger.module'; import { LoggerModule } from '@/common/logger/logger.module';
import { UtilsModule } from '@/common/utils/utils.module'; import { UtilsModule } from '@/common/utils/utils.module';
import { RedisModule } from '@/common/redis/redis.module'; import { RedisModule } from '@/service/redis/redis.module';
import configuration from '@/config/configuration'; import configuration from '@/common/config/configuration';
import { AppController } from '@/app.controller'; import { RequestLoggerMiddleware } from '@/middleware/request-logger.middleware';
import { AppService } from '@/app.service'; import { UserModule } from '@/module/user/user.module';
import { RequestLoggerMiddleware } from '@/common/middleware/request-logger.middleware';
@Module({ @Module({
imports: [ imports: [
@ -27,12 +26,13 @@ import { RequestLoggerMiddleware } from '@/common/middleware/request-logger.midd
InitModule, InitModule,
// 工具模块 // 工具模块
UtilsModule, UtilsModule,
UserModule,
], ],
controllers: [AppController], controllers: [],
providers: [AppService], providers: [],
}) })
export class AppModule implements NestModule { export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) { configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestLoggerMiddleware).forRoutes('*'); consumer.apply(RequestLoggerMiddleware).forRoutes('*path');
} }
} }

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

View File

@ -0,0 +1 @@
export class CreateUserDto {}

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

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

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

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

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

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

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

View File

@ -0,0 +1 @@
export class User {}

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

View 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. 普通登录功能
// - 登录接口,通过邮箱/用户名和密码登录登录后返回tokenReflushToken和AccessTokentoken采用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);
// }
}

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

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

View 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: '删除成功' };
// }
}

View File

@ -5,12 +5,11 @@
* 使 * 使
*/ */
import { Global, Module } from '@nestjs/common'; import { Global, Module } from '@nestjs/common';
import { LoggerModule } from '@/common/logger/logger.module'; import { DatabaseService } from '@/service/database/database.service';
import { DatabaseService } from '@/common/database/database.service';
@Global() @Global()
@Module({ @Module({
imports: [LoggerModule], imports: [],
// 提供数据库服务 // 提供数据库服务
providers: [DatabaseService], providers: [DatabaseService],
// 导出数据库服务,使其可以被其他模块注入使用 // 导出数据库服务,使其可以被其他模块注入使用

View File

@ -7,19 +7,30 @@
import { Injectable, OnModuleInit } from '@nestjs/common'; import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { drizzle, type MySql2Database } from 'drizzle-orm/mysql2'; 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 * as schema from '@/drizzle/schema';
import { CustomLogger } from '@/common/logger/logger.service'; 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() @Injectable()
export class DatabaseService implements OnModuleInit { export class DatabaseService implements OnModuleInit {
// 数据库实例,使用 drizzle ORM // 数据库实例,使用 drizzle ORM
private db!: MySql2Database<typeof schema>; public db!: MySql2Database<typeof schema>;
// MySQL 连接池实例 // MySQL 连接池实例
private pool!: Pool; private pool!: Pool;
constructor(private configService: ConfigService, private logger: CustomLogger) {} constructor(
private configService: ConfigService,
private logger: CustomLogger,
) {}
/** /**
* *
@ -27,7 +38,11 @@ export class DatabaseService implements OnModuleInit {
*/ */
async onModuleInit() { async onModuleInit() {
// 从配置服务获取数据库配置 // 从配置服务获取数据库配置
const dbConfig = this.configService.get('database'); const dbConfig = this.configService.get<DatabaseConfig>('database');
if (!dbConfig) {
throw new Error('数据库配置未找到');
}
// 先创建临时连接(不指定数据库名) // 先创建临时连接(不指定数据库名)
const tempPool = createPool({ const tempPool = createPool({
@ -38,7 +53,9 @@ 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'); this.logger.log(`数据库 ${dbConfig.database} 初始化成功`, 'InitMySQL');
await tempPool.end(); await tempPool.end();
@ -53,7 +70,18 @@ export class DatabaseService implements OnModuleInit {
}); });
// 初始化 drizzle ORM使用默认模式 // 初始化 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() { getDb() {
return this.db; return this.db;
} }
} }

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

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

View 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',
};

View File

@ -0,0 +1,6 @@
export type UserGuard = {
userId: number;
username: string;
email: string;
time: string;
};

View File

@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common'; import { INestApplication } from '@nestjs/common';
import * as request from 'supertest'; import * as request from 'supertest';
import { App } from 'supertest/types'; import { App } from 'supertest/types';
import { AppModule } from './../src/app.module'; import { AppModule } from '../src/module/app.module';
describe('AppController (e2e)', () => { describe('AppController (e2e)', () => {
let app: INestApplication<App>; let app: INestApplication<App>;