remove mark
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@kevisual/code-center",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.11",
|
||||
"description": "code center",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
@@ -47,6 +47,7 @@
|
||||
"@types/busboy": "^1.5.4",
|
||||
"@types/send": "^1.2.1",
|
||||
"@types/ws": "^8.18.1",
|
||||
"bullmq": "^5.65.1",
|
||||
"busboy": "^1.6.0",
|
||||
"commander": "^14.0.2",
|
||||
"cookie": "^1.1.1",
|
||||
@@ -94,7 +95,6 @@
|
||||
"socket.io": "^4.8.1",
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tar": "^7.5.2",
|
||||
"typescript": "^5.9.3",
|
||||
"ws": "npm:@kevisual/ws",
|
||||
"zod": "^4.1.13"
|
||||
},
|
||||
@@ -106,6 +106,9 @@
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild",
|
||||
"sqlite3"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"msgpackr-extract"
|
||||
]
|
||||
},
|
||||
"packageManager": "pnpm@10.24.0"
|
||||
|
||||
220
pnpm-lock.yaml
generated
220
pnpm-lock.yaml
generated
@@ -24,6 +24,9 @@ importers:
|
||||
'@types/ws':
|
||||
specifier: ^8.18.1
|
||||
version: 8.18.1
|
||||
bullmq:
|
||||
specifier: ^5.65.1
|
||||
version: 5.65.1
|
||||
busboy:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.0
|
||||
@@ -145,9 +148,6 @@ importers:
|
||||
tar:
|
||||
specifier: ^7.5.2
|
||||
version: 7.5.2
|
||||
typescript:
|
||||
specifier: ^5.9.3
|
||||
version: 5.9.3
|
||||
ws:
|
||||
specifier: npm:@kevisual/ws
|
||||
version: '@kevisual/ws@8.0.0'
|
||||
@@ -155,6 +155,31 @@ importers:
|
||||
specifier: ^4.1.13
|
||||
version: 4.1.13
|
||||
|
||||
wxmsg:
|
||||
dependencies:
|
||||
'@kevisual/context':
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@kevisual/router':
|
||||
specifier: 0.0.33
|
||||
version: 0.0.33
|
||||
'@types/node':
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.1
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
xml2js:
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
devDependencies:
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/xml2js':
|
||||
specifier: ^0.4.14
|
||||
version: 0.4.14
|
||||
|
||||
wxmsg/pack-dist:
|
||||
dependencies:
|
||||
'@kevisual/context':
|
||||
@@ -173,6 +198,34 @@ importers:
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
|
||||
wxmsg/task/worker:
|
||||
dependencies:
|
||||
'@kevisual/context':
|
||||
specifier: ^0.0.4
|
||||
version: 0.0.4
|
||||
'@kevisual/router':
|
||||
specifier: 0.0.33
|
||||
version: 0.0.33
|
||||
'@types/node':
|
||||
specifier: ^24.10.1
|
||||
version: 24.10.1
|
||||
crypto-js:
|
||||
specifier: ^4.2.0
|
||||
version: 4.2.0
|
||||
xml2js:
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2
|
||||
devDependencies:
|
||||
'@types/bun':
|
||||
specifier: ^1.3.3
|
||||
version: 1.3.3
|
||||
'@types/crypto-js':
|
||||
specifier: ^4.2.2
|
||||
version: 4.2.2
|
||||
'@types/xml2js':
|
||||
specifier: ^0.4.14
|
||||
version: 0.4.14
|
||||
|
||||
packages:
|
||||
|
||||
'@ioredis/commands@1.4.0':
|
||||
@@ -243,6 +296,36 @@ packages:
|
||||
resolution: {integrity: sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
|
||||
resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
|
||||
resolution: {integrity: sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
|
||||
resolution: {integrity: sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
|
||||
resolution: {integrity: sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
|
||||
resolution: {integrity: sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
|
||||
resolution: {integrity: sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@noble/hashes@1.8.0':
|
||||
resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==}
|
||||
engines: {node: ^14.21.3 || >=16}
|
||||
@@ -294,6 +377,9 @@ packages:
|
||||
'@types/archiver@7.0.0':
|
||||
resolution: {integrity: sha512-/3vwGwx9n+mCQdYZ2IKGGHEFL30I96UgBlk8EtRDDFQ9uxM1l4O5Ci6r00EMAkiDaTqD9DQ6nVrWRICnBPtzzg==}
|
||||
|
||||
'@types/bun@1.3.3':
|
||||
resolution: {integrity: sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g==}
|
||||
|
||||
'@types/busboy@1.5.4':
|
||||
resolution: {integrity: sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==}
|
||||
|
||||
@@ -348,6 +434,9 @@ packages:
|
||||
'@types/ws@8.18.1':
|
||||
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
|
||||
|
||||
'@types/xml2js@0.4.14':
|
||||
resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
|
||||
|
||||
'@zxing/text-encoding@0.9.0':
|
||||
resolution: {integrity: sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==}
|
||||
|
||||
@@ -481,6 +570,12 @@ packages:
|
||||
buffer@6.0.3:
|
||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||
|
||||
bullmq@5.65.1:
|
||||
resolution: {integrity: sha512-QgDAzX1G9L5IRy4Orva5CfQTXZT+5K+OfO/kbPrAqN+pmL9LJekCzxijXehlm/u2eXfWPfWvIdJJIqiuz3WJSg==}
|
||||
|
||||
bun-types@1.3.3:
|
||||
resolution: {integrity: sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ==}
|
||||
|
||||
busboy@1.6.0:
|
||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||
engines: {node: '>=10.16.0'}
|
||||
@@ -557,6 +652,10 @@ packages:
|
||||
resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
cron-parser@4.9.0:
|
||||
resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
croner@4.1.97:
|
||||
resolution: {integrity: sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==}
|
||||
|
||||
@@ -638,6 +737,10 @@ packages:
|
||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
dezalgo@1.0.4:
|
||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||
|
||||
@@ -1031,6 +1134,10 @@ packages:
|
||||
resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
luxon@3.7.2:
|
||||
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
merge2@1.4.1:
|
||||
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1095,6 +1202,13 @@ packages:
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
msgpackr-extract@3.0.3:
|
||||
resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==}
|
||||
hasBin: true
|
||||
|
||||
msgpackr@1.11.5:
|
||||
resolution: {integrity: sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==}
|
||||
|
||||
mute-stream@0.0.8:
|
||||
resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==}
|
||||
|
||||
@@ -1116,10 +1230,17 @@ packages:
|
||||
resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==}
|
||||
engines: {node: '>= 0.4.0'}
|
||||
|
||||
node-abort-controller@3.1.1:
|
||||
resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==}
|
||||
|
||||
node-forge@1.3.1:
|
||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||
engines: {node: '>= 6.13.0'}
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
||||
hasBin: true
|
||||
|
||||
nodemon@3.1.11:
|
||||
resolution: {integrity: sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1618,11 +1739,6 @@ packages:
|
||||
tx2@1.0.5:
|
||||
resolution: {integrity: sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undefsafe@2.0.5:
|
||||
resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==}
|
||||
|
||||
@@ -1639,6 +1755,10 @@ packages:
|
||||
util@0.12.5:
|
||||
resolution: {integrity: sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==}
|
||||
|
||||
uuid@11.1.0:
|
||||
resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==}
|
||||
hasBin: true
|
||||
|
||||
uuid@8.3.2:
|
||||
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
|
||||
hasBin: true
|
||||
@@ -1892,6 +2012,24 @@ snapshots:
|
||||
|
||||
'@kevisual/ws@8.0.0': {}
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
|
||||
optional: true
|
||||
|
||||
'@noble/hashes@1.8.0': {}
|
||||
|
||||
'@nodelib/fs.scandir@2.1.5':
|
||||
@@ -1973,6 +2111,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/readdir-glob': 1.1.5
|
||||
|
||||
'@types/bun@1.3.3':
|
||||
dependencies:
|
||||
bun-types: 1.3.3
|
||||
|
||||
'@types/busboy@1.5.4':
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
@@ -2032,6 +2174,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@types/xml2js@0.4.14':
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
'@zxing/text-encoding@0.9.0':
|
||||
optional: true
|
||||
|
||||
@@ -2160,6 +2306,22 @@ snapshots:
|
||||
base64-js: 1.5.1
|
||||
ieee754: 1.2.1
|
||||
|
||||
bullmq@5.65.1:
|
||||
dependencies:
|
||||
cron-parser: 4.9.0
|
||||
ioredis: 5.8.2
|
||||
msgpackr: 1.11.5
|
||||
node-abort-controller: 3.1.1
|
||||
semver: 7.7.3
|
||||
tslib: 2.8.1
|
||||
uuid: 11.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
bun-types@1.3.3:
|
||||
dependencies:
|
||||
'@types/node': 24.10.1
|
||||
|
||||
busboy@1.6.0:
|
||||
dependencies:
|
||||
streamsearch: 1.1.0
|
||||
@@ -2237,6 +2399,10 @@ snapshots:
|
||||
crc-32: 1.2.2
|
||||
readable-stream: 4.5.2
|
||||
|
||||
cron-parser@4.9.0:
|
||||
dependencies:
|
||||
luxon: 3.7.2
|
||||
|
||||
croner@4.1.97: {}
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
@@ -2269,6 +2435,10 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.3:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
debug@4.4.3(supports-color@5.5.0):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
@@ -2293,6 +2463,9 @@ snapshots:
|
||||
|
||||
depd@2.0.0: {}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
optional: true
|
||||
|
||||
dezalgo@1.0.4:
|
||||
dependencies:
|
||||
asap: 2.0.6
|
||||
@@ -2685,6 +2858,8 @@ snapshots:
|
||||
|
||||
lru-cache@7.18.3: {}
|
||||
|
||||
luxon@3.7.2: {}
|
||||
|
||||
merge2@1.4.1: {}
|
||||
|
||||
micromatch@4.0.8:
|
||||
@@ -2751,6 +2926,22 @@ snapshots:
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
msgpackr-extract@3.0.3:
|
||||
dependencies:
|
||||
node-gyp-build-optional-packages: 5.2.2
|
||||
optionalDependencies:
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.3
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.3
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.3
|
||||
'@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.3
|
||||
'@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.3
|
||||
'@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3
|
||||
optional: true
|
||||
|
||||
msgpackr@1.11.5:
|
||||
optionalDependencies:
|
||||
msgpackr-extract: 3.0.3
|
||||
|
||||
mute-stream@0.0.8: {}
|
||||
|
||||
nanoid@5.1.6: {}
|
||||
@@ -2767,8 +2958,15 @@ snapshots:
|
||||
|
||||
netmask@2.0.2: {}
|
||||
|
||||
node-abort-controller@3.1.1: {}
|
||||
|
||||
node-forge@1.3.1: {}
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
optional: true
|
||||
|
||||
nodemon@3.1.11:
|
||||
dependencies:
|
||||
chokidar: 3.6.0
|
||||
@@ -3100,7 +3298,7 @@ snapshots:
|
||||
|
||||
send@1.2.0:
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@5.5.0)
|
||||
debug: 4.4.3
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
@@ -3336,8 +3534,6 @@ snapshots:
|
||||
json-stringify-safe: 5.0.1
|
||||
optional: true
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undefsafe@2.0.5: {}
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
@@ -3354,6 +3550,8 @@ snapshots:
|
||||
is-typed-array: 1.1.13
|
||||
which-typed-array: 1.1.15
|
||||
|
||||
uuid@11.1.0: {}
|
||||
|
||||
uuid@8.3.2: {}
|
||||
|
||||
validator@13.12.0: {}
|
||||
|
||||
11
src/auth/index.ts
Normal file
11
src/auth/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
/**
|
||||
* 可以不需要user成功, 有则赋值,交给下一个中间件
|
||||
*/
|
||||
export const authCan = 'auth-can';
|
||||
/**
|
||||
* 必须需要user成功
|
||||
*/
|
||||
export const auth = 'auth';
|
||||
|
||||
export * from './models/index.ts';
|
||||
81
src/auth/middleware/auth-manual.ts
Normal file
81
src/auth/middleware/auth-manual.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { User } from '../models/user.ts';
|
||||
import http from 'node:http';
|
||||
import cookie from 'cookie';
|
||||
export const error = (msg: string, code = 500) => {
|
||||
return JSON.stringify({ code, message: msg });
|
||||
};
|
||||
type CheckAuthOptions = {
|
||||
check401?: boolean; // 是否返回权限信息
|
||||
};
|
||||
/**
|
||||
* 手动验证token,如果token不存在,则返回401
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const checkAuth = async (req: http.IncomingMessage, res: http.ServerResponse, opts?: CheckAuthOptions) => {
|
||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
const check401 = opts?.check401 ?? true; // 是否返回401错误
|
||||
const resNoPermission = () => {
|
||||
res.statusCode = 401;
|
||||
res.end(error('Invalid authorization'));
|
||||
return { tokenUser: null, token: null, hasToken: false };
|
||||
};
|
||||
if (!token) {
|
||||
token = url.searchParams.get('token') || '';
|
||||
}
|
||||
if (!token) {
|
||||
const parsedCookies = cookie.parse(req.headers.cookie || '');
|
||||
token = parsedCookies.token || '';
|
||||
}
|
||||
if (!token && check401) {
|
||||
return resNoPermission();
|
||||
}
|
||||
if (token) {
|
||||
token = token.replace('Bearer ', '');
|
||||
}
|
||||
let tokenUser;
|
||||
const hasToken = !!token; // 是否有token存在
|
||||
|
||||
try {
|
||||
tokenUser = await User.verifyToken(token);
|
||||
} catch (e) {
|
||||
console.log('checkAuth error', e);
|
||||
res.statusCode = 401;
|
||||
res.end(error('Invalid token'));
|
||||
return { tokenUser: null, token: null, hasToken: false };
|
||||
}
|
||||
return { tokenUser, token, hasToken };
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取登录用户,有则获取,无则返回null
|
||||
* @param req
|
||||
* @returns
|
||||
*/
|
||||
export const getLoginUser = async (req: http.IncomingMessage) => {
|
||||
let token = (req.headers?.['authorization'] as string) || (req.headers?.['Authorization'] as string) || '';
|
||||
const url = new URL(req.url || '', 'http://localhost');
|
||||
if (!token) {
|
||||
token = url.searchParams.get('token') || '';
|
||||
}
|
||||
if (!token) {
|
||||
const parsedCookies = cookie.parse(req.headers.cookie || '');
|
||||
token = parsedCookies.token || '';
|
||||
}
|
||||
|
||||
if (token) {
|
||||
token = token.replace('Bearer ', '');
|
||||
}
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
let tokenUser;
|
||||
try {
|
||||
tokenUser = await User.verifyToken(token);
|
||||
return { tokenUser, token };
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
56
src/auth/middleware/auth.ts
Normal file
56
src/auth/middleware/auth.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { User } from '../models/user.ts';
|
||||
import type { App } from '@kevisual/router';
|
||||
|
||||
/**
|
||||
* 添加auth中间件, 用于验证token
|
||||
* 添加 id: auth 必须需要user成功
|
||||
* 添加 id: auth-can 可以不需要user成功,有则赋值
|
||||
*
|
||||
* @param app
|
||||
*/
|
||||
export const addAuth = (app: App) => {
|
||||
app
|
||||
.route({
|
||||
path: 'auth',
|
||||
id: 'auth',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const token = ctx.query.token;
|
||||
if (!token) {
|
||||
app.throw(401, 'Token is required');
|
||||
}
|
||||
const user = await User.getOauthUser(token);
|
||||
if (!user) {
|
||||
app.throw(401, 'Token is invalid');
|
||||
}
|
||||
if (ctx.state) {
|
||||
ctx.state.tokenUser = user;
|
||||
} else {
|
||||
ctx.state = {
|
||||
tokenUser: user,
|
||||
};
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'auth',
|
||||
key: 'can',
|
||||
id: 'auth-can',
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
if (ctx.query?.token) {
|
||||
const token = ctx.query.token;
|
||||
const user = await User.getOauthUser(token);
|
||||
if (ctx.state) {
|
||||
ctx.state.tokenUser = user;
|
||||
} else {
|
||||
ctx.state = {
|
||||
tokenUser: user,
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
};
|
||||
3
src/auth/models/index.ts
Normal file
3
src/auth/models/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { User, UserInit, UserServices, UserModel } from './user.ts';
|
||||
export { UserSecretInit, UserSecret } from './user-secret.ts';
|
||||
export { OrgInit, Org } from './org.ts';
|
||||
184
src/auth/models/org.ts
Normal file
184
src/auth/models/org.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import { DataTypes, Model, Op, Sequelize } from 'sequelize';
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { SyncOpts, User } from './user.ts';
|
||||
|
||||
type AddUserOpts = {
|
||||
role: string;
|
||||
};
|
||||
export enum OrgRole {
|
||||
admin = 'admin',
|
||||
member = 'member',
|
||||
owner = 'owner',
|
||||
}
|
||||
export class Org extends Model {
|
||||
declare id: string;
|
||||
declare username: string;
|
||||
declare description: string;
|
||||
declare users: { role: string; uid: string }[];
|
||||
/**
|
||||
* operateId 是真实操作者的id
|
||||
* @param user
|
||||
* @param opts
|
||||
*/
|
||||
async addUser(user: User, opts?: { operateId?: string; role: string; needPermission?: boolean; isAdmin?: boolean }) {
|
||||
const hasUser = this.users.find((u) => u.uid === user.id);
|
||||
if (hasUser) {
|
||||
return;
|
||||
}
|
||||
if (user.type !== 'user') {
|
||||
throw Error('Only user can be added to org');
|
||||
}
|
||||
if (opts?.needPermission) {
|
||||
if (opts?.isAdmin) {
|
||||
} else {
|
||||
const adminUsers = this.users.filter((u) => u.role === 'admin' || u.role === 'owner');
|
||||
const adminIds = adminUsers.map((u) => u.uid);
|
||||
const hasPermission = adminIds.includes(opts.operateId);
|
||||
if (!hasPermission) {
|
||||
throw Error('No permission');
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
await user.expireOrgs();
|
||||
} catch (e) {
|
||||
console.error('expireOrgs', e);
|
||||
}
|
||||
const users = [...this.users];
|
||||
if (opts?.role === 'owner') {
|
||||
const orgOwner = users.find((u) => u.role === 'owner');
|
||||
if (opts.isAdmin) {
|
||||
} else {
|
||||
if (!opts.operateId) {
|
||||
throw Error('operateId is required');
|
||||
}
|
||||
const owner = await User.findByPk(opts?.operateId);
|
||||
if (!owner) {
|
||||
throw Error('operateId is not found');
|
||||
}
|
||||
if (orgOwner?.uid !== owner.id) {
|
||||
throw Error('No permission');
|
||||
}
|
||||
}
|
||||
if (orgOwner) {
|
||||
orgOwner.role = 'admin';
|
||||
}
|
||||
users.push({ role: 'owner', uid: user.id });
|
||||
} else {
|
||||
users.push({ role: opts?.role || 'member', uid: user.id });
|
||||
}
|
||||
await Org.update({ users }, { where: { id: this.id } });
|
||||
|
||||
}
|
||||
/**
|
||||
* operateId 是真实操作者的id
|
||||
* @param user
|
||||
* @param opts
|
||||
*/
|
||||
async removeUser(user: User, opts?: { operateId?: string; needPermission?: boolean; isAdmin?: boolean }) {
|
||||
if (opts?.needPermission) {
|
||||
if (opts?.isAdmin) {
|
||||
} else {
|
||||
const adminUsers = this.users.filter((u) => u.role === 'admin' || u.role === 'owner');
|
||||
const adminIds = adminUsers.map((u) => u.uid);
|
||||
const hasPermission = adminIds.includes(opts.operateId);
|
||||
if (!hasPermission) {
|
||||
throw Error('No permission');
|
||||
}
|
||||
}
|
||||
}
|
||||
await user.expireOrgs();
|
||||
const users = this.users.filter((u) => u.uid !== user.id || u.role === 'owner');
|
||||
await Org.update({ users }, { where: { id: this.id } });
|
||||
}
|
||||
/**
|
||||
* operateId 是真实操作者的id
|
||||
* @param user
|
||||
* @param opts
|
||||
*/
|
||||
async getUsers(opts?: { operateId: string; needPermission?: boolean; isAdmin?: boolean }) {
|
||||
const usersIds = this.users.map((u) => u.uid);
|
||||
const orgUser = this.users;
|
||||
if (opts?.needPermission) {
|
||||
// 不在组织内或者不是管理员,如果需要权限,返回空
|
||||
if (opts.isAdmin) {
|
||||
} else {
|
||||
const hasPermission = usersIds.includes(opts.operateId);
|
||||
if (!hasPermission) {
|
||||
return {
|
||||
hasPermission: false,
|
||||
users: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
const _users = await User.findAll({
|
||||
where: {
|
||||
id: {
|
||||
[Op.in]: usersIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const users = _users.map((u) => {
|
||||
const role = orgUser.find((r) => r.uid === u.id)?.role;
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
role: role,
|
||||
};
|
||||
});
|
||||
return { users };
|
||||
}
|
||||
/**
|
||||
* 检测用户是否在组织内,且角色为role
|
||||
* @param user
|
||||
* @param opts
|
||||
*/
|
||||
async getInRole(userId: string, role = 'admin') {
|
||||
const user = this.users.find((u) => u.uid === userId && u.role === role);
|
||||
return !!user;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 组织模型,在sequelize之后初始化
|
||||
*/
|
||||
export const OrgInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
|
||||
const sequelize = useContextKey<Sequelize>('sequelize');
|
||||
Org.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
users: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
defaultValue: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize: newSequelize || sequelize,
|
||||
modelName: tableName || 'cf_org',
|
||||
paranoid: true,
|
||||
},
|
||||
);
|
||||
if (sync) {
|
||||
await Org.sync({ alter: true, logging: false, ...sync }).catch((e) => {
|
||||
console.error('Org sync', e);
|
||||
});
|
||||
return Org;
|
||||
}
|
||||
return Org;
|
||||
};
|
||||
export const OrgModel = useContextKey('OrgModel', () => Org);
|
||||
261
src/auth/models/user-secret.ts
Normal file
261
src/auth/models/user-secret.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { Redis } from 'ioredis';
|
||||
import { SyncOpts, User } from './user.ts';
|
||||
import { oauth } from '../oauth/auth.ts';
|
||||
import { OauthUser } from '../oauth/oauth.ts';
|
||||
export const redis = useContextKey<Redis>('redis');
|
||||
|
||||
const UserSecretStatus = ['active', 'inactive', 'expired'] as const;
|
||||
|
||||
type Data = {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 微信开放平台的某一个应用的openid
|
||||
*/
|
||||
wxOpenid?: string;
|
||||
/**
|
||||
* 微信开放平台的unionid:主要
|
||||
*/
|
||||
wxUnionid?: string;
|
||||
/**
|
||||
* 微信公众号的openid:次要
|
||||
*/
|
||||
wxmpOpenid?: string;
|
||||
|
||||
}
|
||||
export class UserSecret extends Model {
|
||||
static oauth = oauth;
|
||||
declare id: string;
|
||||
declare token: string;
|
||||
declare userId: string;
|
||||
declare orgId: string;
|
||||
declare title: string;
|
||||
declare description: string;
|
||||
declare status: (typeof UserSecretStatus)[number];
|
||||
declare expiredTime: Date;
|
||||
declare data: Data;
|
||||
/**
|
||||
* 验证token
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
static async verifyToken(token: string) {
|
||||
if (!oauth.isSecretKey(token)) {
|
||||
return await oauth.verifyToken(token);
|
||||
}
|
||||
// const secretToken = await oauth.verifyToken(token);
|
||||
// if (secretToken) {
|
||||
// return secretToken;
|
||||
// }
|
||||
const userSecret = await UserSecret.findOne({
|
||||
where: { token },
|
||||
});
|
||||
if (!userSecret) {
|
||||
return null; // 如果没有找到对应的用户密钥,则返回null
|
||||
}
|
||||
if (userSecret.isExpired()) {
|
||||
return null; // 如果用户密钥已过期,则返回null
|
||||
}
|
||||
if (userSecret.status !== 'active') {
|
||||
return null; // 如果用户密钥状态不是active,则返回null
|
||||
}
|
||||
// 如果用户密钥未过期,则返回用户信息
|
||||
const oauthUser = await userSecret.getOauthUser();
|
||||
if (!oauthUser) {
|
||||
return null; // 如果没有找到对应的oauth用户,则返回null
|
||||
}
|
||||
// await oauth.saveSecretKey(oauthUser, userSecret.token);
|
||||
// 存储到oauth中的token store中
|
||||
return oauthUser;
|
||||
}
|
||||
/**
|
||||
* owner 组织用户的 oauthUser
|
||||
* @returns
|
||||
*/
|
||||
async getOauthUser() {
|
||||
const user = await User.findOne({
|
||||
where: { id: this.userId },
|
||||
attributes: ['id', 'username', 'type', 'owner'],
|
||||
});
|
||||
let org: User = null;
|
||||
if (!user) {
|
||||
return null; // 如果没有找到对应的用户,则返回null
|
||||
}
|
||||
const expiredTime = this.expiredTime ? new Date(this.expiredTime).getTime() : null;
|
||||
const oauthUser: Partial<OauthUser> = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
type: 'user',
|
||||
oauthExpand: {
|
||||
expiredTime: expiredTime,
|
||||
},
|
||||
};
|
||||
if (this.orgId) {
|
||||
org = await User.findOne({
|
||||
where: { id: this.orgId },
|
||||
attributes: ['id', 'username', 'type', 'owner'],
|
||||
});
|
||||
if (org) {
|
||||
oauthUser.id = org.id;
|
||||
oauthUser.username = org.username;
|
||||
oauthUser.type = 'org';
|
||||
oauthUser.uid = user.id;
|
||||
} else {
|
||||
console.warn(`getOauthUser: org not found for orgId ${this.orgId}`);
|
||||
}
|
||||
}
|
||||
|
||||
return oauth.getOauthUser(oauthUser);
|
||||
}
|
||||
isExpired() {
|
||||
if (!this.expiredTime) {
|
||||
return false; // 没有设置过期时间
|
||||
}
|
||||
const now = Date.now();
|
||||
const expiredTime = new Date(this.expiredTime);
|
||||
return now > expiredTime.getTime(); // 如果当前时间大于过期时间,则认为已过期
|
||||
}
|
||||
async createNewToken() {
|
||||
if (this.token) {
|
||||
await oauth.delToken(this.token);
|
||||
}
|
||||
const token = await UserSecret.createToken();
|
||||
this.token = token;
|
||||
await this.save();
|
||||
return token;
|
||||
}
|
||||
static async createToken() {
|
||||
let token = oauth.generateSecretKey();
|
||||
// 确保生成的token是唯一的
|
||||
while (await UserSecret.findOne({ where: { token } })) {
|
||||
token = oauth.generateSecretKey();
|
||||
}
|
||||
return token;
|
||||
}
|
||||
static async createSecret(tokenUser: { id: string; uid?: string }, expireDay = 365) {
|
||||
const expireTime = expireDay * 24 * 60 * 60 * 1000; // 转换为毫秒
|
||||
const token = await UserSecret.createToken();
|
||||
let userId = tokenUser.id;
|
||||
let orgId: string = null;
|
||||
if (tokenUser.uid) {
|
||||
userId = tokenUser.uid;
|
||||
orgId = tokenUser.id; // 如果是组织用户,则uid是组织ID
|
||||
}
|
||||
const userSecret = await UserSecret.create({
|
||||
userId,
|
||||
orgId,
|
||||
token,
|
||||
expiredTime: new Date(Date.now() + expireTime),
|
||||
});
|
||||
|
||||
return userSecret;
|
||||
}
|
||||
async getPermission(opts: { id: string; uid?: string }) {
|
||||
const { id, uid } = opts;
|
||||
let userId: string = id;
|
||||
let hasPermission = false;
|
||||
let isUser = false;
|
||||
let isAdmin: boolean = null;
|
||||
if (uid) {
|
||||
userId = uid;
|
||||
}
|
||||
if (!id) {
|
||||
return {
|
||||
hasPermission,
|
||||
isUser,
|
||||
isAdmin,
|
||||
};
|
||||
}
|
||||
if (this.userId === userId) {
|
||||
hasPermission = true;
|
||||
isUser = true;
|
||||
}
|
||||
|
||||
if (hasPermission) {
|
||||
return {
|
||||
hasPermission,
|
||||
isUser,
|
||||
isAdmin,
|
||||
};
|
||||
}
|
||||
if (this.orgId) {
|
||||
const orgUser = await User.findByPk(this.orgId);
|
||||
if (orgUser && orgUser.owner === userId) {
|
||||
isAdmin = true;
|
||||
hasPermission = true;
|
||||
}
|
||||
}
|
||||
return {
|
||||
hasPermission,
|
||||
isUser,
|
||||
isAdmin,
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 组织模型,在sequelize之后初始化
|
||||
*/
|
||||
export const UserSecretInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
|
||||
const sequelize = useContextKey<Sequelize>('sequelize');
|
||||
UserSecret.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
defaultValue: 'active',
|
||||
comment: '状态',
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
expiredTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
token: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '用户密钥',
|
||||
defaultValue: '',
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
defaultValue: {},
|
||||
},
|
||||
orgId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
comment: '组织ID',
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize: newSequelize || sequelize,
|
||||
modelName: tableName || 'cf_user_secret',
|
||||
},
|
||||
);
|
||||
if (sync) {
|
||||
await UserSecret.sync({ alter: true, logging: false, ...sync }).catch((e) => {
|
||||
console.error('UserSecret sync', e);
|
||||
});
|
||||
return UserSecret;
|
||||
}
|
||||
return UserSecret;
|
||||
};
|
||||
export const UserSecretModel = useContextKey('UserSecretModel', () => UserSecret);
|
||||
370
src/auth/models/user.ts
Normal file
370
src/auth/models/user.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
import { DataTypes, Model, Op, Sequelize } from 'sequelize';
|
||||
import { nanoid, customAlphabet } from 'nanoid';
|
||||
import { CustomError } from '@kevisual/router';
|
||||
import { Org } from './org.ts';
|
||||
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { Redis } from 'ioredis';
|
||||
import { oauth } from '../oauth/auth.ts';
|
||||
import { cryptPwd } from '../oauth/salt.ts';
|
||||
import { OauthUser } from '../oauth/oauth.ts';
|
||||
export const redis = useContextKey<Redis>('redis');
|
||||
import { UserSecret } from './user-secret.ts';
|
||||
type UserData = {
|
||||
orgs?: string[];
|
||||
wxUnionId?: string;
|
||||
phone?: string;
|
||||
};
|
||||
export enum UserTypes {
|
||||
'user' = 'user',
|
||||
'org' = 'org',
|
||||
'visitor' = 'visitor',
|
||||
}
|
||||
/**
|
||||
* 用户模型,在sequelize和Org之后初始化
|
||||
*/
|
||||
export class User extends Model {
|
||||
static oauth = oauth;
|
||||
declare id: string;
|
||||
declare username: string;
|
||||
declare nickname: string; // 昵称
|
||||
declare password: string;
|
||||
declare salt: string;
|
||||
declare needChangePassword: boolean;
|
||||
declare description: string;
|
||||
declare data: UserData;
|
||||
declare type: string; // user | org | visitor
|
||||
declare owner: string;
|
||||
declare orgId: string;
|
||||
declare email: string;
|
||||
declare avatar: string;
|
||||
tokenUser: any;
|
||||
setTokenUser(tokenUser: any) {
|
||||
this.tokenUser = tokenUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* uid 是用于 orgId 的用户id, 如果uid存在,则表示是用户是组织,其中uid为真实用户
|
||||
* @param uid
|
||||
* @returns
|
||||
*/
|
||||
async createToken(uid?: string, loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week', expand: any = {}) {
|
||||
const { id, username, type } = this;
|
||||
const oauthUser: OauthUser = {
|
||||
id,
|
||||
username,
|
||||
uid,
|
||||
userId: uid || id, // 必存在,真实用户id
|
||||
type: type as 'user' | 'org',
|
||||
};
|
||||
if (uid) {
|
||||
oauthUser.orgId = id;
|
||||
}
|
||||
const token = await oauth.generateToken(oauthUser, { type: loginType, hasRefreshToken: true, ...expand });
|
||||
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
|
||||
}
|
||||
/**
|
||||
* 验证token
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
static async verifyToken(token: string) {
|
||||
return await UserSecret.verifyToken(token);
|
||||
}
|
||||
/**
|
||||
* 刷新token
|
||||
* @param refreshToken
|
||||
* @returns
|
||||
*/
|
||||
static async refreshToken(refreshToken: string) {
|
||||
const token = await oauth.refreshToken(refreshToken);
|
||||
return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken };
|
||||
}
|
||||
static async getOauthUser(token: string) {
|
||||
return await UserSecret.verifyToken(token);
|
||||
}
|
||||
/**
|
||||
* 清理用户的token,需要重新登陆
|
||||
* @param userid
|
||||
* @param orgid
|
||||
* @returns
|
||||
*/
|
||||
static async clearUserToken(userid: string, type: 'org' | 'user' = 'user') {
|
||||
return await oauth.expireUserTokens(userid, type);
|
||||
}
|
||||
/**
|
||||
* 获取用户信息, 并设置tokenUser
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
static async getUserByToken(token: string) {
|
||||
const oauthUser = await UserSecret.verifyToken(token);
|
||||
if (!oauthUser) {
|
||||
throw new CustomError('Token is invalid. get UserByToken');
|
||||
}
|
||||
const userId = oauthUser?.uid || oauthUser.id;
|
||||
const user = await User.findByPk(userId);
|
||||
user.setTokenUser(oauthUser);
|
||||
return user;
|
||||
}
|
||||
/**
|
||||
* 判断是否在用户列表中, 需要预先设置 tokenUser
|
||||
* orgs has set curentUser
|
||||
* @param username
|
||||
* @param includeMe
|
||||
* @returns
|
||||
*/
|
||||
async hasUser(username: string, includeMe = true) {
|
||||
const orgs = await this.getOrgs();
|
||||
const me = this.username;
|
||||
const allUsers = [...orgs];
|
||||
if (includeMe) {
|
||||
allUsers.push(me);
|
||||
}
|
||||
return allUsers.includes(username);
|
||||
}
|
||||
static async createUser(username: string, password?: string, description?: string) {
|
||||
const user = await User.findOne({ where: { username } });
|
||||
if (user) {
|
||||
throw new CustomError('User already exists');
|
||||
}
|
||||
const salt = nanoid(6);
|
||||
let needChangePassword = !password;
|
||||
password = password || '123456';
|
||||
const cPassword = cryptPwd(password, salt);
|
||||
return await User.create({ username, password: cPassword, description, salt, needChangePassword });
|
||||
}
|
||||
static async createOrg(username: string, owner: string, description?: string) {
|
||||
const user = await User.findOne({ where: { username } });
|
||||
if (user) {
|
||||
throw new CustomError('User already exists');
|
||||
}
|
||||
const me = await User.findByPk(owner);
|
||||
if (!me) {
|
||||
throw new CustomError('Owner not found');
|
||||
}
|
||||
if (me.type !== 'user') {
|
||||
throw new CustomError('Owner type is not user');
|
||||
}
|
||||
const org = await Org.create({ username, description, users: [{ uid: owner, role: 'owner' }] });
|
||||
const newUser = await User.create({ username, password: '', description, type: 'org', owner, orgId: org.id });
|
||||
// owner add
|
||||
await redis.del(`user:${me.id}:orgs`);
|
||||
return newUser;
|
||||
}
|
||||
async createPassword(password: string) {
|
||||
const salt = this.salt;
|
||||
const cPassword = cryptPwd(password, salt);
|
||||
this.password = cPassword;
|
||||
await this.update({ password: cPassword });
|
||||
return cPassword;
|
||||
}
|
||||
checkPassword(password: string) {
|
||||
const salt = this.salt;
|
||||
const cPassword = cryptPwd(password, salt);
|
||||
return this.password === cPassword;
|
||||
}
|
||||
/**
|
||||
* 获取用户信息, 需要先设置 tokenUser 或者设置 uid
|
||||
* @param uid 如果存在,则表示是组织,其中uid为真实用户
|
||||
* @returns
|
||||
*/
|
||||
async getInfo(uid?: string) {
|
||||
const orgs = await this.getOrgs();
|
||||
|
||||
const info: Record<string, any> = {
|
||||
id: this.id,
|
||||
username: this.username,
|
||||
nickname: this.nickname,
|
||||
description: this.description,
|
||||
needChangePassword: this.needChangePassword,
|
||||
type: this.type,
|
||||
avatar: this.avatar,
|
||||
orgs,
|
||||
};
|
||||
const tokenUser = this.tokenUser;
|
||||
if (uid) {
|
||||
info.uid = uid;
|
||||
} else if (tokenUser.uid) {
|
||||
info.uid = tokenUser.uid;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
/**
|
||||
* 获取用户组织
|
||||
* @returns
|
||||
*/
|
||||
async getOrgs() {
|
||||
let id = this.id;
|
||||
if (this.type === 'org') {
|
||||
if (this.tokenUser && this.tokenUser.uid) {
|
||||
id = this.tokenUser.uid;
|
||||
} else {
|
||||
throw new CustomError(400, 'Permission denied');
|
||||
}
|
||||
}
|
||||
const cache = await redis.get(`user:${id}:orgs`);
|
||||
if (cache) {
|
||||
return JSON.parse(cache) as string[];
|
||||
}
|
||||
const orgs = await Org.findAll({
|
||||
order: [['updatedAt', 'DESC']],
|
||||
where: {
|
||||
users: {
|
||||
[Op.contains]: [
|
||||
{
|
||||
uid: id,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const orgNames = orgs.map((org) => org.username);
|
||||
if (orgNames.length > 0) {
|
||||
await redis.set(`user:${id}:orgs`, JSON.stringify(orgNames), 'EX', 60 * 60); // 1 hour
|
||||
}
|
||||
return orgNames;
|
||||
}
|
||||
async expireOrgs() {
|
||||
await redis.del(`user:${this.id}:orgs`);
|
||||
}
|
||||
}
|
||||
export type SyncOpts = {
|
||||
alter?: boolean;
|
||||
logging?: any;
|
||||
force?: boolean;
|
||||
};
|
||||
export const UserInit = async (newSequelize?: any, tableName?: string, sync?: SyncOpts) => {
|
||||
const sequelize = useContextKey<Sequelize>('sequelize');
|
||||
User.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
// 用户名或者手机号
|
||||
// 创建后避免修改的字段,当注册用户后,用户名注册则默认不能用手机号
|
||||
},
|
||||
nickname: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
email: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
avatar: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
salt: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
},
|
||||
type: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: 'user',
|
||||
},
|
||||
owner: {
|
||||
type: DataTypes.UUID,
|
||||
},
|
||||
orgId: {
|
||||
type: DataTypes.UUID,
|
||||
},
|
||||
needChangePassword: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
sequelize: newSequelize || sequelize,
|
||||
tableName: tableName || 'cf_user', // codeflow user
|
||||
paranoid: true,
|
||||
},
|
||||
);
|
||||
if (sync) {
|
||||
await User.sync({ alter: true, logging: true, ...sync })
|
||||
.then((res) => {
|
||||
initializeUser();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Sync User error', err);
|
||||
});
|
||||
return User;
|
||||
}
|
||||
return User;
|
||||
};
|
||||
const letter = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const custom = customAlphabet(letter, 6);
|
||||
export const initializeUser = async (pwd = custom()) => {
|
||||
const w = await User.findOne({ where: { username: 'root' }, logging: false });
|
||||
if (!w) {
|
||||
const root = await User.createUser('root', pwd, '系统管理员');
|
||||
const org = await User.createOrg('admin', root.id, '管理员');
|
||||
console.info(' new Users name', root.username, org.username);
|
||||
console.info('new Users root password', pwd);
|
||||
console.info('new Users id', root.id, org.id);
|
||||
const demo = await createDemoUser();
|
||||
return {
|
||||
code: 200,
|
||||
data: { root, org, pwd: pwd, demo },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 500,
|
||||
message: 'Users has been created',
|
||||
};
|
||||
}
|
||||
};
|
||||
export const createDemoUser = async (username = 'demo', pwd = custom()) => {
|
||||
const u = await User.findOne({ where: { username }, logging: false });
|
||||
if (!u) {
|
||||
const user = await User.createUser(username, pwd, 'demo');
|
||||
console.info('new Users name', user.username, pwd);
|
||||
return {
|
||||
code: 200,
|
||||
data: { user, pwd: pwd },
|
||||
};
|
||||
} else {
|
||||
console.info('Users has been created', u.username);
|
||||
return {
|
||||
code: 500,
|
||||
message: 'Users has been created',
|
||||
};
|
||||
}
|
||||
};
|
||||
// initializeUser();
|
||||
|
||||
export class UserServices extends User {
|
||||
static async loginByPhone(phone: string) {
|
||||
let user = await User.findOne({ where: { username: phone } });
|
||||
let isNew = false;
|
||||
if (!user) {
|
||||
user = await User.createUser(phone, phone.slice(-6));
|
||||
isNew = true;
|
||||
}
|
||||
const token = await user.createToken(null, 'season');
|
||||
return { ...token, isNew };
|
||||
}
|
||||
static initializeUser = initializeUser;
|
||||
static createDemoUser = createDemoUser;
|
||||
}
|
||||
|
||||
export const UserModel = useContextKey('UserModel', () => UserServices);
|
||||
18
src/auth/oauth/auth.ts
Normal file
18
src/auth/oauth/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { OAuth, RedisTokenStore } from './oauth.ts';
|
||||
import { useContextKey } from '@kevisual/use-config/context';
|
||||
import { Redis } from 'ioredis';
|
||||
|
||||
export const oauth = useContextKey('oauth', () => {
|
||||
const redis = useContextKey<Redis>('redis');
|
||||
const store = new RedisTokenStore(redis);
|
||||
// redis是promise
|
||||
if (redis instanceof Promise) {
|
||||
redis.then((r) => {
|
||||
store.setRedis(r);
|
||||
});
|
||||
} else if (redis) {
|
||||
store.setRedis(redis);
|
||||
}
|
||||
const oauth = new OAuth(store);
|
||||
return oauth;
|
||||
});
|
||||
2
src/auth/oauth/index.ts
Normal file
2
src/auth/oauth/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './oauth.ts';
|
||||
export * from './salt.ts';
|
||||
392
src/auth/oauth/oauth.ts
Normal file
392
src/auth/oauth/oauth.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
/**
|
||||
* 一个生成和验证token的模块,不使用jwt,使用redis缓存,
|
||||
* token 分为两种,一种是access_token,一种是refresh_token
|
||||
*
|
||||
* access_token 用于验证用户是否登录,过期时间为1小时
|
||||
* refresh_token 用于刷新access_token,过期时间为7天
|
||||
*
|
||||
* 生成token时,会根据用户信息生成一个access_token和refresh_token,并缓存到redis中
|
||||
* 验证token时,会根据token从redis中获取用户信息
|
||||
* 刷新token时,会根据refresh_token生成一个新的access_token和refresh_token,并缓存到redis中
|
||||
*
|
||||
* 并删除旧的access_token和refresh_token
|
||||
*
|
||||
* 生成token的方法,使用nanoid生成一个随机字符串
|
||||
* 验证token的方法,使用redis的get方法验证token是否存在
|
||||
*
|
||||
* 刷新token的方法,使用redis的set方法刷新token
|
||||
*
|
||||
* 缓存和获取都可以不使用redis,只是用可拓展的接口。store.get和store.set去实现。
|
||||
*/
|
||||
|
||||
import { Redis } from 'ioredis';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
export const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
export const randomId16 = customAlphabet(alphabet, 16);
|
||||
export const randomId24 = customAlphabet(alphabet, 24);
|
||||
export const randomId32 = customAlphabet(alphabet, 32);
|
||||
export const randomId64 = customAlphabet(alphabet, 64);
|
||||
|
||||
export type OauthUser = {
|
||||
/**
|
||||
* 真实用户,非org
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* 组织id,非必须存在
|
||||
*/
|
||||
orgId?: string;
|
||||
/**
|
||||
* 必存在,真实用户id
|
||||
*/
|
||||
userId: string;
|
||||
/**
|
||||
* 当前用户的id,如果是org,则uid为org的id
|
||||
*/
|
||||
uid?: string;
|
||||
username: string;
|
||||
type?: 'user' | 'org'; // 用户类型,默认是user,token类型是用于token的扩展
|
||||
oauthType?: 'user' | 'token'; // 用户类型,默认是user,token类型是用于token的扩展
|
||||
oauthExpand?: UserExpand;
|
||||
};
|
||||
export type UserExpand = {
|
||||
createTime?: number;
|
||||
accessToken?: string;
|
||||
refreshToken?: string;
|
||||
[key: string]: any;
|
||||
} & StoreSetOpts;
|
||||
|
||||
type StoreSetOpts = {
|
||||
loginType?: 'default' | 'plugin' | 'month' | 'season' | 'year' | 'week' | 'day'; // 登陆类型 'default' | 'plugin' | 'month' | 'season' | 'year'
|
||||
expire?: number; // 过期时间,单位为秒
|
||||
hasRefreshToken?: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
interface Store<T> {
|
||||
redis?: Redis;
|
||||
getObject: (key: string) => Promise<T>;
|
||||
setObject: (key: string, value: T, opts?: StoreSetOpts) => Promise<void>;
|
||||
expire: (key: string, ttl?: number) => Promise<void>;
|
||||
delObject: (value?: T) => Promise<void>;
|
||||
keys: (key?: string) => Promise<string[]>;
|
||||
setToken: (value: { accessToken: string; refreshToken: string; value?: T }, opts?: StoreSetOpts) => Promise<void>;
|
||||
delKeys: (keys: string[]) => Promise<number>;
|
||||
}
|
||||
export class RedisTokenStore implements Store<OauthUser> {
|
||||
redis: Redis;
|
||||
private prefix: string = 'oauth:';
|
||||
constructor(redis?: Redis, prefix?: string) {
|
||||
this.redis = redis;
|
||||
this.prefix = prefix || this.prefix;
|
||||
}
|
||||
async setRedis(redis: Redis) {
|
||||
this.redis = redis;
|
||||
}
|
||||
async set(key: string, value: string, ttl?: number) {
|
||||
await this.redis.set(this.prefix + key, value, 'EX', ttl);
|
||||
}
|
||||
async get(key: string) {
|
||||
return await this.redis.get(this.prefix + key);
|
||||
}
|
||||
async expire(key: string, ttl?: number) {
|
||||
await this.redis.expire(this.prefix + key, ttl);
|
||||
}
|
||||
async keys(key?: string) {
|
||||
return await this.redis.keys(this.prefix + key);
|
||||
}
|
||||
async getObject(key: string) {
|
||||
try {
|
||||
const value = await this.get(key);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(value);
|
||||
} catch (error) {
|
||||
console.log('get key parse error', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
async del(key: string) {
|
||||
const number = await this.redis.del(this.prefix + key);
|
||||
return number;
|
||||
}
|
||||
async setObject(key: string, value: OauthUser, opts?: StoreSetOpts) {
|
||||
await this.set(key, JSON.stringify(value), opts?.expire);
|
||||
}
|
||||
async delObject(value?: OauthUser) {
|
||||
const refreshToken = value?.oauthExpand?.refreshToken;
|
||||
const accessToken = value?.oauthExpand?.accessToken;
|
||||
// 清理userPerfix
|
||||
let userPrefix = 'user:' + value?.id;
|
||||
if (value?.orgId) {
|
||||
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
|
||||
}
|
||||
if (refreshToken) {
|
||||
await this.del(refreshToken);
|
||||
await this.del(userPrefix + ':refreshToken:' + refreshToken);
|
||||
}
|
||||
if (accessToken) {
|
||||
await this.del(accessToken);
|
||||
await this.del(userPrefix + ':token:' + accessToken);
|
||||
}
|
||||
}
|
||||
async setToken(data: { accessToken: string; refreshToken: string; value?: OauthUser }, opts?: StoreSetOpts) {
|
||||
const { accessToken, refreshToken, value } = data;
|
||||
let userPrefix = 'user:' + value?.id;
|
||||
if (value?.orgId) {
|
||||
userPrefix = 'org:' + value?.orgId + ':user:' + value?.id;
|
||||
}
|
||||
// 计算过期时间,根据opts.expire 和 opts.loginType
|
||||
// 如果expire存在,则使用expire,否则使用opts.loginType 进行计算;
|
||||
let expire = opts?.expire;
|
||||
if (!expire) {
|
||||
switch (opts.loginType) {
|
||||
case 'day':
|
||||
expire = 24 * 60 * 60;
|
||||
break;
|
||||
case 'week':
|
||||
expire = 7 * 24 * 60 * 60;
|
||||
break;
|
||||
case 'month':
|
||||
expire = 30 * 24 * 60 * 60;
|
||||
break;
|
||||
case 'season':
|
||||
expire = 90 * 24 * 60 * 60;
|
||||
break;
|
||||
default:
|
||||
expire = 7 * 24 * 60 * 60; // 默认过期时间为7天
|
||||
}
|
||||
} else {
|
||||
expire = Math.min(expire, 60 * 60 * 24 * 30, 60 * 60 * 24 * 90); // 默认的过期时间最大为90天
|
||||
}
|
||||
|
||||
await this.set(accessToken, JSON.stringify(value), expire);
|
||||
await this.set(userPrefix + ':token:' + accessToken, accessToken, expire);
|
||||
if (refreshToken) {
|
||||
let refreshTokenExpire = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年
|
||||
// 小于7天, 则设置为7天
|
||||
if (refreshTokenExpire < 60 * 60 * 24 * 7) {
|
||||
refreshTokenExpire = 60 * 60 * 24 * 7;
|
||||
}
|
||||
await this.set(refreshToken, JSON.stringify(value), refreshTokenExpire);
|
||||
await this.set(userPrefix + ':refreshToken:' + refreshToken, refreshToken, refreshTokenExpire);
|
||||
}
|
||||
}
|
||||
async delKeys(keys: string[]) {
|
||||
const prefix = this.prefix;
|
||||
const number = await this.redis.del(keys.map((key) => prefix + key));
|
||||
return number;
|
||||
}
|
||||
}
|
||||
|
||||
export class OAuth<T extends OauthUser> {
|
||||
private store: Store<T>;
|
||||
|
||||
constructor(store: Store<T>) {
|
||||
this.store = store;
|
||||
}
|
||||
generateSecretKey(sk = true) {
|
||||
if (sk) {
|
||||
return 'sk_' + randomId64();
|
||||
}
|
||||
return 'st_' + randomId32();
|
||||
}
|
||||
/**
|
||||
* 生成token
|
||||
* @param user
|
||||
* @param user.id 访问者id
|
||||
* @param user.uid 如果是org,这个是真实用户id,id是orgId
|
||||
* @param user.userId 真实用户id
|
||||
* @param user.orgId 组织id,可选
|
||||
* @param user.username
|
||||
* @param user.type
|
||||
* @returns
|
||||
*/
|
||||
async generateToken(
|
||||
user: T,
|
||||
expandOpts?: StoreSetOpts,
|
||||
): Promise<{
|
||||
accessToken: string;
|
||||
refreshToken?: string;
|
||||
}> {
|
||||
// 拥有refreshToken 为 true 时,accessToken 为 st_ 开头,refreshToken 为 rk_开头
|
||||
// 意思是secretToken 和 secretKey的缩写
|
||||
const accessToken = expandOpts?.hasRefreshToken ? 'st_' + randomId32() : 'sk_' + randomId64();
|
||||
const refreshToken = expandOpts?.hasRefreshToken ? 'rk_' + randomId64() : null;
|
||||
// 初始化 appExpand
|
||||
user.oauthExpand = user.oauthExpand || {};
|
||||
if (expandOpts) {
|
||||
user.oauthExpand = {
|
||||
...user.oauthExpand,
|
||||
...expandOpts,
|
||||
accessToken,
|
||||
createTime: new Date().getTime(), //
|
||||
};
|
||||
if (expandOpts?.hasRefreshToken) {
|
||||
user.oauthExpand.refreshToken = refreshToken;
|
||||
}
|
||||
}
|
||||
await this.store.setToken({ accessToken, refreshToken, value: user }, expandOpts);
|
||||
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
async saveSecretKey(oauthUser: T, secretKey: string, opts?: StoreSetOpts) {
|
||||
// 生成一个secretKey
|
||||
// 设置到store中
|
||||
oauthUser.oauthExpand = {
|
||||
...oauthUser.oauthExpand,
|
||||
accessToken: secretKey,
|
||||
description: 'secretKey',
|
||||
createTime: new Date().getTime(), // 创建时间
|
||||
};
|
||||
await this.store.setToken(
|
||||
{ accessToken: secretKey, refreshToken: '', value: oauthUser },
|
||||
{
|
||||
...opts,
|
||||
hasRefreshToken: false,
|
||||
},
|
||||
);
|
||||
return secretKey;
|
||||
}
|
||||
getOauthUser({ id, uid, username, type }: Partial<T>): OauthUser {
|
||||
const oauthUser: OauthUser = {
|
||||
id,
|
||||
username,
|
||||
uid,
|
||||
userId: uid || id, // 必存在,真实用户id
|
||||
type: type as 'user' | 'org',
|
||||
};
|
||||
if (uid) {
|
||||
oauthUser.orgId = id;
|
||||
}
|
||||
return oauthUser;
|
||||
}
|
||||
/**
|
||||
* 验证token,如果token不存在,返回null
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
async verifyToken(token: string) {
|
||||
const res = await this.store.getObject(token);
|
||||
return res;
|
||||
}
|
||||
/**
|
||||
* 验证token是否是accessToken, sk 开头的为secretKey,没有refreshToken
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
isSecretKey(token: string) {
|
||||
if (!token) {
|
||||
return false;
|
||||
}
|
||||
// 如果是sk_开头,则是secretKey
|
||||
if (token.startsWith('sk_')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* 刷新token
|
||||
* @param refreshToken
|
||||
* @returns
|
||||
*/
|
||||
async refreshToken(refreshToken: string) {
|
||||
const user = await this.store.getObject(refreshToken);
|
||||
if (!user) {
|
||||
// 过期
|
||||
throw new Error('Refresh token not found');
|
||||
}
|
||||
// 删除旧的token
|
||||
await this.store.delObject({ ...user });
|
||||
const token = await this.generateToken(
|
||||
{ ...user },
|
||||
{
|
||||
...user.oauthExpand,
|
||||
hasRefreshToken: true,
|
||||
},
|
||||
);
|
||||
console.log('resetToken token', await this.store.keys());
|
||||
|
||||
return token;
|
||||
}
|
||||
/**
|
||||
* 刷新token的过期时间
|
||||
* expand 为扩展参数,可以扩展到user.oauthExpand中
|
||||
* @param token
|
||||
* @returns
|
||||
*/
|
||||
async resetToken(accessToken: string, expand?: Record<string, any>) {
|
||||
const user = await this.store.getObject(accessToken);
|
||||
if (!user) {
|
||||
// 过期
|
||||
throw new Error('token not found');
|
||||
}
|
||||
user.oauthExpand = user.oauthExpand || {};
|
||||
const refreshToken = user.oauthExpand.refreshToken;
|
||||
if (refreshToken) {
|
||||
await this.store.delObject(user);
|
||||
}
|
||||
user.oauthExpand = {
|
||||
...user.oauthExpand,
|
||||
...expand,
|
||||
};
|
||||
const token = await this.generateToken(
|
||||
{ ...user },
|
||||
{
|
||||
...user.oauthExpand,
|
||||
hasRefreshToken: true,
|
||||
},
|
||||
);
|
||||
|
||||
return token;
|
||||
}
|
||||
/**
|
||||
* 过期token
|
||||
* @param token
|
||||
*/
|
||||
async delToken(token: string) {
|
||||
const user = await this.store.getObject(token);
|
||||
if (!user) {
|
||||
// 过期
|
||||
throw new Error('token not found');
|
||||
}
|
||||
this.store.delObject(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取某一个用户的所有token
|
||||
* @param userId
|
||||
* @returns
|
||||
*/
|
||||
async getUserTokens(userId: string, orgId?: string) {
|
||||
const userPrefix = orgId ? `org:${orgId}:user:${userId}` : `user:${userId}`;
|
||||
const tokens = await this.store.keys(`${userPrefix}:token:*`);
|
||||
return tokens;
|
||||
}
|
||||
/**
|
||||
* 过期某一个用户的所有token
|
||||
* @param userId
|
||||
* @param orgId
|
||||
*/
|
||||
async expireUserTokens(userId: string, type: 'user' | 'org' = 'user') {
|
||||
const userPrefix = type === 'org' ? `org:${userId}:user:*:` : `user:${userId}`;
|
||||
const tokensKeys = await this.store.keys(`${userPrefix}:token:*`);
|
||||
for (const tokenKey of tokensKeys) {
|
||||
try {
|
||||
const token = await this.store.redis.get(tokenKey);
|
||||
const user = await this.store.getObject(token);
|
||||
this.store.delObject(user);
|
||||
} catch (error) {
|
||||
console.error('expireUserTokens error', userId, type, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 过期所有用户的token, 然后重启服务
|
||||
*/
|
||||
async expireAllTokens() {
|
||||
const tokens = await this.store.keys('*');
|
||||
await this.store.delKeys(tokens);
|
||||
}
|
||||
}
|
||||
32
src/auth/oauth/salt.ts
Normal file
32
src/auth/oauth/salt.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import MD5 from 'crypto-js/md5.js';
|
||||
|
||||
/**
|
||||
* 生成随机盐
|
||||
* @returns
|
||||
*/
|
||||
export const getRandomSalt = () => {
|
||||
return Math.random().toString().slice(2, 7);
|
||||
};
|
||||
|
||||
/**
|
||||
* 加密密码
|
||||
* @param password
|
||||
* @param salt
|
||||
* @returns
|
||||
*/
|
||||
export const cryptPwd = (password: string, salt = '') => {
|
||||
const saltPassword = password + ':' + salt;
|
||||
const md5 = MD5(saltPassword);
|
||||
return md5.toString();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check password
|
||||
* @param password
|
||||
* @param salt
|
||||
* @param md5
|
||||
* @returns
|
||||
*/
|
||||
export const checkPwd = (password: string, salt: string, md5: string) => {
|
||||
return cryptPwd(password, salt) === md5;
|
||||
};
|
||||
@@ -1,46 +1 @@
|
||||
// import { DataTypes, Model, Sequelize } from 'sequelize';
|
||||
// import { useContextKey } from '@kevisual/context';
|
||||
// const sequelize = useContextKey<Sequelize>('sequelize');
|
||||
// export class Org extends Model {
|
||||
// declare id: string;
|
||||
// declare username: string;
|
||||
// declare description: string;
|
||||
// declare users: { role: string; uid: string }[];
|
||||
// }
|
||||
|
||||
// Org.init(
|
||||
// {
|
||||
// id: {
|
||||
// type: DataTypes.UUID,
|
||||
// primaryKey: true,
|
||||
// defaultValue: DataTypes.UUIDV4,
|
||||
// },
|
||||
// username: {
|
||||
// type: DataTypes.STRING,
|
||||
// allowNull: false,
|
||||
// unique: true,
|
||||
// },
|
||||
// description: {
|
||||
// type: DataTypes.STRING,
|
||||
// allowNull: true,
|
||||
// },
|
||||
// users: {
|
||||
// type: DataTypes.JSONB,
|
||||
// allowNull: true,
|
||||
// defaultValue: [],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// sequelize,
|
||||
// modelName: 'cf_org',
|
||||
// paranoid: true,
|
||||
// },
|
||||
// );
|
||||
|
||||
// Org.sync({ alter: true, logging: false }).catch((e) => {
|
||||
// console.error('Org sync', e);
|
||||
// });
|
||||
|
||||
// useContextKey('OrgModel', () => Org);
|
||||
import { Org } from '@kevisual/code-center-module/models';
|
||||
export { Org };
|
||||
export { Org } from '../auth/models/index.ts'
|
||||
@@ -1,6 +1,6 @@
|
||||
import { User, UserInit, UserServices } from '@kevisual/code-center-module/models';
|
||||
import { UserSecretInit, UserSecret } from '@kevisual/code-center-module/models';
|
||||
import { OrgInit } from '@kevisual/code-center-module/models';
|
||||
import { User, UserInit, UserServices } from '../auth/models/index.ts';
|
||||
import { UserSecretInit, UserSecret } from '../auth/models/index.ts';
|
||||
import { OrgInit } from '../auth/models/index.ts';
|
||||
export { User, UserInit, UserServices, UserSecret };
|
||||
const init = async () => {
|
||||
await OrgInit(null, null, {
|
||||
|
||||
@@ -12,6 +12,6 @@ import './micro-app/index.ts';
|
||||
|
||||
import './config/index.ts';
|
||||
|
||||
import './mark/index.ts';
|
||||
// import './mark/index.ts';
|
||||
|
||||
import './file-listener/index.ts';
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
import './list.ts';
|
||||
@@ -1,239 +0,0 @@
|
||||
import { app } from '@/app.ts';
|
||||
import { MarkModel } from './model.ts';
|
||||
import { MarkServices } from './services/mark.ts';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'mark',
|
||||
key: 'list',
|
||||
description: 'mark list.',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
ctx.body = await MarkServices.getList({
|
||||
uid: tokenUser.id,
|
||||
query: ctx.query,
|
||||
queryType: 'simple',
|
||||
});
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'mark',
|
||||
key: 'getVersion',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query;
|
||||
if (id) {
|
||||
const markModel = await MarkModel.findByPk(id);
|
||||
if (!markModel) {
|
||||
ctx.throw(404, 'mark not found');
|
||||
}
|
||||
if (markModel.uid !== tokenUser.id) {
|
||||
ctx.throw(403, 'no permission');
|
||||
}
|
||||
ctx.body = {
|
||||
version: Number(markModel.version),
|
||||
updatedAt: markModel.updatedAt,
|
||||
createdAt: markModel.createdAt,
|
||||
id: markModel.id,
|
||||
};
|
||||
} else {
|
||||
ctx.throw(400, 'id is required');
|
||||
// const [markModel, created] = await MarkModel.findOrCreate({
|
||||
// where: {
|
||||
// uid: tokenUser.id,
|
||||
// puid: tokenUser.uid,
|
||||
// title: dayjs().format('YYYY-MM-DD'),
|
||||
// },
|
||||
// defaults: {
|
||||
// title: dayjs().format('YYYY-MM-DD'),
|
||||
// uid: tokenUser.id,
|
||||
// markType: 'wallnote',
|
||||
// tags: ['daily'],
|
||||
// },
|
||||
// });
|
||||
// ctx.body = {
|
||||
// version: Number(markModel.version),
|
||||
// updatedAt: markModel.updatedAt,
|
||||
// createdAt: markModel.createdAt,
|
||||
// id: markModel.id,
|
||||
// created: created,
|
||||
// };
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'mark',
|
||||
key: 'get',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query;
|
||||
if (id) {
|
||||
const markModel = await MarkModel.findByPk(id);
|
||||
if (!markModel) {
|
||||
ctx.throw(404, 'mark not found');
|
||||
}
|
||||
if (markModel.uid !== tokenUser.id) {
|
||||
ctx.throw(403, 'no permission');
|
||||
}
|
||||
ctx.body = markModel;
|
||||
} else {
|
||||
ctx.throw(400, 'id is required');
|
||||
// id 不存在,获取当天的title为 日期的一条数据
|
||||
// const [markModel, created] = await MarkModel.findOrCreate({
|
||||
// where: {
|
||||
// uid: tokenUser.id,
|
||||
// puid: tokenUser.uid,
|
||||
// title: dayjs().format('YYYY-MM-DD'),
|
||||
// },
|
||||
// defaults: {
|
||||
// title: dayjs().format('YYYY-MM-DD'),
|
||||
// uid: tokenUser.id,
|
||||
// markType: 'wallnote',
|
||||
// tags: ['daily'],
|
||||
// uname: tokenUser.username,
|
||||
// puid: tokenUser.uid,
|
||||
// version: 1,
|
||||
// },
|
||||
// });
|
||||
// ctx.body = markModel;
|
||||
}
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'mark',
|
||||
key: 'update',
|
||||
middleware: ['auth'],
|
||||
isDebug: true,
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id, createdAt, updatedAt, uid: _, puid: _2, uname: _3, data, ...rest } = ctx.query.data || {};
|
||||
let markModel: MarkModel;
|
||||
if (id) {
|
||||
markModel = await MarkModel.findByPk(id);
|
||||
if (!markModel) {
|
||||
ctx.throw(404, 'mark not found');
|
||||
}
|
||||
if (markModel.uid !== tokenUser.id) {
|
||||
ctx.throw(403, 'no permission');
|
||||
}
|
||||
const version = Number(markModel.version) + 1;
|
||||
await markModel.update({
|
||||
...markModel.data,
|
||||
...rest,
|
||||
data: {
|
||||
...markModel.data,
|
||||
...data,
|
||||
},
|
||||
version,
|
||||
});
|
||||
} else {
|
||||
markModel = await MarkModel.create({
|
||||
data,
|
||||
...rest,
|
||||
uname: tokenUser.username,
|
||||
uid: tokenUser.id,
|
||||
puid: tokenUser.uid,
|
||||
});
|
||||
}
|
||||
ctx.body = markModel;
|
||||
})
|
||||
.addTo(app);
|
||||
app
|
||||
.route({
|
||||
path: 'mark',
|
||||
key: 'updateNode',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const operate = ctx.query.operate || 'update';
|
||||
const { id, node } = ctx.query.data || {};
|
||||
const markModel = await MarkModel.findByPk(id);
|
||||
if (!markModel) {
|
||||
ctx.throw(404, 'mark not found');
|
||||
}
|
||||
if (markModel.uid !== tokenUser.id) {
|
||||
ctx.throw(403, 'no permission');
|
||||
}
|
||||
await MarkModel.updateJsonNode(id, node, { operate });
|
||||
ctx.body = markModel;
|
||||
})
|
||||
.addTo(app);
|
||||
app
|
||||
.route({
|
||||
path: 'mark',
|
||||
key: 'updateNodes',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id, nodeOperateList } = ctx.query.data || {};
|
||||
const markModel = await MarkModel.findByPk(id);
|
||||
if (!markModel) {
|
||||
ctx.throw(404, 'mark not found');
|
||||
}
|
||||
if (markModel.uid !== tokenUser.id) {
|
||||
ctx.throw(403, 'no permission');
|
||||
}
|
||||
if (!nodeOperateList || !Array.isArray(nodeOperateList) || nodeOperateList.length === 0) {
|
||||
ctx.throw(400, 'nodeOperateList is required');
|
||||
}
|
||||
if (nodeOperateList.some((node) => !node.node)) {
|
||||
ctx.throw(400, 'nodeOperateList node is required');
|
||||
}
|
||||
const newmark = await MarkModel.updateJsonNodes(id, nodeOperateList);
|
||||
ctx.body = newmark;
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({
|
||||
path: 'mark',
|
||||
key: 'delete',
|
||||
middleware: ['auth'],
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query;
|
||||
const markModel = await MarkModel.findByPk(id);
|
||||
if (!markModel) {
|
||||
ctx.throw(404, 'mark not found');
|
||||
}
|
||||
if (markModel.uid !== tokenUser.id) {
|
||||
ctx.throw(403, 'no permission');
|
||||
}
|
||||
await markModel.destroy();
|
||||
ctx.body = markModel;
|
||||
})
|
||||
.addTo(app);
|
||||
|
||||
app
|
||||
.route({ path: 'mark', key: 'getMenu', description: '获取菜单', middleware: ['auth'] })
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { rows, count } = await MarkModel.findAndCountAll({
|
||||
where: {
|
||||
uid: tokenUser.id,
|
||||
},
|
||||
attributes: ['id', 'title', 'summary', 'tags', 'thumbnail', 'link', 'createdAt', 'updatedAt'],
|
||||
});
|
||||
ctx.body = {
|
||||
list: rows,
|
||||
total: count,
|
||||
};
|
||||
})
|
||||
.addTo(app);
|
||||
@@ -1,327 +0,0 @@
|
||||
import { useContextKey } from '@kevisual/context';
|
||||
import { nanoid, customAlphabet } from 'nanoid';
|
||||
import { DataTypes, Model, ModelAttributes } from 'sequelize';
|
||||
import type { Sequelize } from 'sequelize';
|
||||
export const random = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ');
|
||||
export type Mark = Partial<InstanceType<typeof MarkModel>>;
|
||||
export type MarkData = {
|
||||
md?: string; // markdown
|
||||
mdList?: string[]; // markdown list
|
||||
type?: string; // 类型 markdown | json | html | image | video | audio | code | link | file
|
||||
data?: any;
|
||||
key?: string; // 文件的名称, 唯一
|
||||
push?: boolean; // 是否推送到elasticsearch
|
||||
pushTime?: Date; // 推送时间
|
||||
summary?: string; // 摘要
|
||||
nodes?: MarkDataNode[]; // 节点
|
||||
[key: string]: any;
|
||||
};
|
||||
export type MarkFile = {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
size: number;
|
||||
type: 'self' | 'data' | 'generate'; // generate为生成文件
|
||||
query: string; // 'data.nodes[id].content';
|
||||
hash: string;
|
||||
fileKey: string; // 文件的名称, 唯一
|
||||
};
|
||||
export type MarkDataNode = {
|
||||
id?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
export type MarkConfig = {
|
||||
[key: string]: any;
|
||||
};
|
||||
export type MarkAuth = {
|
||||
[key: string]: any;
|
||||
};
|
||||
/**
|
||||
* 隐秘内容
|
||||
* auth
|
||||
* config
|
||||
*
|
||||
*/
|
||||
export class MarkModel extends Model {
|
||||
declare id: string;
|
||||
declare title: string; // 标题,可以ai生成
|
||||
declare description: string; // 描述,可以ai生成
|
||||
declare cover: string; // 封面,可以ai生成
|
||||
declare thumbnail: string; // 缩略图
|
||||
declare key: string; // 文件路径
|
||||
declare markType: string; // markdown | json | html | image | video | audio | code | link | file
|
||||
declare link: string; // 访问链接
|
||||
declare tags: string[]; // 标签
|
||||
declare summary: string; // 摘要, description的简化版
|
||||
declare data: MarkData; // 数据
|
||||
|
||||
declare uid: string; // 操作用户的id
|
||||
declare puid: string; // 父级用户的id, 真实用户
|
||||
declare config: MarkConfig; // mark属于一定不会暴露的内容。
|
||||
|
||||
declare fileList: MarkFile[]; // 文件管理
|
||||
declare uname: string; // 用户的名称, 或者着别名
|
||||
|
||||
declare markedAt: Date; // 标记时间
|
||||
declare createdAt: Date;
|
||||
declare updatedAt: Date;
|
||||
declare version: number;
|
||||
/**
|
||||
* 加锁更新data中的node的节点,通过node的id
|
||||
* @param param0
|
||||
*/
|
||||
static async updateJsonNode(id: string, node: MarkDataNode, opts?: { operate?: 'update' | 'delete'; Model?: any; sequelize?: Sequelize }) {
|
||||
const sequelize = opts?.sequelize || (await useContextKey('sequelize'));
|
||||
const transaction = await sequelize.transaction(); // 开启事务
|
||||
const operate = opts.operate || 'update';
|
||||
const isUpdate = operate === 'update';
|
||||
const Model = opts.Model || MarkModel;
|
||||
try {
|
||||
// 1. 获取当前的 JSONB 字段值(加锁)
|
||||
const mark = await Model.findByPk(id, {
|
||||
transaction,
|
||||
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
|
||||
});
|
||||
if (!mark) {
|
||||
throw new Error('Mark not found');
|
||||
}
|
||||
// 2. 修改特定的数组元素
|
||||
const data = mark.data as MarkData;
|
||||
const items = data.nodes;
|
||||
if (!node.id) {
|
||||
node.id = random(12);
|
||||
}
|
||||
|
||||
// 找到要更新的元素
|
||||
const itemIndex = items.findIndex((item) => item.id === node.id);
|
||||
if (itemIndex === -1) {
|
||||
isUpdate && items.push(node);
|
||||
} else {
|
||||
if (isUpdate) {
|
||||
items[itemIndex] = node;
|
||||
} else {
|
||||
items.splice(itemIndex, 1);
|
||||
}
|
||||
}
|
||||
const version = Number(mark.version) + 1;
|
||||
// 4. 更新 JSONB 字段
|
||||
const result = await mark.update(
|
||||
{
|
||||
data: {
|
||||
...data,
|
||||
nodes: items,
|
||||
},
|
||||
version,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
static async updateJsonNodes(id: string, nodes: { node: MarkDataNode; operate?: 'update' | 'delete' }[], opts?: { Model?: any; sequelize?: Sequelize }) {
|
||||
const sequelize = opts?.sequelize || (await useContextKey('sequelize'));
|
||||
const transaction = await sequelize.transaction(); // 开启事务
|
||||
const Model = opts?.Model || MarkModel;
|
||||
try {
|
||||
const mark = await Model.findByPk(id, {
|
||||
transaction,
|
||||
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
|
||||
});
|
||||
if (!mark) {
|
||||
throw new Error('Mark not found');
|
||||
}
|
||||
const data = mark.data as MarkData;
|
||||
const _nodes = data.nodes || [];
|
||||
// 过滤不在nodes中的节点
|
||||
const blankNodes = nodes.filter((node) => !_nodes.find((n) => n.id === node.node.id)).map((node) => node.node);
|
||||
// 更新或删除节点
|
||||
const newNodes = _nodes
|
||||
.map((node) => {
|
||||
const nodeOperate = nodes.find((n) => n.node.id === node.id);
|
||||
if (nodeOperate) {
|
||||
if (nodeOperate.operate === 'delete') {
|
||||
return null;
|
||||
}
|
||||
return nodeOperate.node;
|
||||
}
|
||||
return node;
|
||||
})
|
||||
.filter((node) => node !== null);
|
||||
const version = Number(mark.version) + 1;
|
||||
const result = await mark.update(
|
||||
{
|
||||
data: {
|
||||
...data,
|
||||
nodes: [...blankNodes, ...newNodes],
|
||||
},
|
||||
version,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
await transaction.commit();
|
||||
return result;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
static async updateData(id: string, data: MarkData, opts: { Model?: any; sequelize?: Sequelize }) {
|
||||
const sequelize = opts.sequelize || (await useContextKey('sequelize'));
|
||||
const transaction = await sequelize.transaction(); // 开启事务
|
||||
const Model = opts.Model || MarkModel;
|
||||
const mark = await Model.findByPk(id, {
|
||||
transaction,
|
||||
lock: transaction.LOCK.UPDATE, // 加锁,防止其他事务同时修改
|
||||
});
|
||||
if (!mark) {
|
||||
throw new Error('Mark not found');
|
||||
}
|
||||
const version = Number(mark.version) + 1;
|
||||
const result = await mark.update(
|
||||
{
|
||||
...mark.data,
|
||||
...data,
|
||||
data: {
|
||||
...mark.data,
|
||||
...data,
|
||||
},
|
||||
version,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
await transaction.commit();
|
||||
return result;
|
||||
}
|
||||
static async createNew(data: any, opts: { Model?: any; sequelize?: Sequelize }) {
|
||||
const sequelize = opts.sequelize || (await useContextKey('sequelize'));
|
||||
const transaction = await sequelize.transaction(); // 开启事务
|
||||
const Model = opts.Model || MarkModel;
|
||||
const result = await Model.create({ ...data, version: 1 }, { transaction });
|
||||
await transaction.commit();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
export type MarkInitOpts<T = any> = {
|
||||
tableName: string;
|
||||
sequelize?: Sequelize;
|
||||
callInit?: (attribute: ModelAttributes) => ModelAttributes;
|
||||
Model?: T extends typeof MarkModel ? T : typeof MarkModel;
|
||||
};
|
||||
export type Opts = {
|
||||
sync?: boolean;
|
||||
alter?: boolean;
|
||||
logging?: boolean | ((...args: any) => any);
|
||||
force?: boolean;
|
||||
};
|
||||
export const MarkMInit = async <T = any>(opts: MarkInitOpts<T>, sync?: Opts) => {
|
||||
const sequelize = await useContextKey('sequelize');
|
||||
opts.sequelize = opts.sequelize || sequelize;
|
||||
const { callInit, Model, ...optsRest } = opts;
|
||||
const modelAttribute = {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
primaryKey: true,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
comment: 'id',
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.TEXT, // 对应的minio的文件路径
|
||||
defaultValue: '',
|
||||
},
|
||||
markType: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: 'md', // markdown | json | html | image | video | audio | code | link | file
|
||||
comment: '类型',
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
},
|
||||
cover: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: '封面',
|
||||
},
|
||||
thumbnail: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: '缩略图',
|
||||
},
|
||||
link: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: '链接',
|
||||
},
|
||||
tags: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: [],
|
||||
},
|
||||
summary: {
|
||||
type: DataTypes.TEXT,
|
||||
defaultValue: '',
|
||||
comment: '摘要',
|
||||
},
|
||||
config: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
},
|
||||
data: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {},
|
||||
},
|
||||
fileList: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: [],
|
||||
},
|
||||
uname: {
|
||||
type: DataTypes.STRING,
|
||||
defaultValue: '',
|
||||
comment: '用户的名称, 更新后的用户的名称',
|
||||
},
|
||||
version: {
|
||||
type: DataTypes.INTEGER, // 更新刷新版本,多人协作
|
||||
defaultValue: 1,
|
||||
},
|
||||
markedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
comment: '标记时间',
|
||||
},
|
||||
uid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
},
|
||||
puid: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
},
|
||||
};
|
||||
const InitModel = Model || MarkModel;
|
||||
InitModel.init(callInit ? callInit(modelAttribute) : modelAttribute, {
|
||||
sequelize,
|
||||
paranoid: true,
|
||||
...optsRest,
|
||||
});
|
||||
if (sync && sync.sync) {
|
||||
const { sync: _, ...rest } = sync;
|
||||
MarkModel.sync({ alter: true, logging: false, ...rest }).catch((e) => {
|
||||
console.error('MarkModel sync', e);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const markModelInit = MarkMInit;
|
||||
|
||||
export const syncMarkModel = async (sync?: Opts, tableName = 'micro_mark') => {
|
||||
const sequelize = await useContextKey('sequelize');
|
||||
await MarkMInit({ sequelize, tableName }, sync);
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from '@kevisual/code-center-module/src/mark/mark-model.ts';
|
||||
import { markModelInit, MarkModel, syncMarkModel } from '@kevisual/code-center-module/src/mark/mark-model.ts';
|
||||
export { markModelInit, MarkModel };
|
||||
|
||||
syncMarkModel({ sync: true, alter: true, logging: false });
|
||||
@@ -1,58 +0,0 @@
|
||||
import { FindAttributeOptions, Op } from 'sequelize';
|
||||
import { MarkModel } from '../model.ts';
|
||||
|
||||
export class MarkServices {
|
||||
static getList = async (opts: {
|
||||
/** 查询用户的 */
|
||||
uid?: string;
|
||||
query?: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
search?: string;
|
||||
markType?: string;
|
||||
sort?: string;
|
||||
};
|
||||
/**
|
||||
* 查询类型
|
||||
* simple: 简单查询 默认
|
||||
*/
|
||||
queryType?: string;
|
||||
}) => {
|
||||
const { uid, query } = opts;
|
||||
const { page = 1, pageSize = 999, search, sort = 'DESC' } = query;
|
||||
const searchWhere = search
|
||||
? {
|
||||
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { summary: { [Op.like]: `%${search}%` } }],
|
||||
}
|
||||
: {};
|
||||
if (opts.query?.markType) {
|
||||
searchWhere['markType'] = opts.query.markType;
|
||||
}
|
||||
const attributes: FindAttributeOptions = {
|
||||
exclude: [],
|
||||
};
|
||||
const queryType = opts.queryType || 'simple';
|
||||
if (queryType === 'simple') {
|
||||
// attributes.include = ['id', 'title', 'link', 'summary', 'thumbnail', 'markType', 'tags', 'uid', 'share', 'uname'];
|
||||
attributes.exclude = ['data', 'config', 'cover', 'description'];
|
||||
}
|
||||
const { rows, count } = await MarkModel.findAndCountAll({
|
||||
where: {
|
||||
uid: uid,
|
||||
...searchWhere,
|
||||
},
|
||||
order: [['updatedAt', sort]],
|
||||
attributes: attributes,
|
||||
limit: pageSize,
|
||||
offset: (page - 1) * pageSize,
|
||||
});
|
||||
return {
|
||||
pagination: {
|
||||
current: page,
|
||||
pageSize,
|
||||
total: count,
|
||||
},
|
||||
list: rows,
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { WxTokenResponse, fetchToken, getUserInfo, getUserInfoByMp, post } from './wx.ts';
|
||||
import { useContextKey } from '@kevisual/use-config/context';
|
||||
import { UserModel } from '@kevisual/code-center-module';
|
||||
import { UserModel } from '../../../auth/index.ts';
|
||||
import { Buffer } from 'buffer';
|
||||
import { CustomError } from '@kevisual/router';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
|
||||
@@ -10,7 +10,6 @@ const wx = {
|
||||
appId: config.WX_MP_APP_ID,
|
||||
appSecret: config.WX_MP_APP_SECRET,
|
||||
}
|
||||
console.log('wx config', wx, wxOpen);
|
||||
export type WxTokenResponse = {
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
|
||||
@@ -10,11 +10,11 @@ app
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { page = 1, pageSize = 20, search, sort = 'DESC', orgId } = ctx.query;
|
||||
const { page = 1, pageSize = 100, search, sort = 'DESC', orgId } = ctx.query;
|
||||
const searchWhere: Record<string, any> = search
|
||||
? {
|
||||
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { description: { [Op.like]: `%${search}%` } }],
|
||||
}
|
||||
[Op.or]: [{ title: { [Op.like]: `%${search}%` } }, { description: { [Op.like]: `%${search}%` } }],
|
||||
}
|
||||
: {};
|
||||
if (orgId) {
|
||||
searchWhere.orgId = orgId;
|
||||
@@ -52,7 +52,7 @@ app
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id, updatedAt: _clear, createdAt: _clear2, token, ...rest } = ctx.query.data;
|
||||
const { id, updatedAt: _clear, title = 'life', createdAt: _clear2, token, ...rest } = ctx.query.data;
|
||||
let secret: UserSecret;
|
||||
let isNew = false;
|
||||
|
||||
@@ -65,6 +65,13 @@ app
|
||||
if (secret.userId !== tokenUser.userId) {
|
||||
ctx.throw(403, 'No permission');
|
||||
}
|
||||
} else if (title) {
|
||||
secret = await UserSecret.findOne({
|
||||
where: {
|
||||
userId: tokenUser.userId,
|
||||
title,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
secret = await UserSecret.createSecret(tokenUser);
|
||||
isNew = true;
|
||||
@@ -87,13 +94,27 @@ app
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
const { id, title } = ctx.query.data || {};
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400, 'id is required');
|
||||
if (!id && !title) {
|
||||
ctx.throw(400, 'id 或者 title 必须提供一个');
|
||||
}
|
||||
let secret: UserSecret;
|
||||
|
||||
const secret = await UserSecret.findByPk(id);
|
||||
if (id) {
|
||||
secret = await UserSecret.findByPk(id);
|
||||
}
|
||||
if (!secret && title) {
|
||||
secret = await UserSecret.findOne({
|
||||
where: {
|
||||
userId: tokenUser.userId,
|
||||
title,
|
||||
},
|
||||
});
|
||||
if (!secret) {
|
||||
ctx.throw(404, 'Secret not found');
|
||||
}
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
ctx.throw(404, 'Secret not found');
|
||||
@@ -115,19 +136,30 @@ app
|
||||
})
|
||||
.define(async (ctx) => {
|
||||
const tokenUser = ctx.state.tokenUser;
|
||||
const { id } = ctx.query.data || {};
|
||||
const { id, title } = ctx.query.data || {};
|
||||
|
||||
if (!id) {
|
||||
ctx.throw(400, 'id is required');
|
||||
if (!id && !title) {
|
||||
ctx.throw(400, 'id 或者 title 必须提供一个');
|
||||
}
|
||||
|
||||
const secret = await UserSecret.findByPk(id);
|
||||
let secret: UserSecret | null = null;
|
||||
|
||||
if (id) {
|
||||
secret = await UserSecret.findByPk(id);
|
||||
} else if (title) {
|
||||
secret = await UserSecret.findOne({
|
||||
where: {
|
||||
userId: tokenUser.userId,
|
||||
title,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!secret) {
|
||||
ctx.throw(404, 'Secret not found');
|
||||
}
|
||||
|
||||
if (secret.userId !== tokenUser.uid) {
|
||||
if (secret.userId !== tokenUser.userId) {
|
||||
ctx.throw(403, 'No permission');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { app } from '@/app.ts';
|
||||
import { User } from '@/models/user.ts';
|
||||
import MD5 from 'crypto-js/md5.js';
|
||||
import { authCan } from '@kevisual/code-center-module/models';
|
||||
import { authCan } from '@/auth/index.ts';
|
||||
import jsonwebtoken from 'jsonwebtoken';
|
||||
|
||||
import { redis } from '@/app.ts';
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { config } from '../modules/config.ts';
|
||||
import { sequelize } from '../modules/sequelize.ts';
|
||||
export { program, Command } from '../program.ts';
|
||||
// import { User, UserInit, OrgInit, Org, UserSecretInit, UserSecret } from '@kevisual/code-center-module/models';
|
||||
import { User, UserInit, OrgInit, Org, UserSecretInit, UserSecret } from '@kevisual/code-center-module/src/core-models.ts';
|
||||
import { User, UserInit, OrgInit, Org, UserSecretInit, UserSecret } from '../auth/index.ts';
|
||||
import { Logger } from '@kevisual/logger';
|
||||
export const close = async () => {
|
||||
process.exit(0);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { sequelize } from '../modules/sequelize.ts';
|
||||
import { User, UserInit, UserServices, Org, OrgInit } from '@kevisual/code-center-module/models';
|
||||
import { User, UserInit, UserServices, Org, OrgInit } from '../auth/index.ts';
|
||||
|
||||
// User.sync({ alter: true, logging: true }).then(() => {
|
||||
// console.log('sync user done');
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useContextKey } from '@kevisual/context';
|
||||
import { Redis } from 'ioredis';
|
||||
import http from 'node:http';
|
||||
import { Wx, WxMsgEvent, parseWxMessage } from './wx/index.ts';
|
||||
import { config } from './modules/config.ts';
|
||||
import { contextConfig as config } from './modules/config.ts';
|
||||
import { loginByTicket } from './wx/login-by-ticket.ts';
|
||||
export const simpleRouter: SimpleRouter = await useContextKey('router');
|
||||
export const redis: Redis = await useContextKey('redis');
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { useConfig } from "@kevisual/context";
|
||||
import { useConfig as useContextConfig } from "@kevisual/context";
|
||||
|
||||
type Config = {
|
||||
WX_MP_APP_ID: string;
|
||||
WX_MP_APP_SECRET: string;
|
||||
}
|
||||
export const config = useConfig<Config>();
|
||||
export const contextConfig = useContextConfig<Config>();
|
||||
|
||||
import { useConfig } from '@kevisual/use-config';
|
||||
|
||||
export const config = useConfig()
|
||||
|
||||
console.log('配置项:', config);
|
||||
9
wxmsg/src/queue.ts
Normal file
9
wxmsg/src/queue.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Queue } from 'bullmq';
|
||||
|
||||
export const wxmsgQueue = new Queue('wxmsg', {
|
||||
connection: {
|
||||
host: process.env.REDIS_HOST || 'kevisual.cn',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getAccessToken } from './get-access-token.ts';
|
||||
import { Redis } from 'ioredis';
|
||||
import { WxCustomServiceMsg, WxMsgText } from './type/custom-service.ts';
|
||||
import { Queue } from 'bullmq';
|
||||
import { useContextKey } from "@kevisual/context";
|
||||
|
||||
export * from './type/custom-service.ts';
|
||||
export * from './type/send.ts';
|
||||
@@ -46,18 +48,14 @@ export class Wx {
|
||||
console.log('Analyzed message:', { touser, msgType });
|
||||
return;
|
||||
}
|
||||
const txtMsg = msg as WxMsgText;
|
||||
const content = txtMsg.content;
|
||||
|
||||
console.log('Analyzing user message:', { touser, msgType, content });
|
||||
const sendData = {
|
||||
touser,
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: 'Hello World',
|
||||
},
|
||||
const wxmsgQueue = useContextKey<Queue>('wxmsgQueue');
|
||||
if (!wxmsgQueue) {
|
||||
throw new Error('wxmsgQueue is not available in context.');
|
||||
}
|
||||
this.sendUserMessage(sendData);
|
||||
wxmsgQueue.add('analyzeUserMsg', {
|
||||
touser,
|
||||
msg,
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 发送客服消息
|
||||
|
||||
15
wxmsg/task/worker/bun.config.ts
Normal file
15
wxmsg/task/worker/bun.config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { build } from 'bun';
|
||||
|
||||
await build({
|
||||
entrypoints: ['./index.ts'],
|
||||
outdir: './dist',
|
||||
target: 'node',
|
||||
format: 'esm',
|
||||
naming: {
|
||||
entry: 'app.js',
|
||||
},
|
||||
minify: false,
|
||||
sourcemap: false,
|
||||
});
|
||||
|
||||
console.log('✅ Build complete: dist/app.js');
|
||||
38
wxmsg/task/worker/index.ts
Normal file
38
wxmsg/task/worker/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Worker } from "bullmq";
|
||||
import { redis } from './redis.ts';
|
||||
|
||||
import { Wx } from "../../src/wx";
|
||||
|
||||
const worker = new Worker('wxmsg', async job => {
|
||||
const wx = new Wx({
|
||||
appId: process.env.WX_APPID || '',
|
||||
appSecret: process.env.WX_APPSECRET || '',
|
||||
redis: redis
|
||||
});
|
||||
if (job.name === 'analyzeUserMsg') {
|
||||
const { touser, msg } = job.data;
|
||||
const accessToken = await wx.getAccessToken();
|
||||
const sendData = {
|
||||
touser,
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: 'Hello World' + new Date().toISOString(),
|
||||
},
|
||||
};
|
||||
await wx.sendUserMessage(sendData);
|
||||
} else {
|
||||
throw new Error(`Unknown job name: ${job.name}`);
|
||||
}
|
||||
}, {
|
||||
connection: redis
|
||||
});
|
||||
|
||||
worker.on('completed', (job) => {
|
||||
console.log(`Job ${job.id} has completed!`);
|
||||
});
|
||||
|
||||
worker.on('failed', (job, err) => {
|
||||
console.log(`Job ${job?.id} has failed with error ${err.message}`);
|
||||
});
|
||||
|
||||
console.log('Worker is running...');
|
||||
37
wxmsg/task/worker/package.json
Normal file
37
wxmsg/task/worker/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@kevisual/wxmsg-worker",
|
||||
"version": "0.0.2",
|
||||
"description": "",
|
||||
"main": "index.ts",
|
||||
"basename": "/root/wxmsg-worker",
|
||||
"app": {
|
||||
"type": "pm2-system-app",
|
||||
"entry": "./app.js"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "bun index.ts",
|
||||
"build": "bun run bun.config.ts",
|
||||
"prepub": "rimraf dist && rimraf pack-dist && pnpm build",
|
||||
"pub": "envision pack -p -u -c"
|
||||
},
|
||||
"keywords": [],
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.24.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@kevisual/context": "^0.0.4",
|
||||
"@kevisual/router": "0.0.33",
|
||||
"@types/node": "^24.10.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"xml2js": "^0.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.3",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/xml2js": "^0.4.14"
|
||||
}
|
||||
}
|
||||
42
wxmsg/task/worker/redis.ts
Normal file
42
wxmsg/task/worker/redis.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import Redis from "ioredis";
|
||||
import { useConfig } from '@kevisual/use-config';
|
||||
import { useContextKey } from "@kevisual/context";
|
||||
|
||||
export const config = useConfig()
|
||||
|
||||
// 首先从 process.env 读取环境变量
|
||||
const redisConfig = {
|
||||
host: process.env.REDIS_HOST || 'kevisual.cn',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
};
|
||||
|
||||
export const createRedisClient = (options = {}) => {
|
||||
const redis = new Redis({
|
||||
// host: 'localhost', // Redis 服务器的主机名或 IP 地址
|
||||
// port: 6379, // Redis 服务器的端口号
|
||||
// password: 'your_password', // Redis 的密码 (如果有)
|
||||
db: 0, // 要使用的 Redis 数据库索引 (0-15)
|
||||
keyPrefix: '', // key 前缀
|
||||
retryStrategy(times) {
|
||||
// 连接重试策略
|
||||
return Math.min(times * 50, 2000); // 每次重试时延迟增加
|
||||
},
|
||||
maxRetriesPerRequest: null, // 允许请求重试的次数 (如果需要无限次重试)
|
||||
...options,
|
||||
});
|
||||
// 监听连接事件
|
||||
redis.on('connect', () => {
|
||||
console.log('Redis 连接成功');
|
||||
});
|
||||
|
||||
redis.on('error', (err) => {
|
||||
console.error('Redis 连接错误', err);
|
||||
});
|
||||
redis.on('ready', () => {
|
||||
console.log('Redis 已准备好处理请求');
|
||||
});
|
||||
return redis;
|
||||
};
|
||||
const redis = useContextKey('redis', createRedisClient(redisConfig));
|
||||
export { redis };
|
||||
Reference in New Issue
Block a user