diff --git a/star-tune/eslint.config.mjs b/star-tune/eslint.config.mjs index caebf6e..9837919 100644 --- a/star-tune/eslint.config.mjs +++ b/star-tune/eslint.config.mjs @@ -28,6 +28,7 @@ export default tseslint.config( rules: { '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'warn', + "@typescript-eslint/no-unsafe-call": "off", '@typescript-eslint/no-unsafe-argument': 'warn' }, }, diff --git a/star-tune/package-lock.json b/star-tune/package-lock.json index 0078013..8b5b8fc 100644 --- a/star-tune/package-lock.json +++ b/star-tune/package-lock.json @@ -9,12 +9,21 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@fastify/static": "^8.2.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/mapped-types": "*", "@nestjs/platform-fastify": "^11.1.2", + "@nestjs/swagger": "^11.2.0", + "@types/jsonwebtoken": "^9.0.9", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "cross-env": "^7.0.3", + "dayjs": "^1.11.13", "drizzle-orm": "^0.44.0", + "jsonwebtoken": "^9.0.2", "mysql2": "^3.14.1", "nest-winston": "^1.10.2", "redis": "^5.1.1", @@ -1745,6 +1754,22 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/@fastify/accept-negotiator/-/accept-negotiator-2.0.1.tgz", + "integrity": "sha512-/c/TW2bO/v9JeEgoD/g1G5GxGeCF1Hafdf79WPmUlgYiBXummY0oX3VVq4yFkKKVBKDNlaDUYoab7g38RpPqCQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, "node_modules/@fastify/ajv-compiler": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", @@ -1929,6 +1954,65 @@ "node": ">= 10" } }, + "node_modules/@fastify/send": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@fastify/send/-/send-4.0.0.tgz", + "integrity": "sha512-eJjKDxyBnZ1iMHcmwYWG5wSA/yzVY/yrBy3Upd2+hc0omcK13tWeXRcbF28zEcbl+Z2kXEgMzJ5Rb/gXGWx9Rg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/send/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@fastify/static": { + "version": "8.2.0", + "resolved": "https://registry.npmmirror.com/@fastify/static/-/static-8.2.0.tgz", + "integrity": "sha512-PejC/DtT7p1yo3p+W7LiUtLMsV8fEvxAK15sozHy9t8kwo5r0uLYmhV/inURmGz1SkHZFz/8CNtHLPyhKcx4SQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^0.5.4", + "fastify-plugin": "^5.0.0", + "fastq": "^1.17.1", + "glob": "^11.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz", @@ -2317,7 +2401,6 @@ "version": "8.0.2", "resolved": "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^5.1.2", @@ -2335,7 +2418,6 @@ "version": "6.2.1", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2348,14 +2430,12 @@ "version": "9.2.2", "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, "license": "MIT" }, "node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", @@ -2373,7 +2453,6 @@ "version": "8.1.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", @@ -2937,6 +3016,21 @@ "node": ">=8" } }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.1", + "resolved": "https://registry.npmmirror.com/@microsoft/tsdoc/-/tsdoc-0.15.1.tgz", + "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", + "license": "MIT" + }, "node_modules/@napi-rs/nice": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/@napi-rs/nice/-/nice-1.0.1.tgz", @@ -3544,6 +3638,26 @@ } } }, + "node_modules/@nestjs/mapped-types": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz", + "integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^10.0.0 || ^11.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/platform-express": { "version": "11.1.2", "resolved": "https://registry.npmmirror.com/@nestjs/platform-express/-/platform-express-11.1.2.tgz", @@ -3699,6 +3813,39 @@ "tslib": "^2.1.0" } }, + "node_modules/@nestjs/swagger": { + "version": "11.2.0", + "resolved": "https://registry.npmmirror.com/@nestjs/swagger/-/swagger-11.2.0.tgz", + "integrity": "sha512-5wolt8GmpNcrQv34tIPUtPoV1EeFbCetm40Ij3+M0FNNnf2RJ3FyWfuQvI8SBlcJyfaounYVTKzKHreFXsUyOg==", + "license": "MIT", + "dependencies": { + "@microsoft/tsdoc": "0.15.1", + "@nestjs/mapped-types": "2.1.0", + "js-yaml": "4.1.0", + "lodash": "4.17.21", + "path-to-regexp": "8.2.0", + "swagger-ui-dist": "5.21.0" + }, + "peerDependencies": { + "@fastify/static": "^8.0.0", + "@nestjs/common": "^11.0.1", + "@nestjs/core": "^11.0.1", + "class-transformer": "*", + "class-validator": "*", + "reflect-metadata": "^0.1.12 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@fastify/static": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + } + } + }, "node_modules/@nestjs/testing": { "version": "11.1.2", "resolved": "https://registry.npmmirror.com/@nestjs/testing/-/testing-11.1.2.tgz", @@ -3877,6 +4024,13 @@ "@redis/client": "^5.1.1" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmmirror.com/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", @@ -4429,6 +4583,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmmirror.com/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmmirror.com/@types/methods/-/methods-1.1.4.tgz", @@ -4436,11 +4600,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.15.24", "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.15.24.tgz", "integrity": "sha512-w9CZGm9RDjzTh/D+hFwlBJ3ziUaVw7oufKA3vOFSOZlzmW9AkZnfjPb+DLnrV6qtgL/LNmP0/2zBNCFHL3F0ng==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -4483,6 +4652,12 @@ "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", "license": "MIT" }, + "node_modules/@types/validator": { + "version": "13.15.1", + "resolved": "https://registry.npmmirror.com/@types/validator/-/validator-13.15.1.tgz", + "integrity": "sha512-9gG6ogYcoI2mCMLdcO0NYI0AYrbxIjv0MDmy/5Ywo6CpWWrqYayc+mmgxRsCgtcGJm9BSbXkMsmxGah1iGHAAQ==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmmirror.com/@types/yargs/-/yargs-17.0.33.tgz", @@ -5726,7 +5901,6 @@ "version": "6.1.0", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -5739,7 +5913,6 @@ "version": "4.3.0", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -5828,7 +6001,6 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, "license": "Python-2.0" }, "node_modules/array-timsort": { @@ -6022,7 +6194,6 @@ "version": "1.0.2", "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/bare-events": { @@ -6054,6 +6225,20 @@ ], "license": "MIT" }, + "node_modules/bcrypt": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/bin-version": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/bin-version/-/bin-version-6.0.0.tgz", @@ -6253,6 +6438,12 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6468,6 +6659,23 @@ "dev": true, "license": "MIT" }, + "node_modules/class-transformer": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/class-transformer/-/class-transformer-0.5.1.tgz", + "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", + "license": "MIT" + }, + "node_modules/class-validator": { + "version": "0.14.2", + "resolved": "https://registry.npmmirror.com/class-validator/-/class-validator-0.14.2.tgz", + "integrity": "sha512-3kMVRF2io8N8pY1IFIXlho9r8IPUUIfHe2hYVtiebvAzU2XeQFXTv+XI4WX+TnXmtwXMDcjngcpkiPM0O9PvLw==", + "license": "MIT", + "dependencies": { + "@types/validator": "^13.11.8", + "libphonenumber-js": "^1.11.1", + "validator": "^13.9.0" + } + }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -6627,7 +6835,6 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -6764,7 +6971,6 @@ "version": "0.5.4", "resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -6930,6 +7136,12 @@ "node": ">= 8" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.1", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", @@ -7054,8 +7266,6 @@ "resolved": "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">= 0.8" } @@ -7295,9 +7505,17 @@ "version": "0.2.0", "resolved": "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/ee-first/-/ee-first-1.1.1.tgz", @@ -7346,7 +7564,6 @@ "version": "8.0.0", "resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, "license": "MIT" }, "node_modules/enabled": { @@ -7512,9 +7729,7 @@ "version": "1.0.3", "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT", - "optional": true, - "peer": true + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -8374,7 +8589,6 @@ "version": "3.3.1", "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -8664,7 +8878,6 @@ "version": "11.0.1", "resolved": "https://registry.npmmirror.com/glob/-/glob-11.0.1.tgz", "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -8708,7 +8921,6 @@ "version": "2.0.1", "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -8718,7 +8930,6 @@ "version": "10.0.1", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.0.1.tgz", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -8877,8 +9088,6 @@ "resolved": "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", @@ -9079,7 +9288,6 @@ "version": "3.0.0", "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9284,7 +9492,6 @@ "version": "4.1.1", "resolved": "https://registry.npmmirror.com/jackspeak/-/jackspeak-4.1.1.tgz", "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -9987,7 +10194,6 @@ "version": "4.1.0", "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -10089,6 +10295,49 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmmirror.com/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmmirror.com/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", @@ -10149,6 +10398,12 @@ "node": ">= 0.8.0" } }, + "node_modules/libphonenumber-js": { + "version": "1.12.8", + "resolved": "https://registry.npmmirror.com/libphonenumber-js/-/libphonenumber-js-1.12.8.tgz", + "integrity": "sha512-f1KakiQJa9tdc7w1phC2ST+DyxWimy9c3g3yeF+84QtEanJr2K77wAmBPP22riU05xldniHsvXuflnLZ4oysqA==", + "license": "MIT" + }, "node_modules/light-my-request": { "version": "6.6.0", "resolved": "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz", @@ -10253,6 +10508,42 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -10267,6 +10558,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-4.1.0.tgz", @@ -10584,7 +10881,6 @@ "version": "7.1.2", "resolved": "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "dev": true, "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" @@ -10783,6 +11079,15 @@ "dev": true, "license": "MIT" }, + "node_modules/node-addon-api": { + "version": "8.3.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-8.3.1.tgz", + "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmmirror.com/node-emoji/-/node-emoji-1.11.0.tgz", @@ -10793,6 +11098,17 @@ "lodash": "^4.17.21" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmmirror.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz", @@ -11065,7 +11381,6 @@ "version": "1.0.1", "resolved": "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/parent-module": { @@ -11151,7 +11466,6 @@ "version": "2.0.0", "resolved": "https://registry.npmmirror.com/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -11168,7 +11482,6 @@ "version": "11.1.0", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.1.0.tgz", "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -11913,7 +12226,6 @@ "version": "5.2.1", "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "devOptional": true, "funding": [ { "type": "github", @@ -12130,9 +12442,7 @@ "version": "1.2.0", "resolved": "https://registry.npmmirror.com/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC", - "optional": true, - "peer": true + "license": "ISC" }, "node_modules/shebang-command": { "version": "2.0.0", @@ -12235,7 +12545,6 @@ "version": "4.1.0", "resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -12403,8 +12712,6 @@ "resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">= 0.8" } @@ -12489,7 +12796,6 @@ "version": "4.2.3", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12505,7 +12811,6 @@ "version": "4.2.3", "resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -12520,7 +12825,6 @@ "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12530,7 +12834,6 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12543,7 +12846,6 @@ "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12553,7 +12855,6 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12566,7 +12867,6 @@ "version": "7.1.0", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -12583,7 +12883,6 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -12596,7 +12895,6 @@ "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12724,6 +13022,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.21.0", + "resolved": "https://registry.npmmirror.com/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz", + "integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, "node_modules/symbol-observable": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/symbol-observable/-/symbol-observable-4.0.0.tgz", @@ -13055,8 +13362,6 @@ "resolved": "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", "license": "MIT", - "optional": true, - "peer": true, "engines": { "node": ">=0.6" } @@ -13421,7 +13726,6 @@ "version": "6.21.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/universalify": { @@ -13514,6 +13818,15 @@ "node": ">=10.12.0" } }, + "node_modules/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmmirror.com/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmmirror.com/vary/-/vary-1.1.2.tgz", @@ -13908,7 +14221,6 @@ "version": "7.0.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -13926,7 +14238,6 @@ "version": "5.0.1", "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13936,7 +14247,6 @@ "version": "6.0.1", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" diff --git a/star-tune/package.json b/star-tune/package.json index 14b6829..e33e3bd 100644 --- a/star-tune/package.json +++ b/star-tune/package.json @@ -28,12 +28,21 @@ "sqlV": "drizzle-kit studio" }, "dependencies": { + "@fastify/static": "^8.2.0", "@nestjs/common": "^11.0.1", "@nestjs/config": "^4.0.2", "@nestjs/core": "^11.0.1", + "@nestjs/mapped-types": "*", "@nestjs/platform-fastify": "^11.1.2", + "@nestjs/swagger": "^11.2.0", + "@types/jsonwebtoken": "^9.0.9", + "bcrypt": "^6.0.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.2", "cross-env": "^7.0.3", + "dayjs": "^1.11.13", "drizzle-orm": "^0.44.0", + "jsonwebtoken": "^9.0.2", "mysql2": "^3.14.1", "nest-winston": "^1.10.2", "redis": "^5.1.1", diff --git a/star-tune/src/app.controller.spec.ts b/star-tune/src/app.controller.spec.ts deleted file mode 100644 index 2552ec5..0000000 --- a/star-tune/src/app.controller.spec.ts +++ /dev/null @@ -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); - }); - - describe('root', () => { - it('should return "Hello World!"', () => { - expect(appController.getHello()).toBe('Hello World!'); - }); - }); -}); diff --git a/star-tune/src/app.controller.ts b/star-tune/src/app.controller.ts deleted file mode 100644 index b8d8b75..0000000 --- a/star-tune/src/app.controller.ts +++ /dev/null @@ -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 { - return await this.appService.getHello(); - } -} diff --git a/star-tune/src/app.service.ts b/star-tune/src/app.service.ts deleted file mode 100644 index 8a88b94..0000000 --- a/star-tune/src/app.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export class AppService { - async getHello(): Promise { - await new Promise(resolve => setTimeout(resolve, 1000)); - let str = 'Hello World!'; - str = str.repeat(1000); - return {str}; - } -} diff --git a/star-tune/src/config/config.module.ts b/star-tune/src/common/config/config.module.ts similarity index 86% rename from star-tune/src/config/config.module.ts rename to star-tune/src/common/config/config.module.ts index dd33fdd..5311592 100644 --- a/star-tune/src/config/config.module.ts +++ b/star-tune/src/common/config/config.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import configuration from '@/config/configuration'; +import configuration from '@/common/config/configuration'; @Module({ imports: [ diff --git a/star-tune/src/config/configuration.ts b/star-tune/src/common/config/configuration.ts similarity index 64% rename from star-tune/src/config/configuration.ts rename to star-tune/src/common/config/configuration.ts index 5bb4aa1..4961426 100644 --- a/star-tune/src/config/configuration.ts +++ b/star-tune/src/common/config/configuration.ts @@ -25,8 +25,18 @@ export default () => { ttl: parseInt(process.env.REDIS_TTL || '3600', 10), }, jwt: { - secret: process.env.JWT_SECRET || 'your-secret-key', - expiresIn: process.env.JWT_EXPIRATION || '1d', + secret: process.env.JWT_SECRET || 'star-tune-salt', + accessExpiresIn: process.env.JWT_ACCESS_EXPIRES || '20m', + refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES || '14d', + whitelist: [ + '/user/login', + '/user/register', + '/user/refreshToken', + '/', + // '/module', + '/docs*', + '/docs/json', + ], }, logging: { level: process.env.LOG_LEVEL || 'info', @@ -36,11 +46,22 @@ export default () => { console: (process.env.LOG_CONSOLE || 'true') === 'true', }, password: { - userDefaultPassword: (process.env.USER_DEFAULT_PASSWORD || 'StarTune123') as string, - salt: (process.env.PASSWORD_SALT || 'star-tune-salt') as string, - iterations: parseInt(process.env.PASSWORD_ITERATIONS || '10000', 10), + userDefaultPassword: + process.env.USER_DEFAULT_PASSWORD || 'StarTune123', + salt: process.env.PASSWORD_SALT || 'star-tune-salt', + iterations: parseInt( + process.env.PASSWORD_ITERATIONS || '10000', + 10, + ), keylen: parseInt(process.env.PASSWORD_KEYLEN || '64', 10), digest: process.env.PASSWORD_DIGEST || 'sha512', }, + // 邮箱 + email: { + // 验证码过期时间 + codeEX: 300, + // 验证码冷却时间 + codeEP: 60, + }, }; }; diff --git a/star-tune/src/common/init/init.module.ts b/star-tune/src/common/init/init.module.ts deleted file mode 100644 index e804aa6..0000000 --- a/star-tune/src/common/init/init.module.ts +++ /dev/null @@ -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 {} \ No newline at end of file diff --git a/star-tune/src/common/logger/logger.module.ts b/star-tune/src/common/logger/logger.module.ts index 5e0fd02..efabd8e 100644 --- a/star-tune/src/common/logger/logger.module.ts +++ b/star-tune/src/common/logger/logger.module.ts @@ -1,10 +1,11 @@ -import { Module } from '@nestjs/common'; +import { Module, Global } from '@nestjs/common'; import { WinstonModule, WinstonModuleOptions, utilities } from 'nest-winston'; import { ConfigService } from '@nestjs/config'; import * as winston from 'winston'; import * as DailyRotateFile from 'winston-daily-rotate-file'; import { join } from 'path'; import { CustomLogger } from '@/common/logger/logger.service'; +import { allColors } from '@/type/enum'; // 定义配置接口 interface LogConfig { @@ -50,12 +51,12 @@ const createCustomFormat = (environment: Environment) => { message: string | Error; context?: string; }) => { - const { timestamp, level, message, context} = info; - + const { timestamp, level, message, context } = info; + // 处理错误信息 let errorMessage = message; let errorStack: string | undefined; - + // 如果消息是错误对象或包含堆栈信息 if (message instanceof Error) { errorMessage = message.message; @@ -98,13 +99,18 @@ const createCustomFormat = (environment: Environment) => { : contextContent.padEnd(FORMAT_CONFIG.CONTEXT_LENGTH, ' '); // 处理错误堆栈信息 - const stackInfo = errorStack ? `\n${levelColor}${errorStack}${reset}` : ''; + const stackInfo = errorStack + ? `\n${levelColor}${errorStack}${reset}` + : ''; // 返回格式化的日志字符串 let paddedContext = formattedContext; - if(formattedContext.trim() === 'RequestLogger') { - paddedContext = `[${levelColors['debug']}${bold}${formattedContext}${reset}]`; - }else{ + if (formattedContext.trim() === 'RequestLogger') { + paddedContext = `[${levelColors['info']}${bold}${formattedContext}${reset}]`; + } else if (formattedContext.trim() === 'SQL_Query') { + paddedContext = `[${allColors.text.magenta}${allColors.bg.bgWhite}${bold}${formattedContext}${reset}]`; + errorMessage = `${allColors.text.magenta}${errorMessage}`; + } else { paddedContext = `[${formattedContext}]`; } return `${envColor}[${envTag}] ${getPaddedPid()}${reset} - ${timestamp} ${levelColor}${upperLevel} ${reset}${paddedContext} ${levelColor}${errorMessage}${reset}${stackInfo}`; @@ -185,6 +191,7 @@ const createLoggerOptions = ( }; }; +@Global() @Module({ imports: [ WinstonModule.forRootAsync({ diff --git a/star-tune/src/common/logger/logger.service.ts b/star-tune/src/common/logger/logger.service.ts index c13f9cd..8936ae0 100644 --- a/star-tune/src/common/logger/logger.service.ts +++ b/star-tune/src/common/logger/logger.service.ts @@ -28,4 +28,4 @@ export class CustomLogger implements LoggerService { info(message: string, context?: string) { this.log(message, context); } -} \ No newline at end of file +} diff --git a/star-tune/src/common/middleware/request-logger.middleware.ts b/star-tune/src/common/middleware/request-logger.middleware.ts deleted file mode 100644 index 15e60c7..0000000 --- a/star-tune/src/common/middleware/request-logger.middleware.ts +++ /dev/null @@ -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('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() - } -} \ No newline at end of file diff --git a/star-tune/src/common/redis/redis.module.ts b/star-tune/src/common/redis/redis.module.ts deleted file mode 100644 index c081e4d..0000000 --- a/star-tune/src/common/redis/redis.module.ts +++ /dev/null @@ -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 {} \ No newline at end of file diff --git a/star-tune/src/common/redis/redis.service.ts b/star-tune/src/common/redis/redis.service.ts deleted file mode 100644 index 94dcd29..0000000 --- a/star-tune/src/common/redis/redis.service.ts +++ /dev/null @@ -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 { - const expireTime = ttl || this.configService.get('redis.ttl'); - await this.client.set(key, value, { EX: expireTime }); - } - - /** - * 获取键值 - * @param key 键 - * @returns 值 - */ - async get(key: string): Promise { - return await this.client.get(key); - } - - /** - * 删除键 - * @param key 键 - */ - async del(key: string): Promise { - await this.client.del(key); - } - - /** - * 检查键是否存在 - * @param key 键 - * @returns 是否存在 - */ - async exists(key: string): Promise { - return (await this.client.exists(key)) === 1; - } - - /** - * 设置键的过期时间 - * @param key 键 - * @param ttl 过期时间(秒) - */ - async expire(key: string, ttl: number): Promise { - await this.client.expire(key, ttl); - } -} \ No newline at end of file diff --git a/star-tune/src/common/utils/utils.module.ts b/star-tune/src/common/utils/utils.module.ts index d2470e0..7bed041 100644 --- a/star-tune/src/common/utils/utils.module.ts +++ b/star-tune/src/common/utils/utils.module.ts @@ -1,9 +1,9 @@ import { Module, Global } from '@nestjs/common'; -import { UtilsServer } from './utils.server'; +import { UtilsService } from './utils.service'; @Global() @Module({ - providers: [UtilsServer], - exports: [UtilsServer], + providers: [UtilsService], + exports: [UtilsService], }) -export class UtilsModule {} \ No newline at end of file +export class UtilsModule {} diff --git a/star-tune/src/common/utils/utils.server.ts b/star-tune/src/common/utils/utils.server.ts deleted file mode 100644 index dc3a312..0000000 --- a/star-tune/src/common/utils/utils.server.ts +++ /dev/null @@ -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 { - const salt = this.configService.get('password.salt'); - const iterations = this.configService.get('password.iterations'); - // 我们设置了 keylen: 128,这表示 128 字节 - // 每个字节转换为 2 个十六进制字符 - // 所以最终输出长度是 128 * 2 = 256 个字符 - const keylen = this.configService.get('password.keylen'); - const digest = this.configService.get('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 { - 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]}`; - } -} \ No newline at end of file diff --git a/star-tune/src/common/utils/utils.service.ts b/star-tune/src/common/utils/utils.service.ts new file mode 100644 index 0000000..c027fcf --- /dev/null +++ b/star-tune/src/common/utils/utils.service.ts @@ -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 { + const salt = this.configService.get('password.salt'); + const iterations = this.configService.get( + 'password.iterations', + ); + // 我们设置了 keylen: 128,这表示 128 字节 + // 每个字节转换为 2 个十六进制字符 + // 所以最终输出长度是 128 * 2 = 256 个字符 + const keylen = this.configService.get('password.keylen'); + const digest = this.configService.get('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 { + 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('jwt.secret') || 'star-tune-salt'; + // 过期时间 + const expiresIn = + this.configService.get('jwt.accessExpiresIn') || '20m'; + return sign(userGuard, secret, { expiresIn }); + } + // 生成刷新令牌 + generateRefreshToken(userGuard: UserGuard): string { + const secret = + this.configService.get('jwt.secret') || 'star-tune-salt'; + const expiresIn = + this.configService.get('jwt.refreshExpiresIn') || + '14d'; + return sign(userGuard, secret, { expiresIn }); + } + + // 验证访问令牌 + verifyToken(token: string): UserGuard { + const secret = + this.configService.get('jwt.secret') || 'star-tune-salt'; + try { + return verify(token, secret) as UserGuard; + } catch { + throw new Error('Invalid token'); + } + } +} diff --git a/star-tune/src/decorators/transform.decorator.ts b/star-tune/src/decorators/transform.decorator.ts new file mode 100644 index 0000000..cadb783 --- /dev/null +++ b/star-tune/src/decorators/transform.decorator.ts @@ -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; + }); +} diff --git a/star-tune/src/decorators/user.decorator.ts b/star-tune/src/decorators/user.decorator.ts new file mode 100644 index 0000000..2fb5754 --- /dev/null +++ b/star-tune/src/decorators/user.decorator.ts @@ -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; + }, +); diff --git a/star-tune/src/decorators/validation.decorator.ts b/star-tune/src/decorators/validation.decorator.ts new file mode 100644 index 0000000..b9c2e08 --- /dev/null +++ b/star-tune/src/decorators/validation.decorator.ts @@ -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} 不能包含特殊字符`; + }, + }, + }); + }; +} diff --git a/star-tune/src/drizzle/0000_perpetual_terrax.sql b/star-tune/src/drizzle/0000_perpetual_terrax.sql new file mode 100644 index 0000000..d0aa6ff --- /dev/null +++ b/star-tune/src/drizzle/0000_perpetual_terrax.sql @@ -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`); +*/ \ No newline at end of file diff --git a/star-tune/src/drizzle/meta/0000_snapshot.json b/star-tune/src/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..b1101b0 --- /dev/null +++ b/star-tune/src/drizzle/meta/0000_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/star-tune/src/drizzle/meta/_journal.json b/star-tune/src/drizzle/meta/_journal.json new file mode 100644 index 0000000..7850a41 --- /dev/null +++ b/star-tune/src/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "mysql", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1748607208823, + "tag": "0000_perpetual_terrax", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/star-tune/src/drizzle/relations.ts b/star-tune/src/drizzle/relations.ts new file mode 100644 index 0000000..34c5368 --- /dev/null +++ b/star-tune/src/drizzle/relations.ts @@ -0,0 +1,2 @@ +import { relations } from 'drizzle-orm/relations'; +import {} from './schema'; diff --git a/star-tune/src/drizzle/schema.ts b/star-tune/src/drizzle/schema.ts new file mode 100644 index 0000000..6e0be2f --- /dev/null +++ b/star-tune/src/drizzle/schema.ts @@ -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' }), + ], +); diff --git a/star-tune/src/guards/auth.guard.ts b/star-tune/src/guards/auth.guard.ts new file mode 100644 index 0000000..1f434ff --- /dev/null +++ b/star-tune/src/guards/auth.guard.ts @@ -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(); + const path = request.url; + + // 检查白名单路由 + const whitelist = this.config.get('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; + } +} diff --git a/star-tune/src/init/init.module.ts b/star-tune/src/init/init.module.ts new file mode 100644 index 0000000..1876d91 --- /dev/null +++ b/star-tune/src/init/init.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { InitService } from '@/init/init.service'; +@Module({ + imports: [], + providers: [InitService], + exports: [InitService], +}) +export class InitModule {} diff --git a/star-tune/src/common/init/init.service.ts b/star-tune/src/init/init.service.ts similarity index 78% rename from star-tune/src/common/init/init.service.ts rename to star-tune/src/init/init.service.ts index 2ac42ab..2236ccb 100644 --- a/star-tune/src/common/init/init.service.ts +++ b/star-tune/src/init/init.service.ts @@ -1,16 +1,16 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; -import { DatabaseService } from '@/common/database/database.service'; +import { DatabaseService } from '@/service/database/database.service'; import { CustomLogger } from '@/common/logger/logger.service'; import { eq } from 'drizzle-orm'; import { user, userPassword, userProfile } from '@/drizzle/schema'; -import { UtilsServer } from '@/common/utils/utils.server'; +import { UtilsService } from '@/common/utils/utils.service'; import { ConfigService } from '@nestjs/config'; @Injectable() export class InitService implements OnModuleInit { constructor( private readonly dbService: DatabaseService, private readonly logger: CustomLogger, - private readonly utilsServer: UtilsServer, + private readonly utilsService: UtilsService, private readonly configService: ConfigService, ) {} @@ -24,7 +24,7 @@ export class InitService implements OnModuleInit { */ private async initRootUser() { const db = this.dbService.getDb(); - + try { // 检查 root 用户是否存在 const rootUser = await db.query.user.findFirst({ @@ -33,11 +33,18 @@ export class InitService implements OnModuleInit { if (!rootUser) { this.logger.log('正在创建 root 用户...', 'InitService'); - + // 生成密码哈希 - const rootPassword = this.configService.get('password.userDefaultPassword'); // 默认密码 - const passwordHash = await this.utilsServer.hashPassword(rootPassword as string); - const now = new Date().toISOString().slice(0, 23).replace('T', ' '); + const rootPassword = this.configService.get( + 'password.userDefaultPassword', + ); // 默认密码 + const passwordHash = await this.utilsService.hashPassword( + rootPassword as string, + ); + const now = new Date() + .toISOString() + .slice(0, 23) + .replace('T', ' '); // 创建 root 用户 await db.transaction(async (tx) => { @@ -78,8 +85,12 @@ export class InitService implements OnModuleInit { this.logger.log('root 用户已存在', 'InitService'); } } catch (error) { - this.logger.error('初始化 root 用户失败', error, 'InitService'); + this.logger.error( + '初始化 root 用户失败', + (error as Error).message, + 'InitService', + ); throw error; } } -} \ No newline at end of file +} diff --git a/star-tune/src/main.ts b/star-tune/src/main.ts index 8eb63f7..5867797 100644 --- a/star-tune/src/main.ts +++ b/star-tune/src/main.ts @@ -4,8 +4,10 @@ import { NestFastifyApplication, } from '@nestjs/platform-fastify'; import { ConfigService } from '@nestjs/config'; -import { AppModule } from '@/app.module'; +import { AppModule } from '@/module/app.module'; import { CustomLogger } from '@/common/logger/logger.service'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { ValidationPipe } from '@nestjs/common'; interface ServerConfig { port: number; @@ -16,7 +18,7 @@ async function bootstrap() { const app = await NestFactory.create( AppModule, new FastifyAdapter({ - logger: false + logger: false, }), ); @@ -33,6 +35,32 @@ async function bootstrap() { const logger = app.get(CustomLogger); app.useLogger(logger); + // 设置全局路由前缀 + app.setGlobalPrefix('/api'); + + // 配置全局验证管道 + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, // 去除在验证类中不存在的属性 + transform: true, // 自动转换类型 + forbidNonWhitelisted: true, // 禁止在验证类中不存在的属性 + disableErrorMessages: false, // 在生产环境中你可能想要设置为 true + }), + ); + + // 配置 Swagger + const config = new DocumentBuilder() + .setTitle('Star Tune API') + .setDescription('Star Tune 项目的 API 文档') + .setVersion('1.0') + .addBearerAuth() + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('api/docs', app, document); + logger.debug( + `Swagger 配置完成: http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}/api/docs`, + ); + // 测试不同级别的日志输出 logger.error('这是一条错误日志:服务启动出现严重问题', 'Bootstrap'); logger.warn('这是一条警告日志:配置项未完全设置', 'Bootstrap'); @@ -43,11 +71,9 @@ async function bootstrap() { await app.listen(port, host); const environment = configService.get('environment'); logger.info( - `应用程序正在运行: http://${host==='0.0.0.0'?'127.0.0.1':host}:${port}, 环境: ${environment}`, + `应用程序正在运行: http://${host === '0.0.0.0' ? '127.0.0.1' : host}:${port}, 环境: ${environment}`, 'Bootstrap', ); } -// 调用引导函数启动应用程序 -// 使用 void 运算符来明确表示我们知道这是一个未处理的 Promise,这是一种最佳实践 void bootstrap(); diff --git a/star-tune/src/middleware/request-logger.middleware.ts b/star-tune/src/middleware/request-logger.middleware.ts new file mode 100644 index 0000000..916829c --- /dev/null +++ b/star-tune/src/middleware/request-logger.middleware.ts @@ -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('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]; + } +} diff --git a/star-tune/src/app.module.ts b/star-tune/src/module/app.module.ts similarity index 57% rename from star-tune/src/app.module.ts rename to star-tune/src/module/app.module.ts index 2d26ef3..fdf14d3 100644 --- a/star-tune/src/app.module.ts +++ b/star-tune/src/module/app.module.ts @@ -1,14 +1,13 @@ import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; -import { InitModule } from '@/common/init/init.module'; -import { DatabaseModule } from '@/common/database/database.module'; +import { InitModule } from '@/init/init.module'; +import { DatabaseModule } from '@/service/database/database.module'; import { LoggerModule } from '@/common/logger/logger.module'; import { UtilsModule } from '@/common/utils/utils.module'; -import { RedisModule } from '@/common/redis/redis.module'; -import configuration from '@/config/configuration'; -import { AppController } from '@/app.controller'; -import { AppService } from '@/app.service'; -import { RequestLoggerMiddleware } from '@/common/middleware/request-logger.middleware'; +import { RedisModule } from '@/service/redis/redis.module'; +import configuration from '@/common/config/configuration'; +import { RequestLoggerMiddleware } from '@/middleware/request-logger.middleware'; +import { UserModule } from '@/module/user/user.module'; @Module({ imports: [ @@ -27,12 +26,13 @@ import { RequestLoggerMiddleware } from '@/common/middleware/request-logger.midd InitModule, // 工具模块 UtilsModule, + UserModule, ], - controllers: [AppController], - providers: [AppService], + controllers: [], + providers: [], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { - consumer.apply(RequestLoggerMiddleware).forRoutes('*'); + consumer.apply(RequestLoggerMiddleware).forRoutes('*path'); } } diff --git a/star-tune/src/module/user/dto/check-email.dto.ts b/star-tune/src/module/user/dto/check-email.dto.ts new file mode 100644 index 0000000..3d13fdc --- /dev/null +++ b/star-tune/src/module/user/dto/check-email.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/dto/create-user.dto.ts b/star-tune/src/module/user/dto/create-user.dto.ts new file mode 100644 index 0000000..0311be1 --- /dev/null +++ b/star-tune/src/module/user/dto/create-user.dto.ts @@ -0,0 +1 @@ +export class CreateUserDto {} diff --git a/star-tune/src/module/user/dto/email-login.dto.ts b/star-tune/src/module/user/dto/email-login.dto.ts new file mode 100644 index 0000000..b1d2720 --- /dev/null +++ b/star-tune/src/module/user/dto/email-login.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/dto/login.dto.ts b/star-tune/src/module/user/dto/login.dto.ts new file mode 100644 index 0000000..d98433b --- /dev/null +++ b/star-tune/src/module/user/dto/login.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/dto/register.dto.ts b/star-tune/src/module/user/dto/register.dto.ts new file mode 100644 index 0000000..45aae60 --- /dev/null +++ b/star-tune/src/module/user/dto/register.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/dto/send-email-code.dto.ts b/star-tune/src/module/user/dto/send-email-code.dto.ts new file mode 100644 index 0000000..9311125 --- /dev/null +++ b/star-tune/src/module/user/dto/send-email-code.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/dto/update-profile.dto.ts b/star-tune/src/module/user/dto/update-profile.dto.ts new file mode 100644 index 0000000..80a6ace --- /dev/null +++ b/star-tune/src/module/user/dto/update-profile.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/dto/update-signature.dto.ts b/star-tune/src/module/user/dto/update-signature.dto.ts new file mode 100644 index 0000000..fe5bd0b --- /dev/null +++ b/star-tune/src/module/user/dto/update-signature.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/dto/update-user.dto.ts b/star-tune/src/module/user/dto/update-user.dto.ts new file mode 100644 index 0000000..de17b84 --- /dev/null +++ b/star-tune/src/module/user/dto/update-user.dto.ts @@ -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; +} diff --git a/star-tune/src/module/user/entities/user.entity.ts b/star-tune/src/module/user/entities/user.entity.ts new file mode 100644 index 0000000..4f82c14 --- /dev/null +++ b/star-tune/src/module/user/entities/user.entity.ts @@ -0,0 +1 @@ +export class User {} diff --git a/star-tune/src/module/user/user.controller.spec.ts b/star-tune/src/module/user/user.controller.spec.ts new file mode 100644 index 0000000..338bf3a --- /dev/null +++ b/star-tune/src/module/user/user.controller.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/star-tune/src/module/user/user.controller.ts b/star-tune/src/module/user/user.controller.ts new file mode 100644 index 0000000..d507939 --- /dev/null +++ b/star-tune/src/module/user/user.controller.ts @@ -0,0 +1,153 @@ +import { + Controller, + Get, + Post, + Body, + Patch, + Delete, + UseGuards, + Headers, + UnauthorizedException, +} from '@nestjs/common'; +import { UserService } from './user.service'; +import { CheckEmailDto } from './dto/check-email.dto'; +import { SendEmailCodeDto } from './dto/send-email-code.dto'; +import { RegisterDto } from './dto/register.dto'; +import { LoginDto } from './dto/login.dto'; +import { EmailLoginDto } from './dto/email-login.dto'; +import { UpdateUserDto } from './dto/update-user.dto'; +import { UpdateProfileDto } from './dto/update-profile.dto'; +import { UpdateSignatureDto } from './dto/update-signature.dto'; +import { AuthGuard } from '../../guards/auth.guard'; +import { User } from '../../decorators/user.decorator'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiResponse, +} from '@nestjs/swagger'; + +@ApiTags('用户管理') +@Controller('user') +export class UserController { + constructor(private readonly userService: UserService) {} + + // 实现功能注意 + // 数据库采用drizzle,已全局注入,使用时直接注入即可 + // 日志采用nestjs的logger,已全局注入,使用时直接注入即可 + // 缓存采用redis,已全局注入,使用时直接注入即可 + // 工具类采用utils,已全局注入,使用时直接注入即可 + // 配置采用config,已全局注入,使用时直接注入即可 + // 数据实体在src/drizzle/schema.ts中,使用时直接引入即可 + + // 1. 邮箱注册功能 + // - 检查邮箱是否可用接口 + // - 发送验证码接口,通过配置系统邮箱发送给目标邮箱邮件,60秒内不允许再发,5分钟内复用上次验证码,采用redis + // - 注册接口,完善用户信息,通过验证码注册,注册后自动登录,清除邮箱验证码,注意加分布式锁,防止重复注册 + + // 2. 普通登录功能 + // - 登录接口,通过邮箱/用户名和密码登录,登录后返回token(ReflushToken和AccessToken),token采用jwt,采用redis缓存,缓存时间AccessToken10分钟,ReflushToken14天, + // - 退出登录接口,清除两个token,清除redis缓存 + // - 刷新token接口,通过ReflushToken刷新AccessToken,刷新后返回新的token,刷新后清除旧的token,清除redis缓存 + + // 3. 邮箱登录功能 + // - 登录接口,通过邮箱和验证码登录,验证码逻辑同邮箱注册功能,但是要区分 + // - token逻辑和普通登录功能一致 + + // 4. 个人用户信息功能 + // - 获取用户信息接口,通过token获取用户信息 + // - 更新用户信息接口,通过token更新用户信息,更新时注意,基础用户表为一个接口,个人简介和个性签名均为单独接口 + // - 删除用户信息接口,通过token删除用户信息 + + @ApiOperation({ summary: '检查邮箱是否可用' }) + @ApiResponse({ status: 200, description: '邮箱检查结果' }) + @Post('check-email') + checkEmail(@Body() dto: CheckEmailDto) { + return this.userService.checkEmail(dto); + } + + @ApiOperation({ summary: '发送邮箱验证码' }) + @ApiResponse({ status: 200, description: '验证码发送成功' }) + @ApiResponse({ + status: 400, + description: '发送失败,可能是因为发送过于频繁', + }) + @Post('send-code') + sendEmailCode(@Body() dto: SendEmailCodeDto) { + return this.userService.sendEmailCode(dto); + } + + @ApiOperation({ summary: '用户注册' }) + @ApiResponse({ status: 201, description: '注册成功并返回用户信息' }) + @ApiResponse({ + status: 400, + description: '注册失败,可能是验证码错误或用户已存在', + }) + @Post('register') + register(@Body() dto: RegisterDto) { + return this.userService.register(dto); + } + + // @Post('login') + // login(@Body() dto: LoginDto) { + // return this.userService.login(dto); + // } + + // @Post('email-login') + // emailLogin(@Body() dto: EmailLoginDto) { + // return this.userService.emailLogin(dto); + // } + + // @Post('refresh-token') + // refreshToken(@Headers('refresh-token') refreshToken: string) { + // if (!refreshToken) { + // throw new UnauthorizedException('缺少刷新令牌'); + // } + // return this.userService.refreshToken(refreshToken); + // } + + // @Post('logout') + // @UseGuards(AuthGuard) + // logout(@User('userId') userId: number) { + // return this.userService.logout(userId); + // } + + // @Get('info') + // @UseGuards(AuthGuard) + // getUserInfo(@User('userId') userId: number) { + // return this.userService.getUserInfo(userId); + // } + + // @Patch() + // @UseGuards(AuthGuard) + // updateUser( + // @User('userId') userId: number, + // @Body() dto: UpdateUserDto, + // ) { + // return this.userService.updateUser(userId, dto); + // } + + // @Patch('profile') + // @UseGuards(AuthGuard) + // updateProfile( + // @User('userId') userId: number, + // @Body() dto: UpdateProfileDto, + // ) { + // return this.userService.updateProfile(userId, dto); + // } + + // @Patch('signature') + // @UseGuards(AuthGuard) + // updateSignature( + // @User('userId') userId: number, + // @Body() dto: UpdateSignatureDto, + // ) { + // return this.userService.updateSignature(userId, dto); + // } + + // @Delete() + // @UseGuards(AuthGuard) + // deleteUser(@User('userId') userId: number) { + // return this.userService.deleteUser(userId); + // } +} diff --git a/star-tune/src/module/user/user.module.ts b/star-tune/src/module/user/user.module.ts new file mode 100644 index 0000000..e17a0cf --- /dev/null +++ b/star-tune/src/module/user/user.module.ts @@ -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 {} diff --git a/star-tune/src/module/user/user.service.spec.ts b/star-tune/src/module/user/user.service.spec.ts new file mode 100644 index 0000000..2cf76ba --- /dev/null +++ b/star-tune/src/module/user/user.service.spec.ts @@ -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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/star-tune/src/module/user/user.service.ts b/star-tune/src/module/user/user.service.ts new file mode 100644 index 0000000..cec0746 --- /dev/null +++ b/star-tune/src/module/user/user.service.ts @@ -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('email.codeEX') || 300; + // 验证码冷却时间 + const codeEP = this.config.get('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: '删除成功' }; + // } +} diff --git a/star-tune/src/common/database/database.module.ts b/star-tune/src/service/database/database.module.ts similarity index 67% rename from star-tune/src/common/database/database.module.ts rename to star-tune/src/service/database/database.module.ts index 72774a4..5222fec 100644 --- a/star-tune/src/common/database/database.module.ts +++ b/star-tune/src/service/database/database.module.ts @@ -1,19 +1,18 @@ /** * 数据库模块 - * + * * 这是一个全局模块,使用 @Global() 装饰器使其在整个应用中可用 * 不需要在每个使用数据库的模块中重复导入 */ import { Global, Module } from '@nestjs/common'; -import { LoggerModule } from '@/common/logger/logger.module'; -import { DatabaseService } from '@/common/database/database.service'; +import { DatabaseService } from '@/service/database/database.service'; @Global() @Module({ - imports: [LoggerModule], + imports: [], // 提供数据库服务 providers: [DatabaseService], // 导出数据库服务,使其可以被其他模块注入使用 exports: [DatabaseService], }) -export class DatabaseModule {} \ No newline at end of file +export class DatabaseModule {} diff --git a/star-tune/src/common/database/database.service.ts b/star-tune/src/service/database/database.service.ts similarity index 63% rename from star-tune/src/common/database/database.service.ts rename to star-tune/src/service/database/database.service.ts index 09b4169..d2ae362 100644 --- a/star-tune/src/common/database/database.service.ts +++ b/star-tune/src/service/database/database.service.ts @@ -1,25 +1,36 @@ /** * 数据库服务 - * + * * 负责管理数据库连接和提供数据库操作接口 * 实现了 OnModuleInit 接口,在模块初始化时自动建立数据库连接 */ import { Injectable, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { drizzle, type MySql2Database } from 'drizzle-orm/mysql2'; -import { createPool, Pool} from 'mysql2/promise'; +import { createPool, Pool } from 'mysql2/promise'; import * as schema from '@/drizzle/schema'; import { CustomLogger } from '@/common/logger/logger.service'; -import { type ResultSetHeader } from 'mysql2'; + +// 定义数据库配置接口 +interface DatabaseConfig { + host: string; + port: number; + username: string; + password: string; + database: string; +} @Injectable() export class DatabaseService implements OnModuleInit { // 数据库实例,使用 drizzle ORM - private db!: MySql2Database; + public db!: MySql2Database; // MySQL 连接池实例 private pool!: Pool; - constructor(private configService: ConfigService, private logger: CustomLogger) {} + constructor( + private configService: ConfigService, + private logger: CustomLogger, + ) {} /** * 模块初始化时自动执行 @@ -27,8 +38,12 @@ export class DatabaseService implements OnModuleInit { */ async onModuleInit() { // 从配置服务获取数据库配置 - const dbConfig = this.configService.get('database'); - + const dbConfig = this.configService.get('database'); + + if (!dbConfig) { + throw new Error('数据库配置未找到'); + } + // 先创建临时连接(不指定数据库名) const tempPool = createPool({ host: dbConfig.host, @@ -38,8 +53,10 @@ export class DatabaseService implements OnModuleInit { }); // 创建数据库(如果不存在) - await tempPool.query(`CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\``) as [ResultSetHeader, any]; - + await tempPool.query( + `CREATE DATABASE IF NOT EXISTS \`${dbConfig.database}\``, + ); + this.logger.log(`数据库 ${dbConfig.database} 初始化成功`, 'InitMySQL'); await tempPool.end(); @@ -53,7 +70,18 @@ export class DatabaseService implements OnModuleInit { }); // 初始化 drizzle ORM,使用默认模式 - this.db = drizzle(this.pool, { schema, mode: 'default' }); + this.db = drizzle(this.pool, { + schema, + mode: 'default', + logger: { + logQuery: (query, params) => { + this.logger.debug( + `SQL: ${query} - Params: ${JSON.stringify(params)}`, + 'SQL_Query', + ); + }, + }, + }); } /** @@ -71,5 +99,4 @@ export class DatabaseService implements OnModuleInit { getDb() { return this.db; } - -} \ No newline at end of file +} diff --git a/star-tune/src/service/redis/redis.module.ts b/star-tune/src/service/redis/redis.module.ts new file mode 100644 index 0000000..d392705 --- /dev/null +++ b/star-tune/src/service/redis/redis.module.ts @@ -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 {} diff --git a/star-tune/src/service/redis/redis.service.ts b/star-tune/src/service/redis/redis.service.ts new file mode 100644 index 0000000..0d2b7fc --- /dev/null +++ b/star-tune/src/service/redis/redis.service.ts @@ -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('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('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 { + const expireTime = ttl || this.configService.get('redis.ttl'); + await this.client.set(key, value, { EX: expireTime }); + } + + /** + * 获取键值 + * @param key 键 + * @returns 值 + */ + async get(key: string): Promise { + return await this.client.get(key); + } + + /** + * 删除键 + * @param key 键 + */ + async del(key: string): Promise { + await this.client.del(key); + } + + /** + * 检查键是否存在 + * @param key 键 + * @returns 是否存在 + */ + async exists(key: string): Promise { + return (await this.client.exists(key)) === 1; + } + + /** + * 设置键的过期时间 + * @param key 键 + * @param ttl 过期时间(秒) + */ + async expire(key: string, ttl: number): Promise { + await this.client.expire(key, ttl); + } + + /** + * 尝试获取分布式锁 + * @param key 锁的键 + * @param ttl 锁的过期时间(秒) + * @returns 是否成功获取锁 + */ + async lock(key: string, ttl: number = 30): Promise { + 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 { + const lockKey = `lock:${key}`; + await this.client.del(lockKey); + } + + /** + * 获取键的剩余过期时间 + * @param key 键 + * @returns 剩余过期时间(秒),如果键不存在返回-2,如果键没有过期时间返回-1 + */ + async ttl(key: string): Promise { + return await this.client.ttl(key); + } + + /** + * 使用自动释放的分布式锁执行操作 + * @param key 锁的键 + * @param fn 需要在锁中执行的函数 + * @param ttl 锁的过期时间(秒) + * @returns 函数执行的结果 + */ + async withLock( + key: string, + fn: () => Promise, + ttl: number = 30, + ): Promise { + const locked = await this.lock(key, ttl); + if (!locked) { + throw new Error('无法获取锁'); + } + + try { + return await fn(); + } finally { + await this.unlock(key); + } + } +} diff --git a/star-tune/src/type/enum.ts b/star-tune/src/type/enum.ts new file mode 100644 index 0000000..56f2e44 --- /dev/null +++ b/star-tune/src/type/enum.ts @@ -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', +}; \ No newline at end of file diff --git a/star-tune/src/type/userGuard.ts b/star-tune/src/type/userGuard.ts new file mode 100644 index 0000000..ff7763f --- /dev/null +++ b/star-tune/src/type/userGuard.ts @@ -0,0 +1,6 @@ +export type UserGuard = { + userId: number; + username: string; + email: string; + time: string; +}; diff --git a/star-tune/test/app.e2e-spec.ts b/star-tune/test/app.e2e-spec.ts index 794b20c..90b85b0 100644 --- a/star-tune/test/app.e2e-spec.ts +++ b/star-tune/test/app.e2e-spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { App } from 'supertest/types'; -import { AppModule } from './../src/app.module'; +import { AppModule } from '../src/module/app.module'; describe('AppController (e2e)', () => { let app: INestApplication;