diff --git a/config.toml b/config.toml index 75b5ceb..1e9ef33 100644 --- a/config.toml +++ b/config.toml @@ -44,6 +44,7 @@ debug = true host = "127.0.0.1" port = 8000 reload = true +access_log = false [development.logging] level = "DEBUG" @@ -65,6 +66,7 @@ host = "0.0.0.0" port = 8000 reload = false workers = 4 +access_log = false [production.logging] level = "INFO" diff --git a/public/config.toml b/public/config.toml index 9d5dd44..ecc1069 100644 --- a/public/config.toml +++ b/public/config.toml @@ -44,6 +44,7 @@ debug = true host = "127.0.0.1" port = 8001 reload = true +access_log = false [development.logging] level = "DEBUG" @@ -65,6 +66,7 @@ host = "0.0.0.0" port = 8000 reload = false workers = 4 +access_log = false [production.logging] level = "INFO" diff --git a/public/main.py b/public/main.py index e7cb0db..d3dff49 100644 --- a/public/main.py +++ b/public/main.py @@ -54,7 +54,7 @@ def app_run(app_config: dict, default_config: dict): reload=reload, workers=workers, log_level="debug" if app_config.get("debug", False) else "info", - access_log=True, + access_log=False, timeout_keep_alive=timeout_keep_alive, ) diff --git a/src/chongming-web/package-lock.json b/src/chongming-web/package-lock.json index 87ac0ad..5787a1a 100644 --- a/src/chongming-web/package-lock.json +++ b/src/chongming-web/package-lock.json @@ -8,17 +8,19 @@ "name": "chongming-web", "version": "0.0.0", "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "element-plus": "^2.13.7", "pinia": "^3.0.4", "vue": "^3.5.31", "vue-router": "^5.0.4" }, "devDependencies": { + "@hey-api/openapi-ts": "^0.96.1", "@tsconfig/node24": "^24.0.4", "@types/node": "^24.12.0", "@vitejs/plugin-vue": "^6.0.5", "@vue/tsconfig": "^0.9.1", "npm-run-all2": "^8.0.4", - "openapi-typescript-codegen": "^0.29.0", "prettier": "3.8.1", "typescript": "~6.0.0", "vite": "^8.0.3", @@ -29,24 +31,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.9.3", - "resolved": "https://registry.npmmirror.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.9.3.tgz", - "integrity": "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jsdevtools/ono": "^7.1.3", - "@types/json-schema": "^7.0.15", - "js-yaml": "^4.1.0" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/philsturgeon" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -501,6 +485,24 @@ "node": ">=6.9.0" } }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/@emnapi/core": { "version": "1.9.2", "resolved": "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz", @@ -535,6 +537,202 @@ "tslib": "^2.4.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hey-api/codegen-core": { + "version": "0.8.0", + "resolved": "https://registry.npmmirror.com/@hey-api/codegen-core/-/codegen-core-0.8.0.tgz", + "integrity": "sha512-OuF/jenX9wz7AWHRBfb37v+jLkrfCt0FJXQuALNH2UsW6+bdZBmoibHl0K778SiHwneotJbAaEvX2S05wEqUQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.4", + "ansi-colors": "4.1.3", + "c12": "3.3.4", + "color-support": "1.1.3" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.4.1.tgz", + "integrity": "sha512-DoPJGxVApDlktP1yYLjmOrF0YBEqb32ieCbx1S1i09n8TyCgdoh4yQaQ3kp0sMTauH+bwNKPsFh7S8qiWCoKZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "7.1.3", + "@types/json-schema": "7.0.15", + "yaml": "2.8.3" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/openapi-ts": { + "version": "0.96.1", + "resolved": "https://registry.npmmirror.com/@hey-api/openapi-ts/-/openapi-ts-0.96.1.tgz", + "integrity": "sha512-EnH+6ncaVC1yNvYWcsO7MoeBSqCvYZaKvRDRqh+S7dsfb9lhe2mKUR2+7sDacGUBm7VDZZ7hg4q4egxlpZaf0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.8.0", + "@hey-api/json-schema-ref-parser": "1.4.1", + "@hey-api/shared": "0.4.1", + "@hey-api/spec-types": "0.2.0", + "@hey-api/types": "0.1.4", + "ansi-colors": "4.1.3", + "color-support": "1.1.3", + "commander": "14.0.3", + "get-tsconfig": "4.14.0" + }, + "bin": { + "openapi-ts": "bin/run.js" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3 || >=6.0.0 || 6.0.1-rc" + } + }, + "node_modules/@hey-api/openapi-ts/node_modules/commander": { + "version": "14.0.3", + "resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/@hey-api/shared": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/@hey-api/shared/-/shared-0.4.1.tgz", + "integrity": "sha512-EDAjvGpEamn2fOB7W87ZStyDSv5mEpnQCciVbiZ+Ll4EfawwIjHMMtDtRQOYol0YjTkAeEm274GFPAtg3xfPGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/codegen-core": "0.8.0", + "@hey-api/json-schema-ref-parser": "1.4.1", + "@hey-api/spec-types": "0.2.0", + "@hey-api/types": "0.1.4", + "ansi-colors": "4.1.3", + "cross-spawn": "7.0.6", + "open": "11.0.0", + "semver": "7.7.4" + }, + "engines": { + "node": ">=22.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/shared/node_modules/open": { + "version": "11.0.0", + "resolved": "https://registry.npmmirror.com/open/-/open-11.0.0.tgz", + "integrity": "sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.4.0", + "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", + "is-inside-container": "^1.0.0", + "powershell-utils": "^0.1.0", + "wsl-utils": "^0.3.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hey-api/shared/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@hey-api/shared/node_modules/wsl-utils": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.3.1.tgz", + "integrity": "sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0", + "powershell-utils": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@hey-api/spec-types": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/@hey-api/spec-types/-/spec-types-0.2.0.tgz", + "integrity": "sha512-ibQ8Is7evMavzr8GNyJCcTg975d8DpaMUyLmOrQ85UBdy1l6t1KuRAwgChAbesJsIlNV6gjmlXruWyegDX18Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hey-api/types": "0.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + } + }, + "node_modules/@hey-api/types": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/@hey-api/types/-/types-0.1.4.tgz", + "integrity": "sha512-thWfawrDIP7wSI9ioT13I5soaaqB5vAPIiZmgD8PbeEVKNrkonc0N/Sjj97ezl7oQgusZmaNphGdMKipPO6IBg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -623,6 +821,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.8", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.8.tgz", + "integrity": "sha512-wOwESXvvED3S8xBmcPWHs2dUuzrE4XiZeFu7e1hROIJkm02a49N120pmOXxY33sBb6hArItm5W5tcg1cBtV+HQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.15", "resolved": "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", @@ -912,6 +1121,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "24.12.2", "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz", @@ -922,6 +1146,12 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-vue": { "version": "6.0.6", "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz", @@ -1257,6 +1487,42 @@ } } }, + "node_modules/@vueuse/core": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz", + "integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "12.0.0", + "@vueuse/shared": "12.0.0", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz", + "integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz", + "integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==", + "license": "MIT", + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", @@ -1276,6 +1542,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/ansi-styles": { "version": "6.2.3", "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", @@ -1299,13 +1575,6 @@ "node": ">=14" } }, - "node_modules/argparse": { - "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/ast-kit": { "version": "2.2.0", "resolved": "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz", @@ -1338,6 +1607,12 @@ "url": "https://github.com/sponsors/sxzz" } }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.10.18", "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz", @@ -1410,19 +1685,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmmirror.com/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "node_modules/c12": { + "version": "3.3.4", + "resolved": "https://registry.npmmirror.com/c12/-/c12-3.3.4.tgz", + "integrity": "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "chokidar": "^5.0.0", + "confbox": "^0.2.4", + "defu": "^6.1.6", + "dotenv": "^17.3.1", + "exsolve": "^1.0.8", + "giget": "^3.2.0", + "jiti": "^2.6.1", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^2.1.0", + "pkg-types": "^2.3.0", + "rc9": "^3.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "magicast": "*" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } } }, + "node_modules/c12/node_modules/perfect-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz", + "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "dev": true, + "license": "MIT" + }, "node_modules/caniuse-lite": { "version": "1.0.30001788", "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", @@ -1459,14 +1757,14 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "license": "ISC", + "bin": { + "color-support": "bin.js" } }, "node_modules/confbox": { @@ -1541,6 +1839,12 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", @@ -1602,6 +1906,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmmirror.com/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", @@ -1612,6 +1930,19 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.336", "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", @@ -1619,6 +1950,32 @@ "dev": true, "license": "ISC" }, + "node_modules/element-plus": { + "version": "2.13.7", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.7.tgz", + "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.2.0", + "@element-plus/icons-vue": "^2.3.2", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.17.20", + "@types/lodash-es": "^4.17.12", + "@vueuse/core": "12.0.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.19", + "lodash": "^4.17.23", + "lodash-es": "^4.17.23", + "lodash-unified": "^1.0.3", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0", + "vue-component-type-helpers": "^3.2.4" + }, + "peerDependencies": { + "vue": "^3.3.0" + } + }, "node_modules/entities": { "version": "7.0.1", "resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", @@ -1680,21 +2037,6 @@ } } }, - "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", @@ -1720,33 +2062,27 @@ "node": ">=6.9.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/handlebars": { - "version": "4.7.9", - "resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.9.tgz", - "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmmirror.com/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", - "neo-async": "^2.6.2", - "source-map": "^0.6.1", - "wordwrap": "^1.0.0" + "resolve-pkg-maps": "^1.0.0" }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/giget": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/giget/-/giget-3.2.0.tgz", + "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "dev": true, + "license": "MIT", "bin": { - "handlebars": "bin/handlebars" - }, - "engines": { - "node": ">=0.4.7" - }, - "optionalDependencies": { - "uglify-js": "^3.1.4" + "giget": "dist/cli.mjs" } }, "node_modules/hookable": { @@ -1771,6 +2107,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -1828,6 +2177,16 @@ "node": ">=18" } }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -1835,19 +2194,6 @@ "dev": true, "license": "MIT" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", @@ -1882,19 +2228,6 @@ "node": ">=6" } }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmmirror.com/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmmirror.com/kolorist/-/kolorist-1.8.0.tgz", @@ -2180,6 +2513,29 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2214,6 +2570,12 @@ "url": "https://github.com/sponsors/sxzz" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/memorystream": { "version": "0.3.1", "resolved": "https://registry.npmmirror.com/memorystream/-/memorystream-0.3.1.tgz", @@ -2223,16 +2585,6 @@ "node": ">= 0.10.0" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/mitt": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", @@ -2309,13 +2661,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true, - "license": "MIT" - }, "node_modules/node-releases": { "version": "2.0.37", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz", @@ -2323,6 +2668,12 @@ "dev": true, "license": "MIT" }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, "node_modules/npm-normalize-package-bin": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", @@ -2386,23 +2737,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/openapi-typescript-codegen": { - "version": "0.29.0", - "resolved": "https://registry.npmmirror.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz", - "integrity": "sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@apidevtools/json-schema-ref-parser": "^11.5.4", - "camelcase": "^6.3.0", - "commander": "^12.0.0", - "fs-extra": "^11.2.0", - "handlebars": "^4.7.8" - }, - "bin": { - "openapi": "bin/index.js" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", @@ -2523,6 +2857,19 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/powershell-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/powershell-utils/-/powershell-utils-0.1.0.tgz", + "integrity": "sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prettier": { "version": "3.8.1", "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.8.1.tgz", @@ -2555,6 +2902,17 @@ ], "license": "MIT" }, + "node_modules/rc9": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/rc9/-/rc9-3.0.1.tgz", + "integrity": "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.6", + "destr": "^2.0.5" + } + }, "node_modules/read-package-json-fast": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/read-package-json-fast/-/read-package-json-fast-4.0.0.tgz", @@ -2582,6 +2940,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", @@ -2709,16 +3077,6 @@ "node": ">=18" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", @@ -2803,20 +3161,6 @@ "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", "license": "MIT" }, - "node_modules/uglify-js": { - "version": "3.19.3", - "resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz", - "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", - "dev": true, - "license": "BSD-2-Clause", - "optional": true, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", @@ -2824,16 +3168,6 @@ "dev": true, "license": "MIT" }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/unplugin": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-3.0.0.tgz", @@ -3132,6 +3466,12 @@ } } }, + "node_modules/vue-component-type-helpers": { + "version": "3.2.7", + "resolved": "https://registry.npmmirror.com/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz", + "integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==", + "license": "MIT" + }, "node_modules/vue-router": { "version": "5.0.4", "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-5.0.4.tgz", @@ -3249,13 +3589,6 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true, - "license": "MIT" - }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/wsl-utils/-/wsl-utils-0.1.0.tgz", diff --git a/src/chongming-web/package.json b/src/chongming-web/package.json index 032212a..71bea70 100644 --- a/src/chongming-web/package.json +++ b/src/chongming-web/package.json @@ -10,20 +10,22 @@ "build-only": "vite build", "type-check": "vue-tsc --build", "format": "prettier --write --experimental-cli src/", - "openapi": "npx openapi-typescript-codegen --input http://127.0.0.1:8000/openapi.json --output ./src/api/generated/" + "openapi": "npx @hey-api/openapi-ts --input http://127.0.0.1:8000/openapi.json --output ./src/api/generated/" }, "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "element-plus": "^2.13.7", "pinia": "^3.0.4", "vue": "^3.5.31", "vue-router": "^5.0.4" }, "devDependencies": { + "@hey-api/openapi-ts": "^0.96.1", "@tsconfig/node24": "^24.0.4", "@types/node": "^24.12.0", "@vitejs/plugin-vue": "^6.0.5", "@vue/tsconfig": "^0.9.1", "npm-run-all2": "^8.0.4", - "openapi-typescript-codegen": "^0.29.0", "prettier": "3.8.1", "typescript": "~6.0.0", "vite": "^8.0.3", @@ -33,4 +35,4 @@ "engines": { "node": "^20.19.0 || >=22.12.0" } -} \ No newline at end of file +} diff --git a/src/chongming-web/src/App.vue b/src/chongming-web/src/App.vue index 38e183b..5e07a41 100644 --- a/src/chongming-web/src/App.vue +++ b/src/chongming-web/src/App.vue @@ -1,111 +1,16 @@ + - + \ No newline at end of file diff --git a/src/chongming-web/src/api/generated/client.gen.ts b/src/chongming-web/src/api/generated/client.gen.ts new file mode 100644 index 0000000..67d2030 --- /dev/null +++ b/src/chongming-web/src/api/generated/client.gen.ts @@ -0,0 +1,16 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type ClientOptions, type Config, createClient, createConfig } from './client'; +import type { ClientOptions as ClientOptions2 } from './types.gen'; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = (override?: Config) => Config & T>; + +export const client = createClient(createConfig({ baseUrl: 'http://127.0.0.1:8000' })); diff --git a/src/chongming-web/src/api/generated/client/client.gen.ts b/src/chongming-web/src/api/generated/client/client.gen.ts new file mode 100644 index 0000000..9ec9ad8 --- /dev/null +++ b/src/chongming-web/src/api/generated/client/client.gen.ts @@ -0,0 +1,298 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { createSseClient } from '../core/serverSentEvents.gen'; +import type { HttpMethod } from '../core/types.gen'; +import { getValidRequestBody } from '../core/utils.gen'; +import type { Client, Config, RequestOptions, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors(); + + const beforeRequest = async < + TData = unknown, + TResponseStyle extends 'data' | 'fields' = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, + >( + options: RequestOptions, + ) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined as string | undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body !== undefined && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body) as string | undefined; + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const resolvedOpts = opts as typeof opts & + ResolvedRequestOptions; + const url = buildUrl(resolvedOpts); + + return { opts: resolvedOpts, url }; + }; + + const request: Client['request'] = async (options) => { + const { opts, url } = await beforeRequest(options); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: getValidRequestBody(opts), + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response: Response; + + try { + response = await _fetch(request); + } catch (error) { + // Handle fetch exceptions (AbortError, network errors, etc.) + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, undefined as any, request, opts)) as unknown; + } + } + + finalError = finalError || ({} as unknown); + + if (opts.throwOnError) { + throw finalError; + } + + // Return error response + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + request, + response: undefined as any, + }; + } + + for (const fn of interceptors.response.fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + if (response.status === 204 || response.headers.get('Content-Length') === '0') { + let emptyData: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'text': + emptyData = await response[parseAs](); + break; + case 'formData': + emptyData = new FormData(); + break; + case 'stream': + emptyData = response.body; + break; + case 'json': + default: + emptyData = {}; + break; + } + return opts.responseStyle === 'data' + ? emptyData + : { + data: emptyData, + ...result, + }; + } + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'text': + data = await response[parseAs](); + break; + case 'json': { + // Some servers return 200 with no Content-Length and empty body. + // response.json() would throw; read as text and parse if non-empty. + const text = await response.text(); + data = text ? JSON.parse(text) : {}; + break; + } + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error.fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + const makeMethodFn = (method: Uppercase) => (options: RequestOptions) => + request({ ...options, method }); + + const makeSseFn = (method: Uppercase) => async (options: RequestOptions) => { + const { opts, url } = await beforeRequest(options); + return createSseClient({ + ...opts, + body: opts.body as BodyInit | null | undefined, + headers: opts.headers as unknown as Record, + method, + onRequest: async (url, init) => { + let request = new Request(url, init); + for (const fn of interceptors.request.fns) { + if (fn) { + request = await fn(request, opts); + } + } + return request; + }, + serializedBody: getValidRequestBody(opts) as BodyInit | null | undefined, + url, + }); + }; + + const _buildUrl: Client['buildUrl'] = (options) => buildUrl({ ..._config, ...options }); + + return { + buildUrl: _buildUrl, + connect: makeMethodFn('CONNECT'), + delete: makeMethodFn('DELETE'), + get: makeMethodFn('GET'), + getConfig, + head: makeMethodFn('HEAD'), + interceptors, + options: makeMethodFn('OPTIONS'), + patch: makeMethodFn('PATCH'), + post: makeMethodFn('POST'), + put: makeMethodFn('PUT'), + request, + setConfig, + sse: { + connect: makeSseFn('CONNECT'), + delete: makeSseFn('DELETE'), + get: makeSseFn('GET'), + head: makeSseFn('HEAD'), + options: makeSseFn('OPTIONS'), + patch: makeSseFn('PATCH'), + post: makeSseFn('POST'), + put: makeSseFn('PUT'), + trace: makeSseFn('TRACE'), + }, + trace: makeMethodFn('TRACE'), + } as Client; +}; diff --git a/src/chongming-web/src/api/generated/client/index.ts b/src/chongming-web/src/api/generated/client/index.ts new file mode 100644 index 0000000..b295ede --- /dev/null +++ b/src/chongming-web/src/api/generated/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { serializeQueryKeyValue } from '../core/queryKeySerializer.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/src/chongming-web/src/api/generated/client/types.gen.ts b/src/chongming-web/src/api/generated/client/types.gen.ts new file mode 100644 index 0000000..c80695b --- /dev/null +++ b/src/chongming-web/src/api/generated/client/types.gen.ts @@ -0,0 +1,215 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + ServerSentEventsOptions, + ServerSentEventsResult, +} from '../core/serverSentEvents.gen'; +import type { Client as CoreClient, Config as CoreConfig } from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: 'arrayBuffer' | 'auto' | 'blob' | 'formData' | 'json' | 'stream' | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TData = unknown, + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> + extends + Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }>, + Pick< + ServerSentEventsOptions, + | 'onRequest' + | 'onSseError' + | 'onSseEvent' + | 'sseDefaultRetryDelay' + | 'sseMaxRetryAttempts' + | 'sseMaxRetryDelay' + > { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + headers: Headers; + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record ? TData[keyof TData] : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? (TData extends Record ? TData[keyof TData] : TData) | undefined + : ( + | { + data: TData extends Record ? TData[keyof TData] : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record ? TError[keyof TError] : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type SseFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => Promise>; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: TData & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponse = unknown, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + ([TData] extends [never] ? unknown : Omit); diff --git a/src/chongming-web/src/api/generated/client/utils.gen.ts b/src/chongming-web/src/api/generated/client/utils.gen.ts new file mode 100644 index 0000000..5162192 --- /dev/null +++ b/src/chongming-web/src/api/generated/client/utils.gen.ts @@ -0,0 +1,316 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import { getUrl } from '../core/utils.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +export const createQuerySerializer = ({ + parameters = {}, + ...args +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + const options = parameters[name] || args; + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'form', + value, + ...options.array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved: options.allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...options.object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved: options.allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = (contentType: string | null): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if (cleanContent.startsWith('application/json') || cleanContent.endsWith('+json')) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => cleanContent.startsWith(type)) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => + getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +const headersEntries = (headers: Headers): Array<[string, string]> => { + const entries: Array<[string, string]> = []; + headers.forEach((value, key) => { + entries.push([key, value]); + }); + return entries; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header) { + continue; + } + + const iterator = header instanceof Headers ? headersEntries(header) : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e., their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = (request: Req, options: Options) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + fns: Array = []; + + clear(): void { + this.fns = []; + } + + eject(id: number | Interceptor): void { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = null; + } + } + + exists(id: number | Interceptor): boolean { + const index = this.getInterceptorIndex(id); + return Boolean(this.fns[index]); + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this.fns[id] ? id : -1; + } + return this.fns.indexOf(id); + } + + update(id: number | Interceptor, fn: Interceptor): number | Interceptor | false { + const index = this.getInterceptorIndex(id); + if (this.fns[index]) { + this.fns[index] = fn; + return id; + } + return false; + } + + use(fn: Interceptor): number { + this.fns.push(fn); + return this.fns.length - 1; + } +} + +export interface Middleware { + error: Interceptors>; + request: Interceptors>; + response: Interceptors>; +} + +export const createInterceptors = (): Middleware< + Req, + Res, + Err, + Options +> => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/src/chongming-web/src/api/generated/core/ApiError.ts b/src/chongming-web/src/api/generated/core/ApiError.ts deleted file mode 100644 index ec7b16a..0000000 --- a/src/chongming-web/src/api/generated/core/ApiError.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; - -export class ApiError extends Error { - public readonly url: string; - public readonly status: number; - public readonly statusText: string; - public readonly body: any; - public readonly request: ApiRequestOptions; - - constructor(request: ApiRequestOptions, response: ApiResult, message: string) { - super(message); - - this.name = 'ApiError'; - this.url = response.url; - this.status = response.status; - this.statusText = response.statusText; - this.body = response.body; - this.request = request; - } -} diff --git a/src/chongming-web/src/api/generated/core/ApiRequestOptions.ts b/src/chongming-web/src/api/generated/core/ApiRequestOptions.ts deleted file mode 100644 index 93143c3..0000000 --- a/src/chongming-web/src/api/generated/core/ApiRequestOptions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiRequestOptions = { - readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; - readonly url: string; - readonly path?: Record; - readonly cookies?: Record; - readonly headers?: Record; - readonly query?: Record; - readonly formData?: Record; - readonly body?: any; - readonly mediaType?: string; - readonly responseHeader?: string; - readonly errors?: Record; -}; diff --git a/src/chongming-web/src/api/generated/core/ApiResult.ts b/src/chongming-web/src/api/generated/core/ApiResult.ts deleted file mode 100644 index ee1126e..0000000 --- a/src/chongming-web/src/api/generated/core/ApiResult.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ApiResult = { - readonly url: string; - readonly ok: boolean; - readonly status: number; - readonly statusText: string; - readonly body: any; -}; diff --git a/src/chongming-web/src/api/generated/core/CancelablePromise.ts b/src/chongming-web/src/api/generated/core/CancelablePromise.ts deleted file mode 100644 index d70de92..0000000 --- a/src/chongming-web/src/api/generated/core/CancelablePromise.ts +++ /dev/null @@ -1,131 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export class CancelError extends Error { - - constructor(message: string) { - super(message); - this.name = 'CancelError'; - } - - public get isCancelled(): boolean { - return true; - } -} - -export interface OnCancel { - readonly isResolved: boolean; - readonly isRejected: boolean; - readonly isCancelled: boolean; - - (cancelHandler: () => void): void; -} - -export class CancelablePromise implements Promise { - #isResolved: boolean; - #isRejected: boolean; - #isCancelled: boolean; - readonly #cancelHandlers: (() => void)[]; - readonly #promise: Promise; - #resolve?: (value: T | PromiseLike) => void; - #reject?: (reason?: any) => void; - - constructor( - executor: ( - resolve: (value: T | PromiseLike) => void, - reject: (reason?: any) => void, - onCancel: OnCancel - ) => void - ) { - this.#isResolved = false; - this.#isRejected = false; - this.#isCancelled = false; - this.#cancelHandlers = []; - this.#promise = new Promise((resolve, reject) => { - this.#resolve = resolve; - this.#reject = reject; - - const onResolve = (value: T | PromiseLike): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isResolved = true; - if (this.#resolve) this.#resolve(value); - }; - - const onReject = (reason?: any): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isRejected = true; - if (this.#reject) this.#reject(reason); - }; - - const onCancel = (cancelHandler: () => void): void => { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#cancelHandlers.push(cancelHandler); - }; - - Object.defineProperty(onCancel, 'isResolved', { - get: (): boolean => this.#isResolved, - }); - - Object.defineProperty(onCancel, 'isRejected', { - get: (): boolean => this.#isRejected, - }); - - Object.defineProperty(onCancel, 'isCancelled', { - get: (): boolean => this.#isCancelled, - }); - - return executor(onResolve, onReject, onCancel as OnCancel); - }); - } - - get [Symbol.toStringTag]() { - return "Cancellable Promise"; - } - - public then( - onFulfilled?: ((value: T) => TResult1 | PromiseLike) | null, - onRejected?: ((reason: any) => TResult2 | PromiseLike) | null - ): Promise { - return this.#promise.then(onFulfilled, onRejected); - } - - public catch( - onRejected?: ((reason: any) => TResult | PromiseLike) | null - ): Promise { - return this.#promise.catch(onRejected); - } - - public finally(onFinally?: (() => void) | null): Promise { - return this.#promise.finally(onFinally); - } - - public cancel(): void { - if (this.#isResolved || this.#isRejected || this.#isCancelled) { - return; - } - this.#isCancelled = true; - if (this.#cancelHandlers.length) { - try { - for (const cancelHandler of this.#cancelHandlers) { - cancelHandler(); - } - } catch (error) { - console.warn('Cancellation threw an error', error); - return; - } - } - this.#cancelHandlers.length = 0; - if (this.#reject) this.#reject(new CancelError('Request aborted')); - } - - public get isCancelled(): boolean { - return this.#isCancelled; - } -} diff --git a/src/chongming-web/src/api/generated/core/OpenAPI.ts b/src/chongming-web/src/api/generated/core/OpenAPI.ts deleted file mode 100644 index 414d47e..0000000 --- a/src/chongming-web/src/api/generated/core/OpenAPI.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ApiRequestOptions } from './ApiRequestOptions'; - -type Resolver = (options: ApiRequestOptions) => Promise; -type Headers = Record; - -export type OpenAPIConfig = { - BASE: string; - VERSION: string; - WITH_CREDENTIALS: boolean; - CREDENTIALS: 'include' | 'omit' | 'same-origin'; - TOKEN?: string | Resolver | undefined; - USERNAME?: string | Resolver | undefined; - PASSWORD?: string | Resolver | undefined; - HEADERS?: Headers | Resolver | undefined; - ENCODE_PATH?: ((path: string) => string) | undefined; -}; - -export const OpenAPI: OpenAPIConfig = { - BASE: '', - VERSION: 'chongming', - WITH_CREDENTIALS: false, - CREDENTIALS: 'include', - TOKEN: undefined, - USERNAME: undefined, - PASSWORD: undefined, - HEADERS: undefined, - ENCODE_PATH: undefined, -}; diff --git a/src/chongming-web/src/api/generated/core/auth.gen.ts b/src/chongming-web/src/api/generated/core/auth.gen.ts new file mode 100644 index 0000000..3ebf994 --- /dev/null +++ b/src/chongming-web/src/api/generated/core/auth.gen.ts @@ -0,0 +1,41 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/src/chongming-web/src/api/generated/core/bodySerializer.gen.ts b/src/chongming-web/src/api/generated/core/bodySerializer.gen.ts new file mode 100644 index 0000000..67daca6 --- /dev/null +++ b/src/chongming-web/src/api/generated/core/bodySerializer.gen.ts @@ -0,0 +1,82 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { ArrayStyle, ObjectStyle, SerializerOptions } from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: unknown) => unknown; + +type QuerySerializerOptionsObject = { + allowReserved?: boolean; + array?: Partial>; + object?: Partial>; +}; + +export type QuerySerializerOptions = QuerySerializerOptionsObject & { + /** + * Per-parameter serialization overrides. When provided, these settings + * override the global array/object settings for specific parameter names. + */ + parameters?: Record; +}; + +const serializeFormDataPair = (data: FormData, key: string, value: unknown): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = (data: URLSearchParams, key: string, value: unknown): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: (body: unknown): FormData => { + const data = new FormData(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: unknown): string => + JSON.stringify(body, (_key, value) => (typeof value === 'bigint' ? value.toString() : value)), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: (body: unknown): string => { + const data = new URLSearchParams(); + + Object.entries(body as Record).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/src/chongming-web/src/api/generated/core/params.gen.ts b/src/chongming-web/src/api/generated/core/params.gen.ts new file mode 100644 index 0000000..7955601 --- /dev/null +++ b/src/chongming-web/src/api/generated/core/params.gen.ts @@ -0,0 +1,169 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + } + | { + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If `in` is omitted, `map` aliases `key` to the transport layer. + */ + map: Slot; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + | { + in: Slot; + map?: string; + } + | { + in?: never; + map: Slot; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if ('key' in config) { + map.set(config.key, { + map: config.map, + }); + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Array.isArray(value) && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = (args: ReadonlyArray, fields: FieldsConfig) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + if (field.in) { + (params[field.in] as Record)[name] = arg; + } + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + if (field.in) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + params[field.map] = value; + } + } else { + const extra = extraPrefixes.find(([prefix]) => key.startsWith(prefix)); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[key.slice(prefix.length)] = value; + } else if ('allowExtra' in config && config.allowExtra) { + for (const [slot, allowed] of Object.entries(config.allowExtra)) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/src/chongming-web/src/api/generated/core/pathSerializer.gen.ts b/src/chongming-web/src/api/generated/core/pathSerializer.gen.ts new file mode 100644 index 0000000..994b284 --- /dev/null +++ b/src/chongming-web/src/api/generated/core/pathSerializer.gen.ts @@ -0,0 +1,171 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions extends SerializePrimitiveOptions, SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [...values, key, allowReserved ? (v as string) : encodeURIComponent(v as string)]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' ? separator + joinedValues : joinedValues; +}; diff --git a/src/chongming-web/src/api/generated/core/queryKeySerializer.gen.ts b/src/chongming-web/src/api/generated/core/queryKeySerializer.gen.ts new file mode 100644 index 0000000..5000df6 --- /dev/null +++ b/src/chongming-web/src/api/generated/core/queryKeySerializer.gen.ts @@ -0,0 +1,117 @@ +// This file is auto-generated by @hey-api/openapi-ts + +/** + * JSON-friendly union that mirrors what Pinia Colada can hash. + */ +export type JsonValue = + | null + | string + | number + | boolean + | JsonValue[] + | { [key: string]: JsonValue }; + +/** + * Replacer that converts non-JSON values (bigint, Date, etc.) to safe substitutes. + */ +export const queryKeyJsonReplacer = (_key: string, value: unknown) => { + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + if (typeof value === 'bigint') { + return value.toString(); + } + if (value instanceof Date) { + return value.toISOString(); + } + return value; +}; + +/** + * Safely stringifies a value and parses it back into a JsonValue. + */ +export const stringifyToJsonValue = (input: unknown): JsonValue | undefined => { + try { + const json = JSON.stringify(input, queryKeyJsonReplacer); + if (json === undefined) { + return undefined; + } + return JSON.parse(json) as JsonValue; + } catch { + return undefined; + } +}; + +/** + * Detects plain objects (including objects with a null prototype). + */ +const isPlainObject = (value: unknown): value is Record => { + if (value === null || typeof value !== 'object') { + return false; + } + const prototype = Object.getPrototypeOf(value as object); + return prototype === Object.prototype || prototype === null; +}; + +/** + * Turns URLSearchParams into a sorted JSON object for deterministic keys. + */ +const serializeSearchParams = (params: URLSearchParams): JsonValue => { + const entries = Array.from(params.entries()).sort(([a], [b]) => a.localeCompare(b)); + const result: Record = {}; + + for (const [key, value] of entries) { + const existing = result[key]; + if (existing === undefined) { + result[key] = value; + continue; + } + + if (Array.isArray(existing)) { + (existing as string[]).push(value); + } else { + result[key] = [existing, value]; + } + } + + return result; +}; + +/** + * Normalizes any accepted value into a JSON-friendly shape for query keys. + */ +export const serializeQueryKeyValue = (value: unknown): JsonValue | undefined => { + if (value === null) { + return null; + } + + if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + return value; + } + + if (value === undefined || typeof value === 'function' || typeof value === 'symbol') { + return undefined; + } + + if (typeof value === 'bigint') { + return value.toString(); + } + + if (value instanceof Date) { + return value.toISOString(); + } + + if (Array.isArray(value)) { + return stringifyToJsonValue(value); + } + + if (typeof URLSearchParams !== 'undefined' && value instanceof URLSearchParams) { + return serializeSearchParams(value); + } + + if (isPlainObject(value)) { + return stringifyToJsonValue(value); + } + + return undefined; +}; diff --git a/src/chongming-web/src/api/generated/core/request.ts b/src/chongming-web/src/api/generated/core/request.ts deleted file mode 100644 index f83d711..0000000 --- a/src/chongming-web/src/api/generated/core/request.ts +++ /dev/null @@ -1,322 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import { ApiError } from './ApiError'; -import type { ApiRequestOptions } from './ApiRequestOptions'; -import type { ApiResult } from './ApiResult'; -import { CancelablePromise } from './CancelablePromise'; -import type { OnCancel } from './CancelablePromise'; -import type { OpenAPIConfig } from './OpenAPI'; - -export const isDefined = (value: T | null | undefined): value is Exclude => { - return value !== undefined && value !== null; -}; - -export const isString = (value: any): value is string => { - return typeof value === 'string'; -}; - -export const isStringWithValue = (value: any): value is string => { - return isString(value) && value !== ''; -}; - -export const isBlob = (value: any): value is Blob => { - return ( - typeof value === 'object' && - typeof value.type === 'string' && - typeof value.stream === 'function' && - typeof value.arrayBuffer === 'function' && - typeof value.constructor === 'function' && - typeof value.constructor.name === 'string' && - /^(Blob|File)$/.test(value.constructor.name) && - /^(Blob|File)$/.test(value[Symbol.toStringTag]) - ); -}; - -export const isFormData = (value: any): value is FormData => { - return value instanceof FormData; -}; - -export const base64 = (str: string): string => { - try { - return btoa(str); - } catch (err) { - // @ts-ignore - return Buffer.from(str).toString('base64'); - } -}; - -export const getQueryString = (params: Record): string => { - const qs: string[] = []; - - const append = (key: string, value: any) => { - qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); - }; - - const process = (key: string, value: any) => { - if (isDefined(value)) { - if (Array.isArray(value)) { - value.forEach(v => { - process(key, v); - }); - } else if (typeof value === 'object') { - Object.entries(value).forEach(([k, v]) => { - process(`${key}[${k}]`, v); - }); - } else { - append(key, value); - } - } - }; - - Object.entries(params).forEach(([key, value]) => { - process(key, value); - }); - - if (qs.length > 0) { - return `?${qs.join('&')}`; - } - - return ''; -}; - -const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { - const encoder = config.ENCODE_PATH || encodeURI; - - const path = options.url - .replace('{api-version}', config.VERSION) - .replace(/{(.*?)}/g, (substring: string, group: string) => { - if (options.path?.hasOwnProperty(group)) { - return encoder(String(options.path[group])); - } - return substring; - }); - - const url = `${config.BASE}${path}`; - if (options.query) { - return `${url}${getQueryString(options.query)}`; - } - return url; -}; - -export const getFormData = (options: ApiRequestOptions): FormData | undefined => { - if (options.formData) { - const formData = new FormData(); - - const process = (key: string, value: any) => { - if (isString(value) || isBlob(value)) { - formData.append(key, value); - } else { - formData.append(key, JSON.stringify(value)); - } - }; - - Object.entries(options.formData) - .filter(([_, value]) => isDefined(value)) - .forEach(([key, value]) => { - if (Array.isArray(value)) { - value.forEach(v => process(key, v)); - } else { - process(key, value); - } - }); - - return formData; - } - return undefined; -}; - -type Resolver = (options: ApiRequestOptions) => Promise; - -export const resolve = async (options: ApiRequestOptions, resolver?: T | Resolver): Promise => { - if (typeof resolver === 'function') { - return (resolver as Resolver)(options); - } - return resolver; -}; - -export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise => { - const [token, username, password, additionalHeaders] = await Promise.all([ - resolve(options, config.TOKEN), - resolve(options, config.USERNAME), - resolve(options, config.PASSWORD), - resolve(options, config.HEADERS), - ]); - - const headers = Object.entries({ - Accept: 'application/json', - ...additionalHeaders, - ...options.headers, - }) - .filter(([_, value]) => isDefined(value)) - .reduce((headers, [key, value]) => ({ - ...headers, - [key]: String(value), - }), {} as Record); - - if (isStringWithValue(token)) { - headers['Authorization'] = `Bearer ${token}`; - } - - if (isStringWithValue(username) && isStringWithValue(password)) { - const credentials = base64(`${username}:${password}`); - headers['Authorization'] = `Basic ${credentials}`; - } - - if (options.body !== undefined) { - if (options.mediaType) { - headers['Content-Type'] = options.mediaType; - } else if (isBlob(options.body)) { - headers['Content-Type'] = options.body.type || 'application/octet-stream'; - } else if (isString(options.body)) { - headers['Content-Type'] = 'text/plain'; - } else if (!isFormData(options.body)) { - headers['Content-Type'] = 'application/json'; - } - } - - return new Headers(headers); -}; - -export const getRequestBody = (options: ApiRequestOptions): any => { - if (options.body !== undefined) { - if (options.mediaType?.includes('/json')) { - return JSON.stringify(options.body) - } else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) { - return options.body; - } else { - return JSON.stringify(options.body); - } - } - return undefined; -}; - -export const sendRequest = async ( - config: OpenAPIConfig, - options: ApiRequestOptions, - url: string, - body: any, - formData: FormData | undefined, - headers: Headers, - onCancel: OnCancel -): Promise => { - const controller = new AbortController(); - - const request: RequestInit = { - headers, - body: body ?? formData, - method: options.method, - signal: controller.signal, - }; - - if (config.WITH_CREDENTIALS) { - request.credentials = config.CREDENTIALS; - } - - onCancel(() => controller.abort()); - - return await fetch(url, request); -}; - -export const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => { - if (responseHeader) { - const content = response.headers.get(responseHeader); - if (isString(content)) { - return content; - } - } - return undefined; -}; - -export const getResponseBody = async (response: Response): Promise => { - if (response.status !== 204) { - try { - const contentType = response.headers.get('Content-Type'); - if (contentType) { - const jsonTypes = ['application/json', 'application/problem+json'] - const isJSON = jsonTypes.some(type => contentType.toLowerCase().startsWith(type)); - if (isJSON) { - return await response.json(); - } else { - return await response.text(); - } - } - } catch (error) { - console.error(error); - } - } - return undefined; -}; - -export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { - const errors: Record = { - 400: 'Bad Request', - 401: 'Unauthorized', - 403: 'Forbidden', - 404: 'Not Found', - 500: 'Internal Server Error', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - ...options.errors, - } - - const error = errors[result.status]; - if (error) { - throw new ApiError(options, result, error); - } - - if (!result.ok) { - const errorStatus = result.status ?? 'unknown'; - const errorStatusText = result.statusText ?? 'unknown'; - const errorBody = (() => { - try { - return JSON.stringify(result.body, null, 2); - } catch (e) { - return undefined; - } - })(); - - throw new ApiError(options, result, - `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` - ); - } -}; - -/** - * Request method - * @param config The OpenAPI configuration object - * @param options The request options from the service - * @returns CancelablePromise - * @throws ApiError - */ -export const request = (config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise => { - return new CancelablePromise(async (resolve, reject, onCancel) => { - try { - const url = getUrl(config, options); - const formData = getFormData(options); - const body = getRequestBody(options); - const headers = await getHeaders(config, options); - - if (!onCancel.isCancelled) { - const response = await sendRequest(config, options, url, body, formData, headers, onCancel); - const responseBody = await getResponseBody(response); - const responseHeader = getResponseHeader(response, options.responseHeader); - - const result: ApiResult = { - url, - ok: response.ok, - status: response.status, - statusText: response.statusText, - body: responseHeader ?? responseBody, - }; - - catchErrorCodes(options, result); - - resolve(result.body); - } - } catch (error) { - reject(error); - } - }); -}; diff --git a/src/chongming-web/src/api/generated/core/serverSentEvents.gen.ts b/src/chongming-web/src/api/generated/core/serverSentEvents.gen.ts new file mode 100644 index 0000000..ddf3c4d --- /dev/null +++ b/src/chongming-web/src/api/generated/core/serverSentEvents.gen.ts @@ -0,0 +1,242 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Config } from './types.gen'; + +export type ServerSentEventsOptions = Omit & + Pick & { + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: typeof fetch; + /** + * Implementing clients can call request interceptors inside this hook. + */ + onRequest?: (url: string, init: RequestInit) => Promise; + /** + * Callback invoked when a network or parsing error occurs during streaming. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param error The error that occurred. + */ + onSseError?: (error: unknown) => void; + /** + * Callback invoked when an event is streamed from the server. + * + * This option applies only if the endpoint returns a stream of events. + * + * @param event Event streamed from the server. + * @returns Nothing (void). + */ + onSseEvent?: (event: StreamEvent) => void; + serializedBody?: RequestInit['body']; + /** + * Default retry delay in milliseconds. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 3000 + */ + sseDefaultRetryDelay?: number; + /** + * Maximum number of retry attempts before giving up. + */ + sseMaxRetryAttempts?: number; + /** + * Maximum retry delay in milliseconds. + * + * Applies only when exponential backoff is used. + * + * This option applies only if the endpoint returns a stream of events. + * + * @default 30000 + */ + sseMaxRetryDelay?: number; + /** + * Optional sleep function for retry backoff. + * + * Defaults to using `setTimeout`. + */ + sseSleepFn?: (ms: number) => Promise; + url: string; + }; + +export interface StreamEvent { + data: TData; + event?: string; + id?: string; + retry?: number; +} + +export type ServerSentEventsResult = { + stream: AsyncGenerator< + TData extends Record ? TData[keyof TData] : TData, + TReturn, + TNext + >; +}; + +export function createSseClient({ + onRequest, + onSseError, + onSseEvent, + responseTransformer, + responseValidator, + sseDefaultRetryDelay, + sseMaxRetryAttempts, + sseMaxRetryDelay, + sseSleepFn, + url, + ...options +}: ServerSentEventsOptions): ServerSentEventsResult { + let lastEventId: string | undefined; + + const sleep = sseSleepFn ?? ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms))); + + const createStream = async function* () { + let retryDelay: number = sseDefaultRetryDelay ?? 3000; + let attempt = 0; + const signal = options.signal ?? new AbortController().signal; + + while (true) { + if (signal.aborted) break; + + attempt++; + + const headers = + options.headers instanceof Headers + ? options.headers + : new Headers(options.headers as Record | undefined); + + if (lastEventId !== undefined) { + headers.set('Last-Event-ID', lastEventId); + } + + try { + const requestInit: RequestInit = { + redirect: 'follow', + ...options, + body: options.serializedBody, + headers, + signal, + }; + let request = new Request(url, requestInit); + if (onRequest) { + request = await onRequest(url, requestInit); + } + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = options.fetch ?? globalThis.fetch; + const response = await _fetch(request); + + if (!response.ok) throw new Error(`SSE failed: ${response.status} ${response.statusText}`); + + if (!response.body) throw new Error('No body in SSE response'); + + const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); + + let buffer = ''; + + const abortHandler = () => { + try { + reader.cancel(); + } catch { + // noop + } + }; + + signal.addEventListener('abort', abortHandler); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buffer += value; + buffer = buffer.replace(/\r\n?/g, '\n'); // normalize line endings + + const chunks = buffer.split('\n\n'); + buffer = chunks.pop() ?? ''; + + for (const chunk of chunks) { + const lines = chunk.split('\n'); + const dataLines: Array = []; + let eventName: string | undefined; + + for (const line of lines) { + if (line.startsWith('data:')) { + dataLines.push(line.replace(/^data:\s*/, '')); + } else if (line.startsWith('event:')) { + eventName = line.replace(/^event:\s*/, ''); + } else if (line.startsWith('id:')) { + lastEventId = line.replace(/^id:\s*/, ''); + } else if (line.startsWith('retry:')) { + const parsed = Number.parseInt(line.replace(/^retry:\s*/, ''), 10); + if (!Number.isNaN(parsed)) { + retryDelay = parsed; + } + } + } + + let data: unknown; + let parsedJson = false; + + if (dataLines.length) { + const rawData = dataLines.join('\n'); + try { + data = JSON.parse(rawData); + parsedJson = true; + } catch { + data = rawData; + } + } + + if (parsedJson) { + if (responseValidator) { + await responseValidator(data); + } + + if (responseTransformer) { + data = await responseTransformer(data); + } + } + + onSseEvent?.({ + data, + event: eventName, + id: lastEventId, + retry: retryDelay, + }); + + if (dataLines.length) { + yield data as any; + } + } + } + } finally { + signal.removeEventListener('abort', abortHandler); + reader.releaseLock(); + } + + break; // exit loop on normal completion + } catch (error) { + // connection failed or aborted; retry after delay + onSseError?.(error); + + if (sseMaxRetryAttempts !== undefined && attempt >= sseMaxRetryAttempts) { + break; // stop after firing error + } + + // exponential backoff: double retry each attempt, cap at 30s + const backoff = Math.min(retryDelay * 2 ** (attempt - 1), sseMaxRetryDelay ?? 30000); + await sleep(backoff); + } + } + }; + + const stream = createStream(); + + return { stream }; +} diff --git a/src/chongming-web/src/api/generated/core/types.gen.ts b/src/chongming-web/src/api/generated/core/types.gen.ts new file mode 100644 index 0000000..9efe71d --- /dev/null +++ b/src/chongming-web/src/api/generated/core/types.gen.ts @@ -0,0 +1,104 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { BodySerializer, QuerySerializer, QuerySerializerOptions } from './bodySerializer.gen'; + +export type HttpMethod = + | 'connect' + | 'delete' + | 'get' + | 'head' + | 'options' + | 'patch' + | 'post' + | 'put' + | 'trace'; + +export type Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, + SseFn = never, +> = { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + getConfig: () => Config; + request: RequestFn; + setConfig: (config: Config) => Config; +} & { + [K in HttpMethod]: MethodFn; +} & ([SseFn] extends [never] ? { sse?: never } : { sse: { [K in HttpMethod]: SseFn } }); + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + string | number | boolean | (string | number | boolean)[] | null | undefined | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: Uppercase; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g., converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true ? never : K]: T[K]; +}; diff --git a/src/chongming-web/src/api/generated/core/utils.gen.ts b/src/chongming-web/src/api/generated/core/utils.gen.ts new file mode 100644 index 0000000..9a4fec7 --- /dev/null +++ b/src/chongming-web/src/api/generated/core/utils.gen.ts @@ -0,0 +1,140 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { BodySerializer, QuerySerializer } from './bodySerializer.gen'; +import { + type ArraySeparatorStyle, + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from './pathSerializer.gen'; + +export interface PathSerializer { + path: Record; + url: string; +} + +export const PATH_PARAM_RE = /\{[^{}]+\}/g; + +export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace(match, serializeArrayParam({ explode, name, style, value })); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export function getValidRequestBody(options: { + body?: unknown; + bodySerializer?: BodySerializer | null; + serializedBody?: unknown; +}) { + const hasBody = options.body !== undefined; + const isSerializedBody = hasBody && options.bodySerializer; + + if (isSerializedBody) { + if ('serializedBody' in options) { + const hasSerializedBody = + options.serializedBody !== undefined && options.serializedBody !== ''; + + return hasSerializedBody ? options.serializedBody : null; + } + + // not all clients implement a serializedBody property (i.e., client-axios) + return options.body !== '' ? options.body : null; + } + + // plain/text body + if (hasBody) { + return options.body; + } + + // no body was provided + return undefined; +} diff --git a/src/chongming-web/src/api/generated/index.ts b/src/chongming-web/src/api/generated/index.ts index 0747deb..b2e5c2a 100644 --- a/src/chongming-web/src/api/generated/index.ts +++ b/src/chongming-web/src/api/generated/index.ts @@ -1,24 +1,4 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export { ApiError } from './core/ApiError'; -export { CancelablePromise, CancelError } from './core/CancelablePromise'; -export { OpenAPI } from './core/OpenAPI'; -export type { OpenAPIConfig } from './core/OpenAPI'; +// This file is auto-generated by @hey-api/openapi-ts -export type { HTTPValidationError } from './models/HTTPValidationError'; -export type { RefreshTokenResponse } from './models/RefreshTokenResponse'; -export type { TaskRequest } from './models/TaskRequest'; -export type { TodoCreate } from './models/TodoCreate'; -export type { TodoRead } from './models/TodoRead'; -export type { TodoUpdate } from './models/TodoUpdate'; -export type { TokenResponse } from './models/TokenResponse'; -export type { UserCreate } from './models/UserCreate'; -export type { UserLogin } from './models/UserLogin'; -export type { UserRead } from './models/UserRead'; -export type { ValidationError } from './models/ValidationError'; - -export { AuthenticationService } from './services/AuthenticationService'; -export { DefaultService } from './services/DefaultService'; -export { TodoService } from './services/TodoService'; +export { createTodoApiV1TodosPost, deleteTodoApiV1TodosTodoIdDelete, getCurrentUserInfoApiV1AuthMeGet, getSessionsApiV1AuthSessionsGet, loginApiV1AuthLoginPost, logoutAllApiV1AuthLogoutAllPost, logoutApiV1AuthLogoutPost, type Options, readTodoApiV1TodosTodoIdGet, readTodosApiV1TodosGet, refreshTokenApiV1AuthRefreshPost, registerApiV1AuthRegisterPost, rootGet, updateTodoApiV1TodosTodoIdPatch, uploadImageApiV1UploadImagePost } from './sdk.gen'; +export type { BodyUploadImageApiV1UploadImagePost, ClientOptions, CreateTodoApiV1TodosPostData, CreateTodoApiV1TodosPostError, CreateTodoApiV1TodosPostErrors, CreateTodoApiV1TodosPostResponse, CreateTodoApiV1TodosPostResponses, DeleteTodoApiV1TodosTodoIdDeleteData, DeleteTodoApiV1TodosTodoIdDeleteError, DeleteTodoApiV1TodosTodoIdDeleteErrors, DeleteTodoApiV1TodosTodoIdDeleteResponses, Detail, GetCurrentUserInfoApiV1AuthMeGetData, GetCurrentUserInfoApiV1AuthMeGetError, GetCurrentUserInfoApiV1AuthMeGetErrors, GetCurrentUserInfoApiV1AuthMeGetResponse, GetCurrentUserInfoApiV1AuthMeGetResponses, GetSessionsApiV1AuthSessionsGetData, GetSessionsApiV1AuthSessionsGetResponses, HttpError, HttpValidationError, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostError, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponse, LoginApiV1AuthLoginPostResponses, LogoutAllApiV1AuthLogoutAllPostData, LogoutAllApiV1AuthLogoutAllPostResponse, LogoutAllApiV1AuthLogoutAllPostResponses, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponse, LogoutApiV1AuthLogoutPostResponses, ReadTodoApiV1TodosTodoIdGetData, ReadTodoApiV1TodosTodoIdGetError, ReadTodoApiV1TodosTodoIdGetErrors, ReadTodoApiV1TodosTodoIdGetResponse, ReadTodoApiV1TodosTodoIdGetResponses, ReadTodosApiV1TodosGetData, ReadTodosApiV1TodosGetError, ReadTodosApiV1TodosGetErrors, ReadTodosApiV1TodosGetResponse, ReadTodosApiV1TodosGetResponses, RefreshTokenApiV1AuthRefreshPostData, RefreshTokenApiV1AuthRefreshPostError, RefreshTokenApiV1AuthRefreshPostErrors, RefreshTokenApiV1AuthRefreshPostResponse, RefreshTokenApiV1AuthRefreshPostResponses, RefreshTokenResponse, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostError, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponse, RegisterApiV1AuthRegisterPostResponses, RootGetData, RootGetResponses, TodoCreate, TodoRead, TodoUpdate, TokenResponse, UpdateTodoApiV1TodosTodoIdPatchData, UpdateTodoApiV1TodosTodoIdPatchError, UpdateTodoApiV1TodosTodoIdPatchErrors, UpdateTodoApiV1TodosTodoIdPatchResponse, UpdateTodoApiV1TodosTodoIdPatchResponses, UploadImageApiV1UploadImagePostData, UploadImageApiV1UploadImagePostError, UploadImageApiV1UploadImagePostErrors, UploadImageApiV1UploadImagePostResponse, UploadImageApiV1UploadImagePostResponses, UploadResponse, UserCreate, UserLogin, Userlogout, UserRead, ValidationError } from './types.gen'; diff --git a/src/chongming-web/src/api/generated/models/HTTPValidationError.ts b/src/chongming-web/src/api/generated/models/HTTPValidationError.ts deleted file mode 100644 index f9b1a79..0000000 --- a/src/chongming-web/src/api/generated/models/HTTPValidationError.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { ValidationError } from './ValidationError'; -export type HTTPValidationError = { - detail?: Array; -}; - diff --git a/src/chongming-web/src/api/generated/models/RefreshTokenResponse.ts b/src/chongming-web/src/api/generated/models/RefreshTokenResponse.ts deleted file mode 100644 index 289ca28..0000000 --- a/src/chongming-web/src/api/generated/models/RefreshTokenResponse.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 刷新 Token 响应模型 - */ -export type RefreshTokenResponse = { - access_token: string; - token_type?: string; -}; - diff --git a/src/chongming-web/src/api/generated/models/TaskRequest.ts b/src/chongming-web/src/api/generated/models/TaskRequest.ts deleted file mode 100644 index 5d1a601..0000000 --- a/src/chongming-web/src/api/generated/models/TaskRequest.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TaskRequest = { - task_name: string; - interval: number; -}; - diff --git a/src/chongming-web/src/api/generated/models/TodoCreate.ts b/src/chongming-web/src/api/generated/models/TodoCreate.ts deleted file mode 100644 index 9e0630c..0000000 --- a/src/chongming-web/src/api/generated/models/TodoCreate.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TodoCreate = { - title: string; - description?: (string | null); - completed?: boolean; -}; - diff --git a/src/chongming-web/src/api/generated/models/TodoRead.ts b/src/chongming-web/src/api/generated/models/TodoRead.ts deleted file mode 100644 index 2c88da7..0000000 --- a/src/chongming-web/src/api/generated/models/TodoRead.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TodoRead = { - title: string; - description?: (string | null); - completed?: boolean; - id: number; -}; - diff --git a/src/chongming-web/src/api/generated/models/TodoUpdate.ts b/src/chongming-web/src/api/generated/models/TodoUpdate.ts deleted file mode 100644 index b6cf6c2..0000000 --- a/src/chongming-web/src/api/generated/models/TodoUpdate.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type TodoUpdate = { - title?: (string | null); - description?: (string | null); - completed?: (boolean | null); -}; - diff --git a/src/chongming-web/src/api/generated/models/TokenResponse.ts b/src/chongming-web/src/api/generated/models/TokenResponse.ts deleted file mode 100644 index 38ad91a..0000000 --- a/src/chongming-web/src/api/generated/models/TokenResponse.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * Token 响应模型 - */ -export type TokenResponse = { - access_token: string; - refresh_token: string; - token_type?: string; - expires_in: number; - refresh_expires_in: number; -}; - diff --git a/src/chongming-web/src/api/generated/models/UserCreate.ts b/src/chongming-web/src/api/generated/models/UserCreate.ts deleted file mode 100644 index 7d269db..0000000 --- a/src/chongming-web/src/api/generated/models/UserCreate.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 用户创建模型 - */ -export type UserCreate = { - email: string; - username?: (string | null); - full_name?: (string | null); - is_active?: boolean; - is_superuser?: boolean; - created_at?: (string | null); - password: string; -}; - diff --git a/src/chongming-web/src/api/generated/models/UserLogin.ts b/src/chongming-web/src/api/generated/models/UserLogin.ts deleted file mode 100644 index 717d42c..0000000 --- a/src/chongming-web/src/api/generated/models/UserLogin.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type UserLogin = { - email: string; - password: string; -}; - diff --git a/src/chongming-web/src/api/generated/models/UserRead.ts b/src/chongming-web/src/api/generated/models/UserRead.ts deleted file mode 100644 index e802a49..0000000 --- a/src/chongming-web/src/api/generated/models/UserRead.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * 用户读取模型 - */ -export type UserRead = { - email: string; - username?: (string | null); - full_name?: (string | null); - is_active?: boolean; - is_superuser?: boolean; - created_at?: (string | null); - id: number; -}; - diff --git a/src/chongming-web/src/api/generated/models/ValidationError.ts b/src/chongming-web/src/api/generated/models/ValidationError.ts deleted file mode 100644 index de2cbd7..0000000 --- a/src/chongming-web/src/api/generated/models/ValidationError.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type ValidationError = { - loc: Array<(string | number)>; - msg: string; - type: string; - input?: any; - ctx?: Record; -}; - diff --git a/src/chongming-web/src/api/generated/sdk.gen.ts b/src/chongming-web/src/api/generated/sdk.gen.ts new file mode 100644 index 0000000..863f1ed --- /dev/null +++ b/src/chongming-web/src/api/generated/sdk.gen.ts @@ -0,0 +1,166 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { type Client, formDataBodySerializer, type Options as Options2, type TDataShape } from './client'; +import { client } from './client.gen'; +import type { CreateTodoApiV1TodosPostData, CreateTodoApiV1TodosPostErrors, CreateTodoApiV1TodosPostResponses, DeleteTodoApiV1TodosTodoIdDeleteData, DeleteTodoApiV1TodosTodoIdDeleteErrors, DeleteTodoApiV1TodosTodoIdDeleteResponses, GetCurrentUserInfoApiV1AuthMeGetData, GetCurrentUserInfoApiV1AuthMeGetErrors, GetCurrentUserInfoApiV1AuthMeGetResponses, GetSessionsApiV1AuthSessionsGetData, GetSessionsApiV1AuthSessionsGetResponses, LoginApiV1AuthLoginPostData, LoginApiV1AuthLoginPostErrors, LoginApiV1AuthLoginPostResponses, LogoutAllApiV1AuthLogoutAllPostData, LogoutAllApiV1AuthLogoutAllPostResponses, LogoutApiV1AuthLogoutPostData, LogoutApiV1AuthLogoutPostResponses, ReadTodoApiV1TodosTodoIdGetData, ReadTodoApiV1TodosTodoIdGetErrors, ReadTodoApiV1TodosTodoIdGetResponses, ReadTodosApiV1TodosGetData, ReadTodosApiV1TodosGetErrors, ReadTodosApiV1TodosGetResponses, RefreshTokenApiV1AuthRefreshPostData, RefreshTokenApiV1AuthRefreshPostErrors, RefreshTokenApiV1AuthRefreshPostResponses, RegisterApiV1AuthRegisterPostData, RegisterApiV1AuthRegisterPostErrors, RegisterApiV1AuthRegisterPostResponses, RootGetData, RootGetResponses, UpdateTodoApiV1TodosTodoIdPatchData, UpdateTodoApiV1TodosTodoIdPatchErrors, UpdateTodoApiV1TodosTodoIdPatchResponses, UploadImageApiV1UploadImagePostData, UploadImageApiV1UploadImagePostErrors, UploadImageApiV1UploadImagePostResponses } from './types.gen'; + +export type Options = Options2 & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Read Todos + */ +export const readTodosApiV1TodosGet = (options?: Options) => (options?.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/todos/', + ...options +}); + +/** + * Create Todo + */ +export const createTodoApiV1TodosPost = (options: Options) => (options.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/todos/', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Delete Todo + */ +export const deleteTodoApiV1TodosTodoIdDelete = (options: Options) => (options.client ?? client).delete({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/todos/{todo_id}', + ...options +}); + +/** + * Read Todo + */ +export const readTodoApiV1TodosTodoIdGet = (options: Options) => (options.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/todos/{todo_id}', + ...options +}); + +/** + * Update Todo + */ +export const updateTodoApiV1TodosTodoIdPatch = (options: Options) => (options.client ?? client).patch({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/todos/{todo_id}', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Register + */ +export const registerApiV1AuthRegisterPost = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/auth/register', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * Login + */ +export const loginApiV1AuthLoginPost = (options: Options) => (options.client ?? client).post({ + url: '/api/v1/auth/login', + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers + } +}); + +/** + * 刷新访问令牌 + */ +export const refreshTokenApiV1AuthRefreshPost = (options?: Options) => (options?.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/auth/refresh', + ...options +}); + +/** + * Logout + */ +export const logoutApiV1AuthLogoutPost = (options?: Options) => (options?.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/auth/logout', + ...options +}); + +/** + * 登出所有设备 + */ +export const logoutAllApiV1AuthLogoutAllPost = (options?: Options) => (options?.client ?? client).post({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/auth/logout-all', + ...options +}); + +/** + * 获取用户会话 + * + * 获取用户的所有活跃会话 + */ +export const getSessionsApiV1AuthSessionsGet = (options?: Options) => (options?.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/auth/sessions', + ...options +}); + +/** + * 获取当前用户信息 + * + * 获取当前登录用户的信息 + */ +export const getCurrentUserInfoApiV1AuthMeGet = (options?: Options) => (options?.client ?? client).get({ + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/auth/me', + ...options +}); + +/** + * Upload Image + */ +export const uploadImageApiV1UploadImagePost = (options: Options) => (options.client ?? client).post({ + ...formDataBodySerializer, + security: [{ scheme: 'bearer', type: 'http' }], + url: '/api/v1/upload/image', + ...options, + headers: { + 'Content-Type': null, + ...options.headers + } +}); + +/** + * 根路径 + * + * 根路径,返回应用信息 + */ +export const rootGet = (options?: Options) => (options?.client ?? client).get({ url: '/', ...options }); diff --git a/src/chongming-web/src/api/generated/services/AuthenticationService.ts b/src/chongming-web/src/api/generated/services/AuthenticationService.ts deleted file mode 100644 index 8c666d6..0000000 --- a/src/chongming-web/src/api/generated/services/AuthenticationService.ts +++ /dev/null @@ -1,109 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { RefreshTokenResponse } from '../models/RefreshTokenResponse'; -import type { TokenResponse } from '../models/TokenResponse'; -import type { UserCreate } from '../models/UserCreate'; -import type { UserLogin } from '../models/UserLogin'; -import type { UserRead } from '../models/UserRead'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class AuthenticationService { - /** - * Register - * @param requestBody - * @returns UserRead Successful Response - * @throws ApiError - */ - public static registerApiV1AuthRegisterPost( - requestBody: UserCreate, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/auth/register', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Login - * @param requestBody - * @returns TokenResponse Successful Response - * @throws ApiError - */ - public static loginApiV1AuthLoginPost( - requestBody: UserLogin, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/auth/login', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * 刷新访问令牌 - * @returns RefreshTokenResponse Successful Response - * @throws ApiError - */ - public static refreshTokenApiV1AuthRefreshPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/auth/refresh', - }); - } - /** - * Logout - * @returns any Successful Response - * @throws ApiError - */ - public static logoutApiV1AuthLogoutPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/auth/logout', - }); - } - /** - * 登出所有设备 - * @returns any Successful Response - * @throws ApiError - */ - public static logoutAllApiV1AuthLogoutAllPost(): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/auth/logout-all', - }); - } - /** - * 获取用户会话 - * 获取用户的所有活跃会话 - * @returns any Successful Response - * @throws ApiError - */ - public static getSessionsApiV1AuthSessionsGet(): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/auth/sessions', - }); - } - /** - * 获取当前用户信息 - * 获取当前登录用户的信息 - * @returns UserRead Successful Response - * @throws ApiError - */ - public static getCurrentUserInfoApiV1AuthMeGet(): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/auth/me', - }); - } -} diff --git a/src/chongming-web/src/api/generated/services/DefaultService.ts b/src/chongming-web/src/api/generated/services/DefaultService.ts deleted file mode 100644 index ae257e0..0000000 --- a/src/chongming-web/src/api/generated/services/DefaultService.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TaskRequest } from '../models/TaskRequest'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class DefaultService { - /** - * 根路径 - * 根路径,返回应用信息 - * @returns any Successful Response - * @throws ApiError - */ - public static rootGet(): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/', - }); - } - /** - * Add Task - * 添加定时任务接口 - * @param requestBody - * @returns any Successful Response - * @throws ApiError - */ - public static addTaskSchedulePost( - requestBody: TaskRequest, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/schedule', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } -} diff --git a/src/chongming-web/src/api/generated/services/TodoService.ts b/src/chongming-web/src/api/generated/services/TodoService.ts deleted file mode 100644 index d2f5b71..0000000 --- a/src/chongming-web/src/api/generated/services/TodoService.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -import type { TodoCreate } from '../models/TodoCreate'; -import type { TodoRead } from '../models/TodoRead'; -import type { TodoUpdate } from '../models/TodoUpdate'; -import type { CancelablePromise } from '../core/CancelablePromise'; -import { OpenAPI } from '../core/OpenAPI'; -import { request as __request } from '../core/request'; -export class TodoService { - /** - * Create Todo - * @param requestBody - * @returns TodoRead Successful Response - * @throws ApiError - */ - public static createTodoApiV1TodosPost( - requestBody: TodoCreate, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'POST', - url: '/api/v1/todos/', - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Read Todos - * @param offset - * @param limit - * @returns TodoRead Successful Response - * @throws ApiError - */ - public static readTodosApiV1TodosGet( - offset?: number, - limit: number = 100, - ): CancelablePromise> { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/todos/', - query: { - 'offset': offset, - 'limit': limit, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Read Todo - * @param todoId - * @returns TodoRead Successful Response - * @throws ApiError - */ - public static readTodoApiV1TodosTodoIdGet( - todoId: number, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'GET', - url: '/api/v1/todos/{todo_id}', - path: { - 'todo_id': todoId, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Update Todo - * @param todoId - * @param requestBody - * @returns TodoRead Successful Response - * @throws ApiError - */ - public static updateTodoApiV1TodosTodoIdPatch( - todoId: number, - requestBody: TodoUpdate, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'PATCH', - url: '/api/v1/todos/{todo_id}', - path: { - 'todo_id': todoId, - }, - body: requestBody, - mediaType: 'application/json', - errors: { - 422: `Validation Error`, - }, - }); - } - /** - * Delete Todo - * @param todoId - * @returns any Successful Response - * @throws ApiError - */ - public static deleteTodoApiV1TodosTodoIdDelete( - todoId: number, - ): CancelablePromise { - return __request(OpenAPI, { - method: 'DELETE', - url: '/api/v1/todos/{todo_id}', - path: { - 'todo_id': todoId, - }, - errors: { - 422: `Validation Error`, - }, - }); - } -} diff --git a/src/chongming-web/src/api/generated/types.gen.ts b/src/chongming-web/src/api/generated/types.gen.ts new file mode 100644 index 0000000..209a857 --- /dev/null +++ b/src/chongming-web/src/api/generated/types.gen.ts @@ -0,0 +1,644 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type ClientOptions = { + baseUrl: 'http://127.0.0.1:8000' | (string & {}); +}; + +/** + * Body_upload_image_api_v1_upload_image_post + */ +export type BodyUploadImageApiV1UploadImagePost = { + /** + * File + */ + file: Blob | File; +}; + +/** + * Detail + */ +export type Detail = { + /** + * Message + */ + message: string; + /** + * Code + */ + code: number; +}; + +/** + * HTTPError + */ +export type HttpError = { + detail?: Detail | null; +}; + +/** + * HTTPValidationError + */ +export type HttpValidationError = { + /** + * Detail + */ + detail?: Array; +}; + +/** + * RefreshTokenResponse + * + * 刷新 Token 响应模型 + */ +export type RefreshTokenResponse = { + /** + * Access Token + */ + access_token: string; + /** + * Token Type + */ + token_type?: string; +}; + +/** + * TodoCreate + */ +export type TodoCreate = { + /** + * Title + */ + title: string; + /** + * Description + */ + description?: string | null; + /** + * Completed + */ + completed?: boolean; +}; + +/** + * TodoRead + */ +export type TodoRead = { + /** + * Title + */ + title: string; + /** + * Description + */ + description?: string | null; + /** + * Completed + */ + completed?: boolean; + /** + * Id + */ + id: number; +}; + +/** + * TodoUpdate + */ +export type TodoUpdate = { + /** + * Title + */ + title?: string | null; + /** + * Description + */ + description?: string | null; + /** + * Completed + */ + completed?: boolean | null; +}; + +/** + * TokenResponse + * + * Token 响应模型 + */ +export type TokenResponse = { + /** + * Access Token + */ + access_token: string; + /** + * Refresh Token + */ + refresh_token: string; + /** + * Token Type + */ + token_type?: string; + /** + * Expires In + */ + expires_in: number; + /** + * Refresh Expires In + */ + refresh_expires_in: number; +}; + +/** + * UploadResponse + */ +export type UploadResponse = { + /** + * Url + */ + url: string; + /** + * Filename + */ + filename: string; + /** + * File Type + */ + file_type: string; +}; + +/** + * UserCreate + * + * 用户创建模型 + */ +export type UserCreate = { + /** + * Email + */ + email: string; + /** + * Username + */ + username?: string | null; + /** + * Full Name + */ + full_name?: string | null; + /** + * Is Active + */ + is_active?: boolean; + /** + * Is Superuser + */ + is_superuser?: boolean; + /** + * Created At + */ + created_at?: string | null; + /** + * Password + */ + password: string; +}; + +/** + * UserLogin + */ +export type UserLogin = { + /** + * Email + */ + email: string; + /** + * Password + */ + password: string; +}; + +/** + * UserRead + * + * 用户读取模型 + */ +export type UserRead = { + /** + * Email + */ + email: string; + /** + * Username + */ + username?: string | null; + /** + * Full Name + */ + full_name?: string | null; + /** + * Is Active + */ + is_active?: boolean; + /** + * Is Superuser + */ + is_superuser?: boolean; + /** + * Created At + */ + created_at?: string | null; + /** + * Id + */ + id: string; +}; + +/** + * Userlogout + */ +export type Userlogout = { + /** + * Code + */ + code?: number; + /** + * Message + */ + message?: string; +}; + +/** + * ValidationError + */ +export type ValidationError = { + /** + * Location + */ + loc: Array; + /** + * Message + */ + msg: string; + /** + * Error Type + */ + type: string; + /** + * Input + */ + input?: unknown; + /** + * Context + */ + ctx?: { + [key: string]: unknown; + }; +}; + +export type ReadTodosApiV1TodosGetData = { + body?: never; + path?: never; + query?: { + /** + * Offset + */ + offset?: number; + /** + * Limit + */ + limit?: number; + }; + url: '/api/v1/todos/'; +}; + +export type ReadTodosApiV1TodosGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReadTodosApiV1TodosGetError = ReadTodosApiV1TodosGetErrors[keyof ReadTodosApiV1TodosGetErrors]; + +export type ReadTodosApiV1TodosGetResponses = { + /** + * Response Read Todos Api V1 Todos Get + * + * Successful Response + */ + 200: Array; +}; + +export type ReadTodosApiV1TodosGetResponse = ReadTodosApiV1TodosGetResponses[keyof ReadTodosApiV1TodosGetResponses]; + +export type CreateTodoApiV1TodosPostData = { + body: TodoCreate; + path?: never; + query?: never; + url: '/api/v1/todos/'; +}; + +export type CreateTodoApiV1TodosPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateTodoApiV1TodosPostError = CreateTodoApiV1TodosPostErrors[keyof CreateTodoApiV1TodosPostErrors]; + +export type CreateTodoApiV1TodosPostResponses = { + /** + * Successful Response + */ + 200: TodoRead; +}; + +export type CreateTodoApiV1TodosPostResponse = CreateTodoApiV1TodosPostResponses[keyof CreateTodoApiV1TodosPostResponses]; + +export type DeleteTodoApiV1TodosTodoIdDeleteData = { + body?: never; + path: { + /** + * Todo Id + */ + todo_id: number; + }; + query?: never; + url: '/api/v1/todos/{todo_id}'; +}; + +export type DeleteTodoApiV1TodosTodoIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type DeleteTodoApiV1TodosTodoIdDeleteError = DeleteTodoApiV1TodosTodoIdDeleteErrors[keyof DeleteTodoApiV1TodosTodoIdDeleteErrors]; + +export type DeleteTodoApiV1TodosTodoIdDeleteResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + +export type ReadTodoApiV1TodosTodoIdGetData = { + body?: never; + path: { + /** + * Todo Id + */ + todo_id: number; + }; + query?: never; + url: '/api/v1/todos/{todo_id}'; +}; + +export type ReadTodoApiV1TodosTodoIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ReadTodoApiV1TodosTodoIdGetError = ReadTodoApiV1TodosTodoIdGetErrors[keyof ReadTodoApiV1TodosTodoIdGetErrors]; + +export type ReadTodoApiV1TodosTodoIdGetResponses = { + /** + * Successful Response + */ + 200: TodoRead; +}; + +export type ReadTodoApiV1TodosTodoIdGetResponse = ReadTodoApiV1TodosTodoIdGetResponses[keyof ReadTodoApiV1TodosTodoIdGetResponses]; + +export type UpdateTodoApiV1TodosTodoIdPatchData = { + body: TodoUpdate; + path: { + /** + * Todo Id + */ + todo_id: number; + }; + query?: never; + url: '/api/v1/todos/{todo_id}'; +}; + +export type UpdateTodoApiV1TodosTodoIdPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateTodoApiV1TodosTodoIdPatchError = UpdateTodoApiV1TodosTodoIdPatchErrors[keyof UpdateTodoApiV1TodosTodoIdPatchErrors]; + +export type UpdateTodoApiV1TodosTodoIdPatchResponses = { + /** + * Successful Response + */ + 200: TodoRead; +}; + +export type UpdateTodoApiV1TodosTodoIdPatchResponse = UpdateTodoApiV1TodosTodoIdPatchResponses[keyof UpdateTodoApiV1TodosTodoIdPatchResponses]; + +export type RegisterApiV1AuthRegisterPostData = { + body: UserCreate; + path?: never; + query?: never; + url: '/api/v1/auth/register'; +}; + +export type RegisterApiV1AuthRegisterPostErrors = { + /** + * 邮箱已被注册 + */ + 400: HttpError; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type RegisterApiV1AuthRegisterPostError = RegisterApiV1AuthRegisterPostErrors[keyof RegisterApiV1AuthRegisterPostErrors]; + +export type RegisterApiV1AuthRegisterPostResponses = { + /** + * Successful Response + */ + 200: UserRead; +}; + +export type RegisterApiV1AuthRegisterPostResponse = RegisterApiV1AuthRegisterPostResponses[keyof RegisterApiV1AuthRegisterPostResponses]; + +export type LoginApiV1AuthLoginPostData = { + body: UserLogin; + path?: never; + query?: never; + url: '/api/v1/auth/login'; +}; + +export type LoginApiV1AuthLoginPostErrors = { + /** + * 邮箱不存在 + */ + 400: HttpError; + /** + * 密码错误 + */ + 401: HttpError; + /** + * 用户被禁用 + */ + 402: HttpError; + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type LoginApiV1AuthLoginPostError = LoginApiV1AuthLoginPostErrors[keyof LoginApiV1AuthLoginPostErrors]; + +export type LoginApiV1AuthLoginPostResponses = { + /** + * Successful Response + */ + 200: TokenResponse; +}; + +export type LoginApiV1AuthLoginPostResponse = LoginApiV1AuthLoginPostResponses[keyof LoginApiV1AuthLoginPostResponses]; + +export type RefreshTokenApiV1AuthRefreshPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/refresh'; +}; + +export type RefreshTokenApiV1AuthRefreshPostErrors = { + /** + * 无效的刷新令牌 + */ + 401: HttpError; +}; + +export type RefreshTokenApiV1AuthRefreshPostError = RefreshTokenApiV1AuthRefreshPostErrors[keyof RefreshTokenApiV1AuthRefreshPostErrors]; + +export type RefreshTokenApiV1AuthRefreshPostResponses = { + /** + * Successful Response + */ + 200: RefreshTokenResponse; +}; + +export type RefreshTokenApiV1AuthRefreshPostResponse = RefreshTokenApiV1AuthRefreshPostResponses[keyof RefreshTokenApiV1AuthRefreshPostResponses]; + +export type LogoutApiV1AuthLogoutPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/logout'; +}; + +export type LogoutApiV1AuthLogoutPostResponses = { + /** + * Successful Response + */ + 200: Userlogout; +}; + +export type LogoutApiV1AuthLogoutPostResponse = LogoutApiV1AuthLogoutPostResponses[keyof LogoutApiV1AuthLogoutPostResponses]; + +export type LogoutAllApiV1AuthLogoutAllPostData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/logout-all'; +}; + +export type LogoutAllApiV1AuthLogoutAllPostResponses = { + /** + * Successful Response + */ + 200: Userlogout; +}; + +export type LogoutAllApiV1AuthLogoutAllPostResponse = LogoutAllApiV1AuthLogoutAllPostResponses[keyof LogoutAllApiV1AuthLogoutAllPostResponses]; + +export type GetSessionsApiV1AuthSessionsGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/sessions'; +}; + +export type GetSessionsApiV1AuthSessionsGetResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + +export type GetCurrentUserInfoApiV1AuthMeGetData = { + body?: never; + path?: never; + query?: never; + url: '/api/v1/auth/me'; +}; + +export type GetCurrentUserInfoApiV1AuthMeGetErrors = { + /** + * 用户不存在 + */ + 404: HttpError; +}; + +export type GetCurrentUserInfoApiV1AuthMeGetError = GetCurrentUserInfoApiV1AuthMeGetErrors[keyof GetCurrentUserInfoApiV1AuthMeGetErrors]; + +export type GetCurrentUserInfoApiV1AuthMeGetResponses = { + /** + * Successful Response + */ + 200: UserRead; +}; + +export type GetCurrentUserInfoApiV1AuthMeGetResponse = GetCurrentUserInfoApiV1AuthMeGetResponses[keyof GetCurrentUserInfoApiV1AuthMeGetResponses]; + +export type UploadImageApiV1UploadImagePostData = { + body: BodyUploadImageApiV1UploadImagePost; + path?: never; + query?: never; + url: '/api/v1/upload/image'; +}; + +export type UploadImageApiV1UploadImagePostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UploadImageApiV1UploadImagePostError = UploadImageApiV1UploadImagePostErrors[keyof UploadImageApiV1UploadImagePostErrors]; + +export type UploadImageApiV1UploadImagePostResponses = { + /** + * Successful Response + */ + 200: UploadResponse; +}; + +export type UploadImageApiV1UploadImagePostResponse = UploadImageApiV1UploadImagePostResponses[keyof UploadImageApiV1UploadImagePostResponses]; + +export type RootGetData = { + body?: never; + path?: never; + query?: never; + url: '/'; +}; + +export type RootGetResponses = { + /** + * Successful Response + */ + 200: unknown; +}; diff --git a/src/chongming-web/src/assets/base.css b/src/chongming-web/src/assets/base.css deleted file mode 100644 index 8816868..0000000 --- a/src/chongming-web/src/assets/base.css +++ /dev/null @@ -1,86 +0,0 @@ -/* color palette from */ -:root { - --vt-c-white: #ffffff; - --vt-c-white-soft: #f8f8f8; - --vt-c-white-mute: #f2f2f2; - - --vt-c-black: #181818; - --vt-c-black-soft: #222222; - --vt-c-black-mute: #282828; - - --vt-c-indigo: #2c3e50; - - --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); - --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); - --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); - --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); - - --vt-c-text-light-1: var(--vt-c-indigo); - --vt-c-text-light-2: rgba(60, 60, 60, 0.66); - --vt-c-text-dark-1: var(--vt-c-white); - --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); -} - -/* semantic color variables for this project */ -:root { - --color-background: var(--vt-c-white); - --color-background-soft: var(--vt-c-white-soft); - --color-background-mute: var(--vt-c-white-mute); - - --color-border: var(--vt-c-divider-light-2); - --color-border-hover: var(--vt-c-divider-light-1); - - --color-heading: var(--vt-c-text-light-1); - --color-text: var(--vt-c-text-light-1); - - --section-gap: 160px; -} - -@media (prefers-color-scheme: dark) { - :root { - --color-background: var(--vt-c-black); - --color-background-soft: var(--vt-c-black-soft); - --color-background-mute: var(--vt-c-black-mute); - - --color-border: var(--vt-c-divider-dark-2); - --color-border-hover: var(--vt-c-divider-dark-1); - - --color-heading: var(--vt-c-text-dark-1); - --color-text: var(--vt-c-text-dark-2); - } -} - -*, -*::before, -*::after { - box-sizing: border-box; - margin: 0; - font-weight: normal; -} - -body { - min-height: 100vh; - color: var(--color-text); - background: var(--color-background); - transition: - color 0.5s, - background-color 0.5s; - line-height: 1.6; - font-family: - Inter, - -apple-system, - BlinkMacSystemFont, - 'Segoe UI', - Roboto, - Oxygen, - Ubuntu, - Cantarell, - 'Fira Sans', - 'Droid Sans', - 'Helvetica Neue', - sans-serif; - font-size: 15px; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} diff --git a/src/chongming-web/src/assets/main.css b/src/chongming-web/src/assets/main.css deleted file mode 100644 index 36fb845..0000000 --- a/src/chongming-web/src/assets/main.css +++ /dev/null @@ -1,35 +0,0 @@ -@import './base.css'; - -#app { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - font-weight: normal; -} - -a, -.green { - text-decoration: none; - color: hsla(160, 100%, 37%, 1); - transition: 0.4s; - padding: 3px; -} - -@media (hover: hover) { - a:hover { - background-color: hsla(160, 100%, 37%, 0.2); - } -} - -@media (min-width: 1024px) { - body { - display: flex; - place-items: center; - } - - #app { - display: grid; - grid-template-columns: 1fr 1fr; - padding: 0 2rem; - } -} diff --git a/src/chongming-web/src/components/HelloWorld.vue b/src/chongming-web/src/components/HelloWorld.vue deleted file mode 100644 index d174cf8..0000000 --- a/src/chongming-web/src/components/HelloWorld.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - - - diff --git a/src/chongming-web/src/components/TheWelcome.vue b/src/chongming-web/src/components/TheWelcome.vue deleted file mode 100644 index 8b731d9..0000000 --- a/src/chongming-web/src/components/TheWelcome.vue +++ /dev/null @@ -1,95 +0,0 @@ - - - diff --git a/src/chongming-web/src/components/WelcomeItem.vue b/src/chongming-web/src/components/WelcomeItem.vue deleted file mode 100644 index 6d7086a..0000000 --- a/src/chongming-web/src/components/WelcomeItem.vue +++ /dev/null @@ -1,87 +0,0 @@ - - - diff --git a/src/chongming-web/src/components/icons/IconCommunity.vue b/src/chongming-web/src/components/icons/IconCommunity.vue deleted file mode 100644 index 2dc8b05..0000000 --- a/src/chongming-web/src/components/icons/IconCommunity.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/chongming-web/src/components/icons/IconDocumentation.vue b/src/chongming-web/src/components/icons/IconDocumentation.vue deleted file mode 100644 index 6d4791c..0000000 --- a/src/chongming-web/src/components/icons/IconDocumentation.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/chongming-web/src/components/icons/IconEcosystem.vue b/src/chongming-web/src/components/icons/IconEcosystem.vue deleted file mode 100644 index c3a4f07..0000000 --- a/src/chongming-web/src/components/icons/IconEcosystem.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/chongming-web/src/components/icons/IconSupport.vue b/src/chongming-web/src/components/icons/IconSupport.vue deleted file mode 100644 index 7452834..0000000 --- a/src/chongming-web/src/components/icons/IconSupport.vue +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/src/chongming-web/src/components/icons/IconTooling.vue b/src/chongming-web/src/components/icons/IconTooling.vue deleted file mode 100644 index 660598d..0000000 --- a/src/chongming-web/src/components/icons/IconTooling.vue +++ /dev/null @@ -1,19 +0,0 @@ - - diff --git a/src/chongming-web/src/main.ts b/src/chongming-web/src/main.ts index fd1b508..ee64a16 100644 --- a/src/chongming-web/src/main.ts +++ b/src/chongming-web/src/main.ts @@ -1,19 +1,31 @@ -import './assets/main.css' +// import './assets/main.css' +import 'element-plus/dist/index.css' import { createApp } from 'vue' import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import zhCn from 'element-plus/es/locale/lang/zh-cn' import App from './App.vue' import router from './router' -import { OpenAPI } from "./api/generated" +import { useAuthStore } from './stores/auth' + /** - * 打包时注释该项 + * 打包时取消该注释 */ -OpenAPI.BASE = "http://localhost:8000" +import { client } from "./api/generated/client.gen" +client.setConfig({ + baseUrl: "/" +}) + const app = createApp(App) app.use(createPinia()) app.use(router) +app.use(ElementPlus, { locale: zhCn }) + +const authStore = useAuthStore() +authStore.initAuth() app.mount('#app') diff --git a/src/chongming-web/src/router/index.ts b/src/chongming-web/src/router/index.ts index 3e49915..63a3c92 100644 --- a/src/chongming-web/src/router/index.ts +++ b/src/chongming-web/src/router/index.ts @@ -1,23 +1,44 @@ +// src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' -import HomeView from '../views/HomeView.vue' +import { useAuthStore } from '@/stores/auth' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ + { path: '/', redirect: '/todos' }, { - path: '/', - name: 'home', - component: HomeView, + path: '/login', + name: 'login', + component: () => import('@/views/LoginView.vue'), + meta: { requiresGuest: true }, }, { - path: '/about', - name: 'about', - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import('../views/AboutView.vue'), + path: '/register', + name: 'register', + component: () => import('@/views/RegisterView.vue'), + meta: { requiresGuest: true }, + }, + { + path: '/todos', + name: 'todos', + component: () => import('@/views/TodoView.vue'), + meta: { requiresAuth: true }, }, ], }) -export default router +// 路由守卫 +router.beforeEach((to, from, next) => { + const authStore = useAuthStore() + const isAuthenticated = !!authStore.accessToken + + if (to.meta.requiresAuth && !isAuthenticated) { + next('/login') + } else if (to.meta.requiresGuest && isAuthenticated) { + next('/todos') + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/src/chongming-web/src/stores/auth.ts b/src/chongming-web/src/stores/auth.ts new file mode 100644 index 0000000..9ac5acf --- /dev/null +++ b/src/chongming-web/src/stores/auth.ts @@ -0,0 +1,77 @@ +import { defineStore } from "pinia" +import { ElMessage } from 'element-plus' +import { client } from "@/api/generated/client.gen" +import { loginApiV1AuthLoginPost, registerApiV1AuthRegisterPost } from "@/api/generated" + + +interface AuthState { + accessToken: string | null + refreshToken: string | null +} + +export const useAuthStore = defineStore("auth", { + state: (): AuthState => ({ + accessToken: localStorage.getItem("access_token"), + refreshToken: localStorage.getItem("refresh_token"), + }), + actions: { + setTokens(access: string, refresh: string) { + this.accessToken = access + this.refreshToken = refresh + localStorage.setItem("access_token", access) + localStorage.setItem("refresh_token", refresh) + client.setConfig({ + auth: () => this.accessToken || '' + }) + }, + clearTokens() { + this.accessToken = null + this.refreshToken = null + localStorage.removeItem("access_token") + localStorage.removeItem("refresh_token") + client.setConfig({ auth: undefined }) + }, + async login(email: string, password: string) { + try { + const { data } = await loginApiV1AuthLoginPost({ + body: { email, password }, + throwOnError: true, + }) + if (data) { + this.setTokens(data.access_token, data.refresh_token) + ElMessage.success("登录成功") + return true + } + return false + } catch (error: any) { + // console.error("登录失败", error) + ElMessage.error(error.message || "登录失败") + return false + } + }, + async register(email: string, password: string, username?: string) { + try { + await registerApiV1AuthRegisterPost({ + body: { email, password, username }, + throwOnError: true, + }) + ElMessage.success("注册成功") + return await this.login(email, password) + } catch (error: any) { + ElMessage.error(error.message || "注册失败") + return false + } + }, + logout() { + this.clearTokens() + ElMessage.info("已退出登录") + }, + initAuth() { + if (this.accessToken) { + client.setConfig({ + auth: () => this.accessToken || '' + }) + } + } + } +}) \ No newline at end of file diff --git a/src/chongming-web/src/stores/todo.ts b/src/chongming-web/src/stores/todo.ts new file mode 100644 index 0000000..96b71dc --- /dev/null +++ b/src/chongming-web/src/stores/todo.ts @@ -0,0 +1,69 @@ +// src/stores/todo.ts +import { defineStore } from 'pinia' +import { + readTodosApiV1TodosGet, + createTodoApiV1TodosPost, + updateTodoApiV1TodosTodoIdPatch, + deleteTodoApiV1TodosTodoIdDelete, + type TodoRead, + type TodoCreate, + type TodoUpdate, +} from '@/api/generated' + +interface TodoState { + todos: TodoRead[] + loading: boolean +} + +export const useTodoStore = defineStore('todo', { + state: (): TodoState => ({ + todos: [], + loading: false, + }), + actions: { + async fetchTodos() { + this.loading = true + try { + const { data } = await readTodosApiV1TodosGet({ + query: { offset: 0, limit: 100 }, + throwOnError: true, + }) + this.todos = data || [] + } catch (error) { + console.error('获取 Todo 列表失败', error) + throw error + } finally { + this.loading = false + } + }, + async addTodo(todo: TodoCreate) { + const { data } = await createTodoApiV1TodosPost({ + body: todo, + throwOnError: true, + }) + if (data) { + this.todos.unshift(data) + } + return data + }, + async updateTodo(id: number, updates: TodoUpdate) { + const { data } = await updateTodoApiV1TodosTodoIdPatch({ + path: { todo_id: id }, + body: updates, + throwOnError: true, + }) + if (data) { + const index = this.todos.findIndex((t) => t.id === id) + if (index !== -1) this.todos[index] = data + } + return data + }, + async deleteTodo(id: number) { + await deleteTodoApiV1TodosTodoIdDelete({ + path: { todo_id: id }, + throwOnError: true, + }) + this.todos = this.todos.filter((t) => t.id !== id) + }, + }, +}) \ No newline at end of file diff --git a/src/chongming-web/src/views/AboutView.vue b/src/chongming-web/src/views/AboutView.vue deleted file mode 100644 index 756ad2a..0000000 --- a/src/chongming-web/src/views/AboutView.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/src/chongming-web/src/views/HomeView.vue b/src/chongming-web/src/views/HomeView.vue deleted file mode 100644 index d5c0217..0000000 --- a/src/chongming-web/src/views/HomeView.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/src/chongming-web/src/views/LoginView.vue b/src/chongming-web/src/views/LoginView.vue new file mode 100644 index 0000000..ce44549 --- /dev/null +++ b/src/chongming-web/src/views/LoginView.vue @@ -0,0 +1,58 @@ + + + + + + \ No newline at end of file diff --git a/src/chongming-web/src/views/RegisterView.vue b/src/chongming-web/src/views/RegisterView.vue new file mode 100644 index 0000000..9851f3f --- /dev/null +++ b/src/chongming-web/src/views/RegisterView.vue @@ -0,0 +1,61 @@ + + + + + + \ No newline at end of file diff --git a/src/chongming-web/src/views/TodoView.vue b/src/chongming-web/src/views/TodoView.vue new file mode 100644 index 0000000..7434a99 --- /dev/null +++ b/src/chongming-web/src/views/TodoView.vue @@ -0,0 +1,245 @@ + + + + + + \ No newline at end of file diff --git a/src/chongming-web/tsconfig.app.json b/src/chongming-web/tsconfig.app.json index c0f2d86..0804c9f 100644 --- a/src/chongming-web/tsconfig.app.json +++ b/src/chongming-web/tsconfig.app.json @@ -1,18 +1,24 @@ { "extends": "@vue/tsconfig/tsconfig.dom.json", - "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], - "exclude": ["src/**/__tests__/*"], + "include": [ + "env.d.ts", + "src/**/*", + "src/**/*.vue" + ], + "exclude": [ + "src/**/__tests__/*" + ], "compilerOptions": { // Extra safety for array and object lookups, but may have false positives. "noUncheckedIndexedAccess": true, - // Path mapping for cleaner imports. "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] }, - // `vue-tsc --build` produces a .tsbuildinfo file for incremental type-checking. // Specified here to keep it out of the root directory. "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo" } -} +} \ No newline at end of file diff --git a/src/chongming/app/__init__.py b/src/chongming/app/__init__.py index 245867d..0ee8f32 100644 --- a/src/chongming/app/__init__.py +++ b/src/chongming/app/__init__.py @@ -1,3 +1,4 @@ +import time from pathlib import Path from contextlib import asynccontextmanager @@ -8,23 +9,19 @@ from fastapi.staticfiles import StaticFiles from sqlmodel import SQLModel, text from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker from sqlalchemy.exc import OperationalError -from pydantic import BaseModel from .core.config import get_config from .core.cache import get_cache from .core.logger import get_logger -from .core.scheduler import get_task_service_instance from .core.static_files import SVFSStaticFiles from .api import api_router from .task import init_tasks_callback - -class TaskRequest(BaseModel): - task_name: str - interval: int +from plugins.scheduler.scheduler import get_task_service_instance config = get_config() +cache = get_cache() logger = get_logger("app") @@ -38,6 +35,9 @@ except KeyError: # pragma: no cover @asynccontextmanager async def lifespan(app: FastAPI): from plugins import hello + from plugins.jwt.jwt_cache import get_jwt_cache + + get_jwt_cache(config, cache) hello() database = config["database"] @@ -73,7 +73,9 @@ async def lifespan(app: FastAPI): raise # 启动时:初始化任务服务 - task_service = get_task_service_instance(async_session_maker) + task_service = get_task_service_instance( + config, get_logger("scheduler"), async_session_maker + ) app.state.task_service = task_service await task_service.start(init_tasks_callback) @@ -82,7 +84,7 @@ async def lifespan(app: FastAPI): # 关闭时:释放引擎 await task_service.shutdown(wait=True) await engine.dispose() - get_cache().close() + cache.close() # 创建 FastAPI 应用 @@ -104,6 +106,21 @@ app.add_middleware(CORSMiddleware, **cors) app.include_router(api_router, prefix=config["default"]["prefix"]) +@app.middleware("http") +async def access_log_middleware(request: Request, call_next): + start = time.perf_counter() + response = await call_next(request) + duration = (time.perf_counter() - start) * 1000 + client = request.client.host if request.client else "-" + + logger.info( + f"{request.method} {request.url.path} status={response.status_code} " + f"duration={duration:.2f}ms client={client}", + extra={"status_code": response.status_code}, # ← 关键 + ) + return response + + # 根路由 @app.get("/", summary="根路径") async def root(): @@ -116,6 +133,14 @@ async def root(): } +# from pydantic import BaseModel + + +# class TaskRequest(BaseModel): +# task_name: str +# interval: int + + # @app.post("/schedule") # async def add_task( # request: TaskRequest, task_service: TaskService = Depends(get_task_service) diff --git a/src/chongming/app/api/deps.py b/src/chongming/app/api/deps.py index 775e039..c3eaec4 100644 --- a/src/chongming/app/api/deps.py +++ b/src/chongming/app/api/deps.py @@ -2,7 +2,7 @@ from fastapi import Depends, HTTPException, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from ..service.auth import get_auth_service -from ..core.jwt_cache import TokenData +from plugins.jwt.jwt_cache import TokenData access_scheme = HTTPBearer(scheme_name="AccessToken") diff --git a/src/chongming/app/api/routers/auth.py b/src/chongming/app/api/routers/auth.py index 95ef957..89bb380 100644 --- a/src/chongming/app/api/routers/auth.py +++ b/src/chongming/app/api/routers/auth.py @@ -5,13 +5,15 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.ext.asyncio import AsyncSession from ...core.database import get_session -from ...core.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse from ...core.security import verify_password from ...service.user import UserService from ...service.auth import get_auth_service -from ...model.user import UserCreate, UserRead, UserLogin +from ...model.user import UserCreate, UserRead, UserLogin, Userlogout, UserSession +from ...model.http_error import HTTPError, Detail from ..deps import get_current_user +from plugins.jwt.jwt_cache import TokenData, TokenResponse, RefreshTokenResponse + router = APIRouter(tags=["authentication"]) @@ -19,16 +21,23 @@ access_scheme = HTTPBearer(scheme_name="AccessToken") refresh_scheme = HTTPBearer(scheme_name="RefreshToken") -@router.post("/register", response_model=UserRead) +@router.post( + "/register", + response_model=UserRead, + responses={400: {"model": HTTPError, "description": "邮箱已被注册"}}, +) async def register( user_data: UserCreate, session: AsyncSession = Depends(get_session), ): - print(user_data.password) - existing = await UserService.get_user_by_email(user_data.email, session) if existing: - raise HTTPException(status_code=400, detail="邮箱已被注册") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=Detail( + message="邮箱已被注册", code=status.HTTP_400_BAD_REQUEST + ).model_dump(), + ) user = await UserService.create_user( session=session, @@ -40,7 +49,15 @@ async def register( return user -@router.post("/login", response_model=TokenResponse) +@router.post( + "/login", + response_model=TokenResponse, + responses={ + 400: {"model": HTTPError, "description": "邮箱不存在"}, + 401: {"model": HTTPError, "description": "密码错误"}, + 402: {"model": HTTPError, "description": "用户被禁用"}, + }, +) async def login( user_data: UserLogin, request: Request, @@ -48,16 +65,29 @@ async def login( ): user = await UserService.get_user_by_email(user_data.email, session) if not user: - raise HTTPException(status_code=400, detail="邮箱或密码错误") + raise HTTPException( + status_code=400, detail=Detail(message="邮箱不存在", code=400).model_dump() + ) if not verify_password(user_data.password, user.hashed_password): - raise HTTPException(status_code=400, detail="邮箱或密码错误") + raise HTTPException( + status_code=400, detail=Detail(message="密码错误", code=401).model_dump() + ) if not user.is_active: - raise HTTPException(status_code=400, detail="用户被禁用") + raise HTTPException( + status_code=400, detail=Detail(message="用户被禁用", code=402).model_dump() + ) auth_service = get_auth_service() return await auth_service.create_tokens(user, request, device_id=None) -@router.post("/refresh", response_model=RefreshTokenResponse, summary="刷新访问令牌") +@router.post( + "/refresh", + response_model=RefreshTokenResponse, + responses={ + 401: {"model": HTTPError, "description": "无效的刷新令牌"}, + }, + summary="刷新访问令牌", +) async def refresh_token( request: Request, credentials: HTTPAuthorizationCredentials = Depends(refresh_scheme), @@ -72,28 +102,28 @@ async def refresh_token( if not new_access_token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="无效的刷新令牌", + detail=Detail(message="无效的刷新令牌", code=401).model_dump(), headers={"WWW-Authenticate": "Bearer"}, ) return {"access_token": new_access_token, "token_type": "bearer"} -@router.post("/logout") +@router.post("/logout", response_model=Userlogout) async def logout( credentials: HTTPAuthorizationCredentials = Depends(access_scheme), ): auth_service = get_auth_service() token = credentials.credentials await auth_service.logout(token) - return {"message": "登出成功"} + return {"message": "登出成功", "code": 200} -@router.post("/logout-all", summary="登出所有设备") +@router.post("/logout-all", summary="登出所有设备", response_model=Userlogout) async def logout_all(current_user: TokenData = Depends(get_current_user)): auth_service = get_auth_service() await auth_service.logout_all(current_user.user_id) - return {"message": "所有设备已登出"} + return {"message": "所有设备已登出", "code": 200} @router.get("/sessions", summary="获取用户会话") @@ -102,14 +132,17 @@ async def get_sessions(current_user: TokenData = Depends(get_current_user)): auth_service = get_auth_service() sessions = await auth_service.get_user_sessions(current_user.user_id) - return { - "user_id": current_user.user_id, - "sessions": sessions, - "count": len(sessions), - } + return UserSession( + user_id=current_user.user_id, sessions=sessions, count=len(sessions) + ) -@router.get("/me", response_model=UserRead, summary="获取当前用户信息") +@router.get( + "/me", + response_model=UserRead, + summary="获取当前用户信息", + responses={404: {"model": HTTPError, "description": "用户不存在"}}, +) async def get_current_user_info( session: AsyncSession = Depends(get_session), current_user: TokenData = Depends(get_current_user), @@ -117,6 +150,9 @@ async def get_current_user_info( """获取当前登录用户的信息""" user = await UserService.get_user_by_id(UUID(current_user.user_id), session) if not user: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="用户不存在") + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=Detail(message="用户不存在", code=404).model_dump(), + ) return user diff --git a/src/chongming/app/core/config.py b/src/chongming/app/core/config.py index 09fed3d..46b682a 100644 --- a/src/chongming/app/core/config.py +++ b/src/chongming/app/core/config.py @@ -64,6 +64,7 @@ class Server(TypedDict, total=False): port: Required[int] reload: bool workers: int + access_log: bool class Logging(TypedDict): diff --git a/src/chongming/app/core/logger.py b/src/chongming/app/core/logger.py index 5b026bf..770b384 100644 --- a/src/chongming/app/core/logger.py +++ b/src/chongming/app/core/logger.py @@ -1,3 +1,4 @@ +import re import logging import datetime from pathlib import Path @@ -12,6 +13,36 @@ server_config = config[env]["server"] log_config = config[env]["logging"] +class ColoredFormatter(logging.Formatter): + """为 status_code 添加颜色的日志格式化器""" + + COLORS = { + "2": "\033[32m", # 绿色 2xx + "3": "\033[36m", # 青色 3xx + "4": "\033[33m", # 黄色 4xx + "5": "\033[31m", # 红色 5xx + } + RESET = "\033[0m" + + def format(self, record): + # 保存原始消息 + original_msg = record.msg + # 如果日志记录包含 status_code 属性,则为其添加颜色 + status_code = getattr(record, "status_code", None) + if status_code and isinstance(status_code, int): + first_digit = str(status_code)[0] + color = self.COLORS.get(first_digit, "") + colored_status = f"{color}{status_code}{self.RESET}" + # 替换消息中的 "status=数字" 为带颜色的版本 + record.msg = re.sub( + rf"status={status_code}\b", f"status={colored_status}", original_msg + ) + result = super().format(record) + # 恢复原始消息,避免影响其他 handler + record.msg = original_msg + return result + + class ExcludeWatchFilesFilter(logging.Filter): def __init__(self): super().__init__() @@ -29,7 +60,13 @@ class ExcludeWatchFilesFilter(logging.Filter): return not record.name.startswith(self._filter_starts_with_tulpe) +_configured_loggers = set() + + def setup_logging(logger: logging.Logger, log_name: str = "app"): + if log_name in _configured_loggers: + return + _configured_loggers.add(log_name) log_level = getattr(logging, log_config["level"], logging.INFO) log_format = log_config["format"] # 创建文件夹 @@ -41,7 +78,8 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"): logger.setLevel(log_level) # 创建格式化器 - formatter = logging.Formatter(log_format) + plain_formatter = logging.Formatter(log_format) + color_formatter = ColoredFormatter(log_format) # 创建轮转文件处理器 - 限制文件大小为 10MB,保留 5 个备份文件 max_bytes = log_config.get("max_size", 10 * 1024 * 1024) # 默认 10MB @@ -53,12 +91,12 @@ def setup_logging(logger: logging.Logger, log_name: str = "app"): maxBytes=max_bytes, backupCount=backup_count, ) - file_handler.setFormatter(formatter) + file_handler.setFormatter(plain_formatter) file_handler.addFilter(ExcludeWatchFilesFilter()) # 创建控制台处理器并添加过滤器 console_handler = logging.StreamHandler() - console_handler.setFormatter(formatter) + console_handler.setFormatter(color_formatter) console_handler.addFilter(ExcludeWatchFilesFilter()) # 添加处理器到根日志记录器 diff --git a/src/chongming/app/model/http_error.py b/src/chongming/app/model/http_error.py new file mode 100644 index 0000000..4e3cd53 --- /dev/null +++ b/src/chongming/app/model/http_error.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel +from typing import Optional + + +class Detail(BaseModel): + message: str + code: int + + +class HTTPError(BaseModel): + detail: Optional[Detail] = None diff --git a/src/chongming/app/model/user.py b/src/chongming/app/model/user.py index 9e10e57..d1fd151 100644 --- a/src/chongming/app/model/user.py +++ b/src/chongming/app/model/user.py @@ -1,4 +1,4 @@ -from typing import Optional, List, TYPE_CHECKING +from typing import Optional, List, Dict, TYPE_CHECKING from sqlmodel import SQLModel, Field, Relationship from pydantic import BaseModel @@ -42,3 +42,14 @@ class UserRead(UserBase): class UserLogin(BaseModel): email: str password: str + + +class Userlogout(BaseModel): + code: int = 200 + message: str = "logout success" + + +class UserSession(BaseModel): + user_id: str + sessions: List[Dict[str, Optional[str]]] + count: int diff --git a/src/chongming/app/service/auth.py b/src/chongming/app/service/auth.py index c695ffc..3cf66d6 100644 --- a/src/chongming/app/service/auth.py +++ b/src/chongming/app/service/auth.py @@ -8,10 +8,11 @@ from passlib.context import CryptContext from sqlalchemy.ext.asyncio import AsyncSession from ..model.user import User -from ..core.jwt_cache import get_jwt_cache, TokenResponse, TokenData from ..core.config import get_config from .user import UserService +from plugins.jwt.jwt_cache import get_jwt_cache, TokenResponse, TokenData + class AuthService: """认证服务""" diff --git a/src/chongming/app/service/uploads.py b/src/chongming/app/service/uploads.py index 61c9009..a9b9e3b 100644 --- a/src/chongming/app/service/uploads.py +++ b/src/chongming/app/service/uploads.py @@ -1,5 +1,4 @@ import os -import time import uuid from pathlib import Path diff --git a/src/chongming/app/task/__init__.py b/src/chongming/app/task/__init__.py index ca2ba25..a835188 100644 --- a/src/chongming/app/task/__init__.py +++ b/src/chongming/app/task/__init__.py @@ -1,7 +1,8 @@ -from ..core.scheduler import TaskService from ..core.config import get_config from .dev_init_db import init_db +from plugins.scheduler.scheduler import TaskService + config = get_config() diff --git a/src/chongming/app/task/dev_init_db.py b/src/chongming/app/task/dev_init_db.py index 1ae5055..659eb86 100644 --- a/src/chongming/app/task/dev_init_db.py +++ b/src/chongming/app/task/dev_init_db.py @@ -1,9 +1,13 @@ -from ..core.scheduler import logger -from ..core.scheduler import get_task_service_instance +from ..core.logger import get_logger from ..core.constant import PermissionConstant from ..service.user import UserService from ..service.permission import PermissionService +from plugins.scheduler.scheduler import get_task_service_instance + + +logger = get_logger("scheduler") + async def dev_init_admin(session): admin_user = await UserService.get_user_by_email("admin", session) diff --git a/src/chongming/scripts/build.py b/src/chongming/scripts/build.py index 2e0e95e..3a56432 100644 --- a/src/chongming/scripts/build.py +++ b/src/chongming/scripts/build.py @@ -250,6 +250,13 @@ async def main(): with open("./build/utils/launch.py", "w", encoding="utf-8") as f: f.write(context) + jwt_key = Encryption.generate_key() + with open("./build/config.toml", "r", encoding="utf-8") as f: + context = f.read() + context = context.replace("your-secret-key-change-in-production", jwt_key) + with open("build/config.toml", "w", encoding="utf-8") as f: + f.write(context) + # 混淆 run_pyarmor_obfuscate() diff --git a/src/chongming/app/core/mqtt.py b/src/plugins/jwt/__init__.py similarity index 100% rename from src/chongming/app/core/mqtt.py rename to src/plugins/jwt/__init__.py diff --git a/src/chongming/app/core/jwt_cache.py b/src/plugins/jwt/jwt_cache.py similarity index 97% rename from src/chongming/app/core/jwt_cache.py rename to src/plugins/jwt/jwt_cache.py index 97cbbc2..d02a034 100644 --- a/src/chongming/app/core/jwt_cache.py +++ b/src/plugins/jwt/jwt_cache.py @@ -8,9 +8,6 @@ import threading import jwt -from .config import get_config -from .cache import get_cache - @dataclass class TokenCacheData: @@ -38,12 +35,12 @@ class TokenCacheData: class JWTCache: """JWT Token 缓存管理器""" - def __init__(self, db_path: Optional[str] = None): - self.config = get_config() + def __init__(self, config, cache): + self.config = config self.lock = threading.RLock() - self.cache = get_cache() + self.cache = cache self.secret_key = self.config["security"]["secret_key"] self.algorithm = self.config["security"]["algorithm"] @@ -347,11 +344,13 @@ class JWTCache: _jwt_cache = None -def get_jwt_cache() -> JWTCache: +def get_jwt_cache(config=None, cache=None) -> JWTCache: """获取 JWT 缓存实例(单例模式)""" global _jwt_cache if _jwt_cache is None: - _jwt_cache = JWTCache() + if config is None or cache is None: + raise ValueError("参数 config 和 cache 不能为空") + _jwt_cache = JWTCache(config, cache) return _jwt_cache diff --git a/src/chongming/app/core/websocket.py b/src/plugins/mqtt/__init__.py similarity index 100% rename from src/chongming/app/core/websocket.py rename to src/plugins/mqtt/__init__.py diff --git a/src/plugins/mqtt/mqtt.py b/src/plugins/mqtt/mqtt.py new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/scheduler/__init__.py b/src/plugins/scheduler/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/chongming/app/core/scheduler.py b/src/plugins/scheduler/scheduler.py similarity index 87% rename from src/chongming/app/core/scheduler.py rename to src/plugins/scheduler/scheduler.py index 94ce746..82e3b39 100644 --- a/src/chongming/app/core/scheduler.py +++ b/src/plugins/scheduler/scheduler.py @@ -19,13 +19,6 @@ from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger -from ..core.config import get_config -from ..core.logger import get_logger - - -config = get_config() -logger = get_logger("scheduler") - def acquire_scheduler_lock(): lock_path = os.path.join(tempfile.gettempdir(), "chongming_scheduler.lock") @@ -48,7 +41,7 @@ def acquire_scheduler_lock(): class TaskService: """任务调度服务""" - def __init__(self, async_session_maker=None): + def __init__(self, config, logger, async_session_maker=None): self.async_session_maker = async_session_maker # 使用独立的 SQLite 数据库存储任务(与业务数据库分开) self.job_store_db_path = config.get("scheduler", {}).get( @@ -73,6 +66,7 @@ class TaskService: ) self._started = False self.lock_file, self.is_scheduler_worker = acquire_scheduler_lock() + self.logger = logger async def start(self, init_tasks_callback: Optional[Callable] = None): """启动调度器""" @@ -80,7 +74,7 @@ class TaskService: if not self._started: self.scheduler.start() self._started = True - logger.info("任务调度器已启动") + self.logger.info("任务调度器已启动") if init_tasks_callback: await init_tasks_callback(self) @@ -94,7 +88,7 @@ class TaskService: if self._started: self.scheduler.shutdown(wait=wait) self._started = False - logger.info("任务调度器已关闭") + self.logger.info("任务调度器已关闭") if sys.platform == "win32": import msvcrt @@ -129,7 +123,7 @@ class TaskService: kwargs=kwargs or {}, replace_existing=replace_existing, ) - logger.info(f"添加间隔任务: {job.id}") + self.logger.info(f"添加间隔任务: {job.id}") return job async def add_cron_job( @@ -167,7 +161,7 @@ class TaskService: kwargs=kwargs or {}, replace_existing=replace_existing, ) - logger.info(f"添加 cron 任务: {job.id}") + self.logger.info(f"添加 cron 任务: {job.id}") return job async def add_date_job( @@ -189,37 +183,37 @@ class TaskService: kwargs=kwargs or {}, replace_existing=replace_existing, ) - logger.info(f"添加一次性任务: {job.id}") + self.logger.info(f"添加一次性任务: {job.id}") return job async def remove_job(self, job_id: str) -> bool: """删除任务""" try: self.scheduler.remove_job(job_id) - logger.info(f"删除任务: {job_id}") + self.logger.info(f"删除任务: {job_id}") return True except Exception as e: - logger.error(f"删除任务失败 {job_id}: {e}") + self.logger.error(f"删除任务失败 {job_id}: {e}") return False async def pause_job(self, job_id: str) -> bool: """暂停任务""" try: self.scheduler.pause_job(job_id) - logger.info(f"暂停任务: {job_id}") + self.logger.info(f"暂停任务: {job_id}") return True except Exception as e: - logger.error(f"暂停任务失败 {job_id}: {e}") + self.logger.error(f"暂停任务失败 {job_id}: {e}") return False async def resume_job(self, job_id: str) -> bool: """恢复任务""" try: self.scheduler.resume_job(job_id) - logger.info(f"恢复任务: {job_id}") + self.logger.info(f"恢复任务: {job_id}") return True except Exception as e: - logger.error(f"恢复任务失败 {job_id}: {e}") + self.logger.error(f"恢复任务失败 {job_id}: {e}") return False async def get_job(self, job_id: str) -> Optional[Dict]: @@ -253,10 +247,10 @@ class TaskService: raise ValueError(f"不支持的 trigger 类型: {trigger_type}") job.reschedule(trigger=trigger) - logger.info(f"重新调度任务: {job_id}") + self.logger.info(f"重新调度任务: {job_id}") return job except Exception as e: - logger.error(f"重新调度任务失败 {job_id}: {e}") + self.logger.error(f"重新调度任务失败 {job_id}: {e}") return None def _job_to_dict(self, job: Job) -> Dict: @@ -277,10 +271,12 @@ class TaskService: _task_service: Optional[TaskService] = None -def get_task_service_instance(async_session_maker=None): +def get_task_service_instance(config=None, logger=None, async_session_maker=None): global _task_service if _task_service is None: - _task_service = TaskService(async_session_maker) + if config is None or logger is None: + raise ValueError("请传入 config 和 logger") + _task_service = TaskService(config, logger, async_session_maker) return _task_service diff --git a/src/plugins/websocket/__init__.py b/src/plugins/websocket/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/plugins/websocket/websocket.py b/src/plugins/websocket/websocket.py new file mode 100644 index 0000000..e69de29 diff --git a/uploads/images/283173713eda4b60b0b338d245f088b7.jpg b/uploads/images/283173713eda4b60b0b338d245f088b7.jpg new file mode 100644 index 0000000..183cf86 Binary files /dev/null and b/uploads/images/283173713eda4b60b0b338d245f088b7.jpg differ