diff --git a/bun.lock b/bun.lock index 803d320..de623c8 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@elysiajs/jwt": "^1.3.1", "@elysiajs/swagger": "^1.3.0", "@types/ua-parser-js": "^0.7.39", + "canvas": "^3.1.2", "chalk": "^5.4.1", "drizzle-orm": "^0.44.2", "mysql2": "^3.14.1", @@ -276,10 +277,16 @@ "balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "brace-expansion": ["brace-expansion@1.1.12", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "braces": ["braces@3.0.3", "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.2.17", "https://registry.npmmirror.com/bun-types/-/bun-types-1.2.17.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], @@ -290,12 +297,16 @@ "callsites": ["callsites@3.1.0", "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + "canvas": ["canvas@3.1.2", "", { "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" } }, "sha512-Z/tzFAcBzoCvJlOSlCnoekh1Gu8YMn0J51+UAuXJAbW1Z6I9l2mZgdD7738MepoeeIcUdDtbMnOg6cC7GJxy/g=="], + "chai": ["chai@5.2.0", "https://registry.npmmirror.com/chai/-/chai-5.2.0.tgz", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], "chalk": ["chalk@5.4.1", "https://registry.npmmirror.com/chalk/-/chalk-5.4.1.tgz", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "check-error": ["check-error@2.1.1", "https://registry.npmmirror.com/check-error/-/check-error-2.1.1.tgz", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], "color": ["color@3.2.1", "https://registry.npmmirror.com/color/-/color-3.2.1.tgz", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], @@ -318,8 +329,12 @@ "debug": ["debug@4.4.1", "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "deep-eql": ["deep-eql@5.0.2", "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "deep-is": ["deep-is@0.1.4", "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "delayed-stream": ["delayed-stream@1.0.0", "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], @@ -328,6 +343,8 @@ "detect-europe-js": ["detect-europe-js@0.1.2", "https://registry.npmmirror.com/detect-europe-js/-/detect-europe-js-0.1.2.tgz", {}, "sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow=="], + "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], + "drizzle-kit": ["drizzle-kit@0.31.4", "https://registry.npmmirror.com/drizzle-kit/-/drizzle-kit-0.31.4.tgz", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="], "drizzle-orm": ["drizzle-orm@0.44.2", "https://registry.npmmirror.com/drizzle-orm/-/drizzle-orm-0.44.2.tgz", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ=="], @@ -338,6 +355,8 @@ "enabled": ["enabled@2.0.0", "https://registry.npmmirror.com/enabled/-/enabled-2.0.0.tgz", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -376,6 +395,8 @@ "exact-mirror": ["exact-mirror@0.1.2", "https://registry.npmmirror.com/exact-mirror/-/exact-mirror-0.1.2.tgz", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-wFCPCDLmHbKGUb8TOi/IS7jLsgR8WVDGtDK3CzcB4Guf/weq7G+I+DkXiRSZfbemBFOxOINKpraM6ml78vo8Zw=="], + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.2.1", "https://registry.npmmirror.com/expect-type/-/expect-type-1.2.1.tgz", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], @@ -414,6 +435,8 @@ "form-data": ["form-data@4.0.3", "https://registry.npmmirror.com/form-data/-/form-data-4.0.3.tgz", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="], + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + "fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -426,6 +449,8 @@ "get-tsconfig": ["get-tsconfig@4.10.1", "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.10.1.tgz", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="], + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "glob-parent": ["glob-parent@6.0.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@14.0.0", "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -456,6 +481,8 @@ "inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + "is-arrayish": ["is-arrayish@0.3.2", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.2.tgz", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], "is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], @@ -516,8 +543,14 @@ "mime-types": ["mime-types@2.1.35", "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + "minimatch": ["minimatch@3.1.2", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "moment": ["moment@2.30.1", "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -528,14 +561,22 @@ "nanoid": ["nanoid@5.1.5", "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.5.tgz", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + "natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + "node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], "nodemailer": ["nodemailer@7.0.4", "https://registry.npmmirror.com/nodemailer/-/nodemailer-7.0.4.tgz", {}, "sha512-9O00Vh89/Ld2EcVCqJ/etd7u20UhME0f/NToPfArwPEe1Don1zy4mAIz6ariRr7mJ2RDxtaDzN0WJVdVXPtZaw=="], "object-hash": ["object-hash@3.0.0", "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "https://registry.npmmirror.com/one-time/-/one-time-1.0.0.tgz", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], "openapi-types": ["openapi-types@12.1.3", "https://registry.npmmirror.com/openapi-types/-/openapi-types-12.1.3.tgz", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], @@ -562,14 +603,20 @@ "postcss": ["postcss@8.5.6", "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "prettier": ["prettier@3.6.2", "https://registry.npmmirror.com/prettier/-/prettier-3.6.2.tgz", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "queue-microtask": ["queue-microtask@1.2.3", "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + "readable-stream": ["readable-stream@3.6.2", "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "redis": ["redis@5.5.6", "https://registry.npmmirror.com/redis/-/redis-5.5.6.tgz", { "dependencies": { "@redis/bloom": "5.5.6", "@redis/client": "5.5.6", "@redis/json": "5.5.6", "@redis/search": "5.5.6", "@redis/time-series": "5.5.6" } }, "sha512-hbpqBfcuhWHOS9YLNcXcJ4akNr7HFX61Dq3JuFZ9S7uU7C7kvnzuH2PDIXOP62A3eevvACoG8UacuXP3N07xdg=="], @@ -600,6 +647,10 @@ "siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "simple-swizzle": ["simple-swizzle@0.2.2", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], "source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -626,6 +677,10 @@ "supports-color": ["supports-color@7.2.0", "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "text-hex": ["text-hex@1.0.0", "https://registry.npmmirror.com/text-hex/-/text-hex-1.0.0.tgz", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "tinybench": ["tinybench@2.9.0", "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -650,6 +705,8 @@ "ts-api-utils": ["ts-api-utils@2.1.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + "type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-fest": ["type-fest@4.41.0", "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], @@ -692,6 +749,8 @@ "word-wrap": ["word-wrap@1.2.5", "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + "yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "zhead": ["zhead@2.2.4", "https://registry.npmmirror.com/zhead/-/zhead-2.2.4.tgz", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], @@ -726,6 +785,8 @@ "postcss/nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], diff --git a/package.json b/package.json index 8769eed..b162337 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@elysiajs/jwt": "^1.3.1", "@elysiajs/swagger": "^1.3.0", "@types/ua-parser-js": "^0.7.39", + "canvas": "^3.1.2", "chalk": "^5.4.1", "drizzle-orm": "^0.44.2", "mysql2": "^3.14.1", diff --git a/src/config/db.config.ts b/src/config/db.config.ts index 4f29b1c..0f1fb3d 100644 --- a/src/config/db.config.ts +++ b/src/config/db.config.ts @@ -17,13 +17,13 @@ */ export const dbConfig = { /** 数据库主机地址 */ - host: process.env.DB_HOST || '172.16.1.3', + host: process.env.DB_HOST || 'uair.cc', /** 数据库端口号 */ port: Number(process.env.DB_PORT) || 3306, /** 数据库用户名 */ - user: process.env.DB_USER || 'docker', + user: process.env.DB_USER || 'nie', /** 数据库密码 */ - password: process.env.DB_PASSWORD || 'docker', + password: process.env.DB_PASSWORD || 'nie', /** 数据库名称 */ - database: process.env.DB_NAME || 'docker', + database: process.env.DB_NAME || 'nie', }; diff --git a/src/config/email.config.ts b/src/config/email.config.ts index 0973d4d..445d6dd 100644 --- a/src/config/email.config.ts +++ b/src/config/email.config.ts @@ -23,8 +23,8 @@ export const smtpConfig = { secure: process.env.SMTP_SECURE === 'true' || false, /** 认证信息 */ auth: { - user: process.env.SMTP_USER || '', - pass: process.env.SMTP_PASS || '', + user: process.env.SMTP_USER || 'togy.gc@qq.com', + pass: process.env.SMTP_PASS || 'xmqeoeydzdgzddej', }, /** 连接超时时间(毫秒) */ connectionTimeout: Number(process.env.SMTP_TIMEOUT) || 60000, @@ -42,11 +42,11 @@ export const smtpConfig = { */ export const emailConfig = { /** 发件人信息 - QQ邮箱要求From地址必须与SMTP用户名一致 */ - from: process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER || '', + from: process.env.SMTP_FROM_EMAIL || process.env.SMTP_USER || 'togy.gc@qq.com', /** 发件人名称 */ - fromName: process.env.SMTP_FROM_NAME || '星撰系统', + fromName: process.env.SMTP_FROM_NAME || '星撰玉衡', /** 回复邮箱 */ - replyTo: process.env.EMAIL_REPLY_TO || process.env.SMTP_USER || '', + replyTo: process.env.EMAIL_REPLY_TO || process.env.SMTP_USER || 'expressgy@qq.com', /** 字符编码 */ charset: 'utf-8', /** 邮件优先级 */ diff --git a/src/config/redis.config.ts b/src/config/redis.config.ts index 0c151e0..2de2a04 100644 --- a/src/config/redis.config.ts +++ b/src/config/redis.config.ts @@ -21,13 +21,13 @@ export const redisConfig = { /** Redis连接名称 */ connectName: process.env.REDIS_CONNECT_NAME || 'cursor-init-redis', /** Redis服务器主机地址 */ - host: process.env.REDIS_HOST || '172.16.1.3', + host: process.env.REDIS_HOST || 'uair.cc', /** Redis服务器端口号 */ port: Number(process.env.REDIS_PORT) || 6379, /** Redis用户名 */ username: process.env.REDIS_USERNAME || 'default', /** Redis密码 */ - password: process.env.REDIS_PASSWORD || 'docker', + password: process.env.REDIS_PASSWORD || 'nie', /** Redis数据库索引 */ database: Number(process.env.REDIS_DATABASE) || 0, }; diff --git a/src/modules/captcha/captcha.controller.ts b/src/modules/captcha/captcha.controller.ts new file mode 100644 index 0000000..e5595f6 --- /dev/null +++ b/src/modules/captcha/captcha.controller.ts @@ -0,0 +1,86 @@ +/** + * @file 验证码控制器 + * @author AI助手 + * @date 2024-12-27 + * @description 验证码相关的API路由定义 + */ + +import { Elysia, t } from 'elysia'; +import { GenerateCaptchaSchema, VerifyCaptchaSchema } from './captcha.schema'; +import { GenerateCaptchaResponses, VerifyCaptchaResponses } from './captcha.response'; +import { captchaService } from './captcha.service'; +import { tags } from '@/modules/tags'; + +export const captchaController = new Elysia() + /** + * 生成验证码 + * @route POST /api/captcha/generate + */ + .post( + '/generate', + ({ body }) => captchaService.generateCaptcha(body), + { + body: GenerateCaptchaSchema, + detail: { + summary: '生成验证码', + description: '生成图形验证码,支持自定义尺寸和过期时间', + tags: [tags.captcha], + }, + response: GenerateCaptchaResponses, + } + ) + + /** + * 验证验证码 + * @route POST /api/captcha/verify + */ + .post( + '/verify', + ({ body }) => captchaService.verifyCaptcha(body), + { + body: VerifyCaptchaSchema, + detail: { + summary: '验证验证码', + description: '验证用户输入的验证码是否正确', + tags: [tags.captcha], + }, + response: VerifyCaptchaResponses, + } + ) + + /** + * 清理过期验证码(管理接口) + * @route POST /api/captcha/cleanup + */ + .post( + '/cleanup', + async () => { + const cleanedCount = await captchaService.cleanupExpiredCaptchas(); + return { + code: 'SUCCESS' as const, + message: '清理完成', + data: { cleanedCount } + }; + }, + { + detail: { + summary: '清理过期验证码', + description: '清理Redis中已过期的验证码数据', + tags: [tags.captcha], + }, + response: { + 200: t.Object({ + code: t.Literal('SUCCESS'), + message: t.String(), + data: t.Object({ + cleanedCount: t.Number() + }) + }), + 500: t.Object({ + code: t.Literal('INTERNAL_ERROR'), + message: t.String(), + data: t.Null(), + }), + }, + } + ); \ No newline at end of file diff --git a/src/modules/captcha/captcha.response.ts b/src/modules/captcha/captcha.response.ts new file mode 100644 index 0000000..cd977d4 --- /dev/null +++ b/src/modules/captcha/captcha.response.ts @@ -0,0 +1,94 @@ +/** + * @file 验证码模块响应格式定义 + * @author AI助手 + * @date 2024-12-27 + * @description 验证码相关接口的响应格式定义 + */ + +import { t, type Static } from 'elysia'; +import { globalResponseWrapperSchema } from '@/validators/global.response'; +import { CaptchaGenerateResponseSchema } from './captcha.schema'; + +/** + * 生成验证码成功响应 + */ +export const GenerateCaptchaSuccessResponseSchema = globalResponseWrapperSchema(CaptchaGenerateResponseSchema); +export type GenerateCaptchaSuccessResponse = Static; + +/** + * 验证验证码成功响应 + */ +export const VerifyCaptchaSuccessResponseSchema = globalResponseWrapperSchema(t.Object({ + valid: t.Boolean({ description: '验证结果' }), + message: t.String({ description: '验证消息' }) +})); +export type VerifyCaptchaSuccessResponse = Static; + +/** + * 验证码不存在错误响应 + */ +export const CaptchaNotFoundResponseSchema = t.Object({ + code: t.Literal('CAPTCHA_NOT_FOUND'), + message: t.String({ examples: ['验证码不存在或已过期'] }), + data: t.Null(), +}); +export type CaptchaNotFoundResponse = Static; + +/** + * 验证码错误响应 + */ +export const CaptchaInvalidResponseSchema = t.Object({ + code: t.Literal('CAPTCHA_INVALID'), + message: t.String({ examples: ['验证码错误'] }), + data: t.Null(), +}); +export type CaptchaInvalidResponse = Static; + +/** + * 验证码过期错误响应 + */ +export const CaptchaExpiredResponseSchema = t.Object({ + code: t.Literal('CAPTCHA_EXPIRED'), + message: t.String({ examples: ['验证码已过期'] }), + data: t.Null(), +}); +export type CaptchaExpiredResponse = Static; + +/** + * 生成验证码接口响应组合 + */ +export const GenerateCaptchaResponses = { + 200: GenerateCaptchaSuccessResponseSchema, + 400: t.Object({ + code: t.Literal('VALIDATION_ERROR'), + message: t.String(), + data: t.Null(), + }), + 500: t.Object({ + code: t.Literal('INTERNAL_ERROR'), + message: t.String(), + data: t.Null(), + }), +}; + +/** + * 验证验证码接口响应组合 + */ +export const VerifyCaptchaResponses = { + 200: VerifyCaptchaSuccessResponseSchema, + 400: t.Union([ + CaptchaNotFoundResponseSchema, + CaptchaInvalidResponseSchema, + CaptchaExpiredResponseSchema, + t.Object({ + code: t.Literal('VALIDATION_ERROR'), + message: t.String(), + data: t.Null(), + }) + ]), + 500: t.Object({ + code: t.Literal('INTERNAL_ERROR'), + message: t.String(), + data: t.Null(), + }), +}; \ No newline at end of file diff --git a/src/modules/captcha/captcha.schema.ts b/src/modules/captcha/captcha.schema.ts new file mode 100644 index 0000000..b671e08 --- /dev/null +++ b/src/modules/captcha/captcha.schema.ts @@ -0,0 +1,101 @@ +/** + * @file 验证码模块Schema定义 + * @author AI助手 + * @date 2024-12-27 + * @description 验证码生成、验证相关的数据结构定义 + */ + +import { t, type Static } from 'elysia'; + +/** + * 生成验证码请求Schema + */ +export const GenerateCaptchaSchema = t.Object({ + type: t.Optional(t.Union([ + t.Literal('image'), + t.Literal('sms'), + t.Literal('email') + ], { + description: '验证码类型', + examples: ['image', 'sms', 'email'], + default: 'image' + })), + width: t.Optional(t.Number({ + minimum: 100, + maximum: 400, + description: '验证码图片宽度', + examples: [200], + default: 200 + })), + height: t.Optional(t.Number({ + minimum: 40, + maximum: 100, + description: '验证码图片高度', + examples: [60], + default: 60 + })), + length: t.Optional(t.Number({ + minimum: 4, + maximum: 8, + description: '验证码长度', + examples: [4], + default: 4 + })), + expireTime: t.Optional(t.Number({ + minimum: 60, + maximum: 1800, + description: '验证码过期时间(秒)', + examples: [300], + default: 300 + })) +}); + +/** + * 验证验证码请求Schema + */ +export const VerifyCaptchaSchema = t.Object({ + captchaId: t.String({ + minLength: 1, + description: '验证码ID', + examples: ['captcha_1234567890'] + }), + captchaCode: t.String({ + minLength: 4, + maxLength: 8, + description: '用户输入的验证码', + examples: ['1234'] + }), + scene: t.Optional(t.String({ + description: '验证场景', + examples: ['login', 'register', 'reset_password'] + })) +}); + +/** + * 验证码数据Schema + */ +export const CaptchaDataSchema = t.Object({ + id: t.String({ description: '验证码ID' }), + code: t.String({ description: '验证码内容' }), + type: t.String({ description: '验证码类型' }), + image: t.Optional(t.String({ description: 'Base64图片数据' })), + expireTime: t.Number({ description: '过期时间戳' }), + scene: t.Optional(t.String({ description: '验证场景' })), + createdAt: t.Number({ description: '创建时间戳' }) +}); + +/** + * 验证码生成响应Schema + */ +export const CaptchaGenerateResponseSchema = t.Object({ + id: t.String({ description: '验证码ID' }), + image: t.String({ description: 'Base64编码的验证码图片' }), + expireTime: t.Number({ description: '过期时间戳' }), + type: t.String({ description: '验证码类型' }) +}); + +// 导出TypeScript类型 +export type GenerateCaptchaRequest = Static; +export type VerifyCaptchaRequest = Static; +export type CaptchaData = Static; +export type CaptchaGenerateResponse = Static; \ No newline at end of file diff --git a/src/modules/captcha/captcha.service.ts b/src/modules/captcha/captcha.service.ts new file mode 100644 index 0000000..7bfd16e --- /dev/null +++ b/src/modules/captcha/captcha.service.ts @@ -0,0 +1,260 @@ +/** + * @file 验证码服务实现 + * @author AI助手 + * @date 2024-12-27 + * @description 验证码生成、验证和存储的业务逻辑 + */ + +import { randomBytes, randomInt } from 'crypto'; +import { createCanvas } from 'canvas'; +import type { + GenerateCaptchaRequest, + VerifyCaptchaRequest, + CaptchaData, + CaptchaGenerateResponse +} from './captcha.schema'; +import type { + GenerateCaptchaSuccessResponse, + VerifyCaptchaSuccessResponse +} from './captcha.response'; +import { Logger } from '@/plugins/logger/logger.service'; +import { redisService } from '@/plugins/redis/redis.service'; +import { ERROR_CODES } from '@/constants/error-codes'; +import { successResponse } from '@/utils/response.helper'; + +export class CaptchaService { + /** + * 生成验证码 + * @param request 生成验证码请求参数 + * @returns Promise + */ + async generateCaptcha(request: GenerateCaptchaRequest): Promise { + try { + Logger.info(`生成验证码请求:${JSON.stringify(request)}`); + + const { + type = 'image', + width = 200, + height = 60, + length = 4, + expireTime = 300 + } = request; + + // 生成验证码ID + const captchaId = `captcha_${randomBytes(16).toString('hex')}`; + + // 生成验证码内容 + const code = this.generateRandomCode(length); + + // 计算过期时间 + const expireTimestamp = Date.now() + (expireTime * 1000); + + let imageData: string | undefined; + + if (type === 'image') { + // 生成图形验证码 + imageData = await this.generateImageCaptcha(code, width, height); + } + + // 构建验证码数据 + const captchaData: CaptchaData = { + id: captchaId, + code: code.toLowerCase(), // 存储时转为小写,验证时忽略大小写 + type, + image: imageData, + expireTime: expireTimestamp, + createdAt: Date.now() + }; + + // 存储到Redis + const redisKey = `captcha:${captchaId}`; + await redisService.setex(redisKey, expireTime, JSON.stringify(captchaData)); + + Logger.info(`验证码生成成功:${captchaId}`); + + // 构建响应数据 + const responseData: CaptchaGenerateResponse = { + id: captchaId, + image: imageData || '', + expireTime: expireTimestamp, + type + }; + + return successResponse(responseData, '验证码生成成功'); + } catch (error) { + Logger.error(new Error(`生成验证码失败:${error}`)); + throw error; + } + } + + /** + * 验证验证码 + * @param request 验证验证码请求参数 + * @returns Promise + */ + async verifyCaptcha(request: VerifyCaptchaRequest): Promise { + try { + Logger.info(`验证验证码请求:${JSON.stringify(request)}`); + + const { captchaId, captchaCode, scene } = request; + + // 从Redis获取验证码数据 + const redisKey = `captcha:${captchaId}`; + const captchaDataStr = await redisService.get(redisKey); + + if (!captchaDataStr) { + Logger.warn(`验证码不存在:${captchaId}`); + return successResponse( + { valid: false, message: '验证码不存在或已过期' }, + '验证失败' + ); + } + + const captchaData: CaptchaData = JSON.parse(captchaDataStr); + + // 检查是否过期 + if (Date.now() > captchaData.expireTime) { + Logger.warn(`验证码已过期:${captchaId}`); + // 删除过期的验证码 + await redisService.del(redisKey); + return successResponse( + { valid: false, message: '验证码已过期' }, + '验证失败' + ); + } + + // 验证验证码内容(忽略大小写) + const isValid = captchaData.code.toLowerCase() === captchaCode.toLowerCase(); + + if (isValid) { + // 验证成功后删除验证码,防止重复使用 + await redisService.del(redisKey); + Logger.info(`验证码验证成功:${captchaId}`); + return successResponse( + { valid: true, message: '验证码验证成功' }, + '验证成功' + ); + } else { + Logger.warn(`验证码错误:${captchaId},输入:${captchaCode},正确:${captchaData.code}`); + return successResponse( + { valid: false, message: '验证码错误' }, + '验证失败' + ); + } + } catch (error) { + Logger.error(new Error(`验证验证码失败:${error}`)); + throw error; + } + } + + /** + * 生成随机验证码 + * @param length 验证码长度 + * @returns string 随机验证码 + */ + private generateRandomCode(length: number): string { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + let result = ''; + for (let i = 0; i < length; i++) { + result += chars.charAt(randomInt(chars.length)); + } + return result; + } + + /** + * 生成图形验证码 + * @param code 验证码内容 + * @param width 图片宽度 + * @param height 图片高度 + * @returns Promise Base64编码的图片数据 + */ + private async generateImageCaptcha(code: string, width: number, height: number): Promise { + return new Promise((resolve, reject) => { + try { + const canvas = createCanvas(width, height); + const ctx = canvas.getContext('2d'); + + // 设置背景色 + ctx.fillStyle = '#f0f0f0'; + ctx.fillRect(0, 0, width, height); + + // 添加干扰线 + for (let i = 0; i < 3; i++) { + ctx.strokeStyle = `rgb(${randomInt(100, 200)}, ${randomInt(100, 200)}, ${randomInt(100, 200)})`; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(randomInt(width), randomInt(height)); + ctx.lineTo(randomInt(width), randomInt(height)); + ctx.stroke(); + } + + // 添加干扰点 + for (let i = 0; i < 50; i++) { + ctx.fillStyle = `rgb(${randomInt(100, 200)}, ${randomInt(100, 200)}, ${randomInt(100, 200)})`; + ctx.fillRect(randomInt(width), randomInt(height), 1, 1); + } + + // 绘制验证码文字 + const fontSize = Math.min(width / code.length, height * 0.6); + ctx.font = `bold ${fontSize}px Arial`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + const charWidth = width / code.length; + for (let i = 0; i < code.length; i++) { + const x = charWidth * i + charWidth / 2; + const y = height / 2 + randomInt(-5, 5); + + // 随机颜色 + ctx.fillStyle = `rgb(${randomInt(0, 100)}, ${randomInt(0, 100)}, ${randomInt(0, 100)})`; + + // 随机旋转 + ctx.save(); + ctx.translate(x, y); + ctx.rotate((randomInt(-15, 15) * Math.PI) / 180); + ctx.fillText(code[i]!, 0, 0); + ctx.restore(); + } + + // 转换为Base64 + const buffer = canvas.toBuffer('image/png'); + const base64 = buffer.toString('base64'); + resolve(`data:image/png;base64,${base64}`); + } catch (error) { + reject(error); + } + }); + } + + /** + * 清理过期验证码 + * @returns Promise 清理的验证码数量 + */ + async cleanupExpiredCaptchas(): Promise { + try { + const pattern = 'captcha:*'; + const keys = await redisService.keys(pattern); + let cleanedCount = 0; + + for (const key of keys) { + const captchaDataStr = await redisService.get(key); + if (captchaDataStr) { + const captchaData: CaptchaData = JSON.parse(captchaDataStr); + if (Date.now() > captchaData.expireTime) { + await redisService.del(key); + cleanedCount++; + } + } + } + + Logger.info(`清理过期验证码完成,共清理 ${cleanedCount} 个`); + return cleanedCount; + } catch (error) { + Logger.error(new Error(`清理过期验证码失败:${error}`)); + throw error; + } + } +} + +// 导出单例实例 +export const captchaService = new CaptchaService(); \ No newline at end of file diff --git a/src/modules/captcha/captcha.test.ts b/src/modules/captcha/captcha.test.ts new file mode 100644 index 0000000..045aa9c --- /dev/null +++ b/src/modules/captcha/captcha.test.ts @@ -0,0 +1,190 @@ +/** + * @file 验证码模块测试用例 + * @author AI助手 + * @date 2024-12-27 + * @description 验证码生成、验证和清理功能的测试 + */ + +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; +import { app } from '@/app'; +import { redisService } from '@/plugins/redis/redis.service'; +import type { GenerateCaptchaRequest, VerifyCaptchaRequest } from './captcha.schema'; + +describe('Captcha API', () => { + let captchaId: string; + let captchaCode: string; + + beforeAll(async () => { + // 初始化Redis服务 + try { + await redisService.initialize(); + } catch (error) { + console.warn('Redis初始化失败,跳过Redis相关测试:', error); + } + }); + + afterAll(async () => { + // 关闭Redis连接 + try { + await redisService.close(); + } catch (error) { + console.warn('Redis关闭失败:', error); + } + }); + + beforeEach(async () => { + // 每个测试前重置状态 + captchaId = ''; + captchaCode = ''; + }); + + describe('POST /api/captcha/generate', () => { + it('应该成功生成图形验证码', async () => { + const payload: GenerateCaptchaRequest = { + type: 'image', + width: 200, + height: 60, + length: 4, + expireTime: 300 + }; + + const response = await app + .handle(new Request('http://localhost/api/captcha/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(200); + const result = await response.json() as any; + expect(result.code).toBe('SUCCESS'); + expect(result.data.id).toBeDefined(); + expect(result.data.image).toBeDefined(); + expect(result.data.type).toBe('image'); + expect(result.data.expireTime).toBeGreaterThan(Date.now()); + + // 保存验证码信息供后续测试使用 + captchaId = result.data.id; + captchaCode = 'TEST'; // 模拟验证码 + }); + + it('应该使用默认参数生成验证码', async () => { + const payload = {}; + + const response = await app + .handle(new Request('http://localhost/api/captcha/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(200); + const result = await response.json() as any; + expect(result.code).toBe('SUCCESS'); + expect(result.data.type).toBe('image'); + expect(result.data.image).toMatch(/^data:image\/png;base64,/); + }); + + it('应该验证参数范围限制', async () => { + const payload: GenerateCaptchaRequest = { + width: 50, // 小于最小值100 + height: 20, // 小于最小值40 + length: 2, // 小于最小值4 + expireTime: 30 // 小于最小值60 + }; + + const response = await app + .handle(new Request('http://localhost/api/captcha/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + }); + + describe('POST /api/captcha/verify', () => { + it('应该验证验证码不存在的情况', async () => { + const payload: VerifyCaptchaRequest = { + captchaId: 'nonexistent_captcha_id', + captchaCode: '1234', + scene: 'login' + }; + + const response = await app + .handle(new Request('http://localhost/api/captcha/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(200); + const result = await response.json() as any; + expect(result.code).toBe('SUCCESS'); + expect(result.data.valid).toBe(false); + expect(result.data.message).toContain('验证码不存在或已过期'); + }); + + it('应该验证参数格式', async () => { + const payload = { + captchaId: '', // 空字符串 + captchaCode: '123', // 长度小于4 + }; + + const response = await app + .handle(new Request('http://localhost/api/captcha/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + })); + + expect(response.status).toBe(400); + }); + }); + + describe('POST /api/captcha/cleanup', () => { + it('应该成功清理过期验证码', async () => { + const response = await app + .handle(new Request('http://localhost/api/captcha/cleanup', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + })); + + expect(response.status).toBe(200); + const result = await response.json() as any; + expect(result.code).toBe('SUCCESS'); + expect(result.data.cleanedCount).toBeGreaterThanOrEqual(0); + }); + }); + + describe('验证码服务功能测试', () => { + it('应该生成指定长度的随机验证码', async () => { + const { captchaService } = await import('./captcha.service'); + + // 测试不同长度的验证码 + const lengths = [4, 6, 8]; + for (const length of lengths) { + const code = (captchaService as any).generateRandomCode(length); + expect(code).toHaveLength(length); + expect(code).toMatch(/^[A-Z0-9]+$/); + } + }); + + it('应该生成Base64格式的图片数据', async () => { + const { captchaService } = await import('./captcha.service'); + + const imageData = await (captchaService as any).generateImageCaptcha('TEST', 200, 60); + expect(imageData).toMatch(/^data:image\/png;base64,/); + expect(imageData.length).toBeGreaterThan(100); // 确保有实际的图片数据 + }); + }); + + describe('验证码安全性测试', () => { + it('应该忽略验证码大小写', async () => { + // 这个测试需要实际的验证码,这里只是验证逻辑 + // 实际实现中,验证码存储时转为小写,验证时也转为小写比较 + expect('ABC'.toLowerCase()).toBe('abc'.toLowerCase()); + }); + }); +}); \ No newline at end of file diff --git a/src/modules/example/example.service.ts b/src/modules/example/example.service.ts index b8ebc5d..f9d316a 100644 --- a/src/modules/example/example.service.ts +++ b/src/modules/example/example.service.ts @@ -22,7 +22,7 @@ import { eq } from 'drizzle-orm'; import { db } from '@/plugins/drizzle/drizzle.service'; -import { users } from '@/eneities/users'; +import { sysUsers as users } from '@/eneities'; import { ERROR_CODES } from '@/validators/global.response'; import { type GetUserByUsernameType } from './example.schema'; import type { JwtUserType } from '@/type/jwt.type'; diff --git a/src/modules/index.ts b/src/modules/index.ts index c9c55f0..18b787d 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -12,6 +12,7 @@ import { healthController } from './health/health.controller'; import { userController } from './user/user.controller'; import { testController } from './test/test.controller'; import { exampleController } from './example/example.controller'; +import { captchaController } from './captcha/captcha.controller'; /** * 主路由控制器 - API 路由总入口 @@ -32,4 +33,6 @@ export const controllers = new Elysia({ // 健康检查接口 .group('/health', (app) => app.use(healthController)) // 样例接口 - .group('/example', (app) => app.use(exampleController)); + .group('/example', (app) => app.use(exampleController)) + // 验证码接口 + .group('/captcha', (app) => app.use(captchaController)); diff --git a/src/modules/tags.ts b/src/modules/tags.ts index 66afb13..7b6512e 100644 --- a/src/modules/tags.ts +++ b/src/modules/tags.ts @@ -28,6 +28,8 @@ export const tags = { system: 'System', /** 权限管理接口 */ permission: 'Permission', + /** 验证码相关接口 */ + captcha: 'Captcha', } as const; /** diff --git a/src/plugins/drizzle/drizzle.service.ts b/src/plugins/drizzle/drizzle.service.ts index 400b506..08f0cd9 100644 --- a/src/plugins/drizzle/drizzle.service.ts +++ b/src/plugins/drizzle/drizzle.service.ts @@ -40,14 +40,8 @@ export class DrizzleService { connectionLimit: Number(process.env.DB_CONNECTION_LIMIT) || 10, /** 队列限制 */ queueLimit: Number(process.env.DB_QUEUE_LIMIT) || 0, - /** 连接超时 */ - acquireTimeout: Number(process.env.DB_ACQUIRE_TIMEOUT) || 60000, - /** 空闲超时 */ - timeout: Number(process.env.DB_TIMEOUT) || 60000, - /** 重连配置 */ - reconnect: true, - /** 最大重连次数 */ - maxReconnects: 3, + /** 等待连接 */ + waitForConnections: true, }; /** @@ -271,9 +265,8 @@ export class DrizzleService { */ public getPoolStats(): { connectionLimit: number; - acquireTimeout: number; - timeout: number; queueLimit: number; + waitForConnections: boolean; } | null { if (!this._connectionPool) { return null; @@ -281,9 +274,8 @@ export class DrizzleService { return { connectionLimit: this._poolConfig.connectionLimit, - acquireTimeout: this._poolConfig.acquireTimeout, - timeout: this._poolConfig.timeout, queueLimit: this._poolConfig.queueLimit, + waitForConnections: this._poolConfig.waitForConnections, }; } diff --git a/src/plugins/email/email.service.ts b/src/plugins/email/email.service.ts index d885f53..eb70dcf 100644 --- a/src/plugins/email/email.service.ts +++ b/src/plugins/email/email.service.ts @@ -131,22 +131,6 @@ export class EmailService { this.validateConfig(); this.updateStatus('unhealthy', 'disconnected'); - console.log({ - host: smtpConfig.host, - port: smtpConfig.port, - secure: smtpConfig.secure, - auth: { - user: smtpConfig.auth.user, - pass: smtpConfig.auth.pass, - }, - connectionTimeout: smtpConfig.connectionTimeout, - greetingTimeout: smtpConfig.greetingTimeout, - socketTimeout: smtpConfig.socketTimeout, - pool: true, // 使用连接池 - maxConnections: 5, // 最大连接数 - maxMessages: 100, // 每个连接最大消息数 - }) - // 创建邮件传输器 this._transporter = nodemailer.createTransport({ host: smtpConfig.host, diff --git a/src/plugins/errorHandle/errorHandler.plugins.ts b/src/plugins/errorHandle/errorHandler.plugins.ts index 8793d0f..80d5883 100644 --- a/src/plugins/errorHandle/errorHandler.plugins.ts +++ b/src/plugins/errorHandle/errorHandler.plugins.ts @@ -8,12 +8,13 @@ */ import { Elysia } from 'elysia'; +import { Logger } from '@/plugins/logger/logger.service'; /** * 全局错误处理插件 */ export const errorHandlerPlugin = (app: Elysia) => - app.onError(({ error, set, code, log, env }) => { + app.onError(({ error, set, code, env }) => { switch (code) { case 'VALIDATION': { set.status = 400; @@ -43,7 +44,7 @@ export const errorHandlerPlugin = (app: Elysia) => } case 'INTERNAL_SERVER_ERROR': { set.status = 500; - log.error(error); + Logger.error(error); return { code: 500, message: '服务器内部错误', @@ -71,7 +72,7 @@ export const errorHandlerPlugin = (app: Elysia) => console.log('error', error); set.status = 500; - log.error(error); + Logger.error(error); return { code: 500, message: '服务器内部错误', diff --git a/src/plugins/logger/logger.plugins.ts b/src/plugins/logger/logger.plugins.ts index ba97079..5ea1cc3 100644 --- a/src/plugins/logger/logger.plugins.ts +++ b/src/plugins/logger/logger.plugins.ts @@ -10,7 +10,7 @@ import { Elysia } from 'elysia'; import { browserInfo } from '@/utils/deviceInfo'; import { getRandomBackgroundColor } from '@/utils/randomChalk'; -import Logger, { getResponseSize } from '@/plugins/logger/logger.service'; +import { Logger, getResponseSize } from '@/plugins/logger/logger.service'; /** * HTTP请求日志插件 diff --git a/src/plugins/redis/redis.service.ts b/src/plugins/redis/redis.service.ts index 64eb279..8393964 100644 --- a/src/plugins/redis/redis.service.ts +++ b/src/plugins/redis/redis.service.ts @@ -243,6 +243,88 @@ export class RedisService { // 重新初始化连接 return await this.initialize(); } + + /** + * 设置键值对 + */ + public async set(key: string, value: string): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + await this._client.set(key, value); + } + + /** + * 设置键值对并设置过期时间 + */ + public async setex(key: string, seconds: number, value: string): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + await this._client.setEx(key, seconds, value); + } + + /** + * 获取键值 + */ + public async get(key: string): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + return await this._client.get(key); + } + + /** + * 删除键 + */ + public async del(key: string): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + return await this._client.del(key); + } + + /** + * 获取匹配的键列表 + */ + public async keys(pattern: string): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + return await this._client.keys(pattern); + } + + /** + * 检查键是否存在 + */ + public async exists(key: string): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + const result = await this._client.exists(key); + return result === 1; + } + + /** + * 设置键的过期时间 + */ + public async expire(key: string, seconds: number): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + const result = await this._client.expire(key, seconds); + return result === 1; + } + + /** + * 获取键的剩余过期时间 + */ + public async ttl(key: string): Promise { + if (!this._client) { + throw new Error('Redis未初始化'); + } + return await this._client.ttl(key); + } } /** diff --git a/src/validators/global.response.ts b/src/validators/global.response.ts index f5f71cc..4297023 100644 --- a/src/validators/global.response.ts +++ b/src/validators/global.response.ts @@ -1,259 +1,284 @@ -/** - * @file 全局响应Schema定义 - * @author hotok - * @date 2025-06-28 - * @lastEditor hotok - * @lastEditTime 2025-06-28 - * @description 定义全局通用的响应结构、错误码说明,供Swagger文档和接口验证使用 - */ - -import { t } from 'elysia'; - -/** - * 全局错误码定义 - * @description 系统错误码说明,便于前端开发和API文档查阅 - */ -export const ERROR_CODES = { - /** 成功 */ - SUCCESS: 0, - /** 通用业务错误 */ - BUSINESS_ERROR: 400, - /** 认证失败 */ - UNAUTHORIZED: 401, - /** 权限不足 */ - FORBIDDEN: 403, - /** 资源未找到 */ - NOT_FOUND: 404, - /** 参数验证失败 */ - VALIDATION_ERROR: 422, - /** 服务器内部错误 */ - INTERNAL_ERROR: 500, - /** 服务不可用 */ - SERVICE_UNAVAILABLE: 503, -} as const; - -/** - * 错误码说明映射 - */ -export const ERROR_CODE_DESCRIPTIONS = { - [ERROR_CODES.SUCCESS]: '操作成功', - [ERROR_CODES.BUSINESS_ERROR]: '业务逻辑错误', - [ERROR_CODES.UNAUTHORIZED]: '身份认证失败,请重新登录', - [ERROR_CODES.FORBIDDEN]: '权限不足,无法访问该资源', - [ERROR_CODES.NOT_FOUND]: '请求的资源不存在', - [ERROR_CODES.VALIDATION_ERROR]: '请求参数验证失败', - [ERROR_CODES.INTERNAL_ERROR]: '服务器内部错误,请稍后重试', - [ERROR_CODES.SERVICE_UNAVAILABLE]: '服务暂时不可用,请稍后重试', -} as const; - -/** - * 基础响应结构Schema - */ -export const BaseResponseSchema = t.Object({ - /** 响应码:0表示成功,其他表示错误 */ - code: t.Number({ - description: '响应码,0表示成功,其他表示错误', - examples: [0, 400, 401, 403, 404, 422, 500, 503], - }), - /** 响应消息 */ - message: t.String({ - description: '响应消息,描述操作结果', - examples: ['操作成功', '参数验证失败', '权限不足'], - }), - /** 响应数据 */ - data: t.Any({ - description: '响应数据,成功时包含具体数据,失败时通常为null', - }), -}); - -/** - * 成功响应Schema - */ -export const SuccessResponseSchema = t.Object({ - code: t.Literal(0, { - description: '成功响应码', - }), - message: t.String({ - description: '成功消息', - examples: ['操作成功', '获取数据成功', '创建成功'], - }), - data: t.Any({ - description: '成功时返回的数据', - }), -}); - -/** - * 错误响应Schema - */ -export const ErrorResponseSchema = t.Object({ - code: t.Number({ - description: '错误响应码', - examples: [400, 401, 403, 404, 422, 500, 503], - }), - message: t.String({ - description: '错误消息', - examples: ['参数验证失败', '认证失败', '权限不足', '资源不存在', '服务器内部错误'], - }), - data: t.Null({ - description: '错误时数据字段为null', - }), -}); - -/** - * 分页响应Schema - */ -export const PaginationResponseSchema = t.Object({ - code: t.Literal(0), - message: t.String(), - data: t.Object({ - /** 分页数据列表 */ - list: t.Array(t.Any(), { - description: '数据列表', - }), - /** 分页信息 */ - pagination: t.Object({ - /** 当前页码 */ - page: t.Number({ - description: '当前页码,从1开始', - minimum: 1, - examples: [1, 2, 3], - }), - /** 每页条数 */ - pageSize: t.Number({ - description: '每页条数', - minimum: 1, - maximum: 100, - examples: [10, 20, 50], - }), - /** 总条数 */ - total: t.Number({ - description: '总条数', - minimum: 0, - examples: [0, 100, 1500], - }), - /** 总页数 */ - totalPages: t.Number({ - description: '总页数', - minimum: 0, - examples: [0, 5, 75], - }), - /** 是否有下一页 */ - hasNext: t.Boolean({ - description: '是否有下一页', - }), - /** 是否有上一页 */ - hasPrev: t.Boolean({ - description: '是否有上一页', - }), - }), - }), -}); - -/** - * 常用HTTP状态码响应模板 - */ -export const CommonResponses = { - /** 200 成功 */ - 200: SuccessResponseSchema, - /** 400 业务错误 */ - 400: ErrorResponseSchema, - /** 401 认证失败 */ - 401: t.Object({ - code: t.Literal(401), - message: t.String({ - examples: ['身份认证失败,请重新登录', 'Token已过期', 'Token格式错误'], - }), - data: t.Null(), - }), - /** 403 权限不足 */ - 403: t.Object({ - code: t.Literal(403), - message: t.String({ - examples: ['权限不足,无法访问该资源', '用户角色权限不够'], - }), - data: t.Null(), - }), - /** 404 资源未找到 */ - 404: t.Object({ - code: t.Literal(404), - message: t.String({ - examples: ['请求的资源不存在', '用户不存在', '文件未找到'], - }), - data: t.Null(), - }), - /** 422 参数验证失败 */ - 422: t.Object({ - code: t.Literal(422), - message: t.String({ - examples: ['请求参数验证失败', '邮箱格式不正确', '密码长度不符合要求'], - }), - data: t.Null(), - }), - /** 500 服务器内部错误 */ - 500: t.Object({ - code: t.Literal(500), - message: t.String({ - examples: ['服务器内部错误,请稍后重试', '数据库连接失败', '系统异常'], - }), - data: t.Null(), - }), - /** 503 服务不可用 */ - 503: t.Object({ - code: t.Literal(503), - message: t.String({ - examples: ['服务暂时不可用,请稍后重试', '系统维护中', '依赖服务异常'], - }), - data: t.Null(), - }), -}; - -/** - * 健康检查响应Schema - */ -export const HealthCheckResponseSchema = t.Object({ - code: t.Number(), - message: t.String(), - data: t.Object({ - status: t.Union([ - t.Literal('healthy'), - t.Literal('unhealthy'), - t.Literal('degraded'), - ], { - description: '系统健康状态:healthy-健康,unhealthy-不健康,degraded-降级', - }), - timestamp: t.String({ - description: 'ISO时间戳', - examples: ['2024-06-28T12:00:00.000Z'], - }), - uptime: t.Number({ - description: '系统运行时间(秒)', - examples: [3600, 86400], - }), - responseTime: t.Number({ - description: '响应时间(毫秒)', - examples: [15, 50, 100], - }), - version: t.String({ - description: '系统版本', - examples: ['1.0.0', '1.2.3'], - }), - environment: t.String({ - description: '运行环境', - examples: ['development', 'production', 'test'], - }), - components: t.Object({ - mysql: t.Optional(t.Object({ - status: t.String(), - responseTime: t.Optional(t.Number()), - error: t.Optional(t.String()), - details: t.Optional(t.Any()), - })), - redis: t.Optional(t.Object({ - status: t.String(), - responseTime: t.Optional(t.Number()), - error: t.Optional(t.String()), - details: t.Optional(t.Any()), - })), - }), - }), +/** + * @file 全局响应Schema定义 + * @author hotok + * @date 2025-06-28 + * @lastEditor hotok + * @lastEditTime 2025-06-28 + * @description 定义全局通用的响应结构、错误码说明,供Swagger文档和接口验证使用 + */ + +import { t } from 'elysia'; + +/** + * 全局错误码定义 + * @description 系统错误码说明,便于前端开发和API文档查阅 + */ +export const ERROR_CODES = { + /** 成功 */ + SUCCESS: 0, + /** 通用业务错误 */ + BUSINESS_ERROR: 400, + /** 认证失败 */ + UNAUTHORIZED: 401, + /** 权限不足 */ + FORBIDDEN: 403, + /** 资源未找到 */ + NOT_FOUND: 404, + /** 参数验证失败 */ + VALIDATION_ERROR: 422, + /** 服务器内部错误 */ + INTERNAL_ERROR: 500, + /** 服务不可用 */ + SERVICE_UNAVAILABLE: 503, +} as const; + +/** + * 错误码说明映射 + */ +export const ERROR_CODE_DESCRIPTIONS = { + [ERROR_CODES.SUCCESS]: '操作成功', + [ERROR_CODES.BUSINESS_ERROR]: '业务逻辑错误', + [ERROR_CODES.UNAUTHORIZED]: '身份认证失败,请重新登录', + [ERROR_CODES.FORBIDDEN]: '权限不足,无法访问该资源', + [ERROR_CODES.NOT_FOUND]: '请求的资源不存在', + [ERROR_CODES.VALIDATION_ERROR]: '请求参数验证失败', + [ERROR_CODES.INTERNAL_ERROR]: '服务器内部错误,请稍后重试', + [ERROR_CODES.SERVICE_UNAVAILABLE]: '服务暂时不可用,请稍后重试', +} as const; + +/** + * 基础响应结构Schema + */ +export const BaseResponseSchema = t.Object({ + /** 响应码:0表示成功,其他表示错误 */ + code: t.Number({ + description: '响应码,0表示成功,其他表示错误', + examples: [0, 400, 401, 403, 404, 422, 500, 503], + }), + /** 响应消息 */ + message: t.String({ + description: '响应消息,描述操作结果', + examples: ['操作成功', '参数验证失败', '权限不足'], + }), + /** 响应数据 */ + data: t.Any({ + description: '响应数据,成功时包含具体数据,失败时通常为null', + }), +}); + +/** + * 成功响应Schema + */ +export const SuccessResponseSchema = t.Object({ + code: t.Literal(0, { + description: '成功响应码', + }), + message: t.String({ + description: '成功消息', + examples: ['操作成功', '获取数据成功', '创建成功'], + }), + data: t.Any({ + description: '成功时返回的数据', + }), +}); + +/** + * 错误响应Schema + */ +export const ErrorResponseSchema = t.Object({ + code: t.Number({ + description: '错误响应码', + examples: [400, 401, 403, 404, 422, 500, 503], + }), + message: t.String({ + description: '错误消息', + examples: ['参数验证失败', '认证失败', '权限不足', '资源不存在', '服务器内部错误'], + }), + data: t.Null({ + description: '错误时数据字段为null', + }), +}); + +/** + * 分页响应Schema + */ +export const PaginationResponseSchema = t.Object({ + code: t.Literal(0), + message: t.String(), + data: t.Object({ + /** 分页数据列表 */ + list: t.Array(t.Any(), { + description: '数据列表', + }), + /** 分页信息 */ + pagination: t.Object({ + /** 当前页码 */ + page: t.Number({ + description: '当前页码,从1开始', + minimum: 1, + examples: [1, 2, 3], + }), + /** 每页条数 */ + pageSize: t.Number({ + description: '每页条数', + minimum: 1, + maximum: 100, + examples: [10, 20, 50], + }), + /** 总条数 */ + total: t.Number({ + description: '总条数', + minimum: 0, + examples: [0, 100, 1500], + }), + /** 总页数 */ + totalPages: t.Number({ + description: '总页数', + minimum: 0, + examples: [0, 5, 75], + }), + /** 是否有下一页 */ + hasNext: t.Boolean({ + description: '是否有下一页', + }), + /** 是否有上一页 */ + hasPrev: t.Boolean({ + description: '是否有上一页', + }), + }), + }), +}); + +/** + * 常用HTTP状态码响应模板 + */ +export const CommonResponses = { + /** 200 成功 */ + 200: SuccessResponseSchema, + /** 400 业务错误 */ + 400: ErrorResponseSchema, + /** 401 认证失败 */ + 401: t.Object({ + code: t.Literal(401), + message: t.String({ + examples: ['身份认证失败,请重新登录', 'Token已过期', 'Token格式错误'], + }), + data: t.Null(), + }), + /** 403 权限不足 */ + 403: t.Object({ + code: t.Literal(403), + message: t.String({ + examples: ['权限不足,无法访问该资源', '用户角色权限不够'], + }), + data: t.Null(), + }), + /** 404 资源未找到 */ + 404: t.Object({ + code: t.Literal(404), + message: t.String({ + examples: ['请求的资源不存在', '用户不存在', '文件未找到'], + }), + data: t.Null(), + }), + /** 422 参数验证失败 */ + 422: t.Object({ + code: t.Literal(422), + message: t.String({ + examples: ['请求参数验证失败', '邮箱格式不正确', '密码长度不符合要求'], + }), + data: t.Null(), + }), + /** 500 服务器内部错误 */ + 500: t.Object({ + code: t.Literal(500), + message: t.String({ + examples: ['服务器内部错误,请稍后重试', '数据库连接失败', '系统异常'], + }), + data: t.Null(), + }), + /** 503 服务不可用 */ + 503: t.Object({ + code: t.Literal(503), + message: t.String({ + examples: ['服务暂时不可用,请稍后重试', '系统维护中', '依赖服务异常'], + }), + data: t.Null(), + }), +}; + +/** + * 全局响应包装器Schema + * @param dataSchema 数据Schema + * @returns 包装后的响应Schema + */ +export const globalResponseWrapperSchema = (dataSchema: any) => + t.Object({ + code: t.Union([ + t.Literal('SUCCESS'), + t.Literal('VALIDATION_ERROR'), + t.Literal('UNAUTHORIZED'), + t.Literal('FORBIDDEN'), + t.Literal('NOT_FOUND'), + t.Literal('BUSINESS_ERROR'), + t.Literal('INTERNAL_ERROR'), + ], { + description: '响应状态码', + }), + message: t.String({ + description: '响应消息', + examples: ['操作成功', '获取数据成功', '创建成功'], + }), + data: dataSchema, + }); + +/** + * 健康检查响应Schema + */ +export const HealthCheckResponseSchema = t.Object({ + code: t.Number(), + message: t.String(), + data: t.Object({ + status: t.Union([ + t.Literal('healthy'), + t.Literal('unhealthy'), + t.Literal('degraded'), + ], { + description: '系统健康状态:healthy-健康,unhealthy-不健康,degraded-降级', + }), + timestamp: t.String({ + description: 'ISO时间戳', + examples: ['2024-06-28T12:00:00.000Z'], + }), + uptime: t.Number({ + description: '系统运行时间(秒)', + examples: [3600, 86400], + }), + responseTime: t.Number({ + description: '响应时间(毫秒)', + examples: [15, 50, 100], + }), + version: t.String({ + description: '系统版本', + examples: ['1.0.0', '1.2.3'], + }), + environment: t.String({ + description: '运行环境', + examples: ['development', 'production', 'test'], + }), + components: t.Object({ + mysql: t.Optional(t.Object({ + status: t.String(), + responseTime: t.Optional(t.Number()), + error: t.Optional(t.String()), + details: t.Optional(t.Any()), + })), + redis: t.Optional(t.Object({ + status: t.String(), + responseTime: t.Optional(t.Number()), + error: t.Optional(t.String()), + details: t.Optional(t.Any()), + })), + }), + }), }); \ No newline at end of file diff --git a/tasks/M2-基础用户系统-开发任务计划.md b/tasks/M2-基础用户系统-开发任务计划.md index fcded13..6a07e5e 100644 --- a/tasks/M2-基础用户系统-开发任务计划.md +++ b/tasks/M2-基础用户系统-开发任务计划.md @@ -57,7 +57,7 @@ - SMTP配置、邮件模板 - [x] **集成验证码服务** `(1h)` - - 图形验证码生成、验证 + - [x] 图形验证码生成、验证 ### 🔐 用户注册功能 (第4天) **预估工时**:8小时