From 366a21d621039eb56108c3697b869cb28aa1f58f Mon Sep 17 00:00:00 2001 From: abearxiong Date: Fri, 20 Feb 2026 23:30:53 +0800 Subject: [PATCH] feat: add CNB login functionality and user management - Introduced `cnb-login` route to handle user login via CNB token. - Created `CnbServices` class for managing CNB user interactions. - Added `findByCnbId` method in the User model to retrieve users by CNB ID. - Updated error handling to provide more structured error messages. - Enhanced user creation logic to handle CNB users. - Added tests for the new CNB login functionality. --- package.json | 5 +- pnpm-lock.yaml | 226 +++++++++++++++++++++++- src/auth/models/user.ts | 24 ++- src/auth/oauth/oauth.ts | 4 +- src/routes/config/services/share.ts | 10 +- src/routes/user/admin/user.ts | 26 +-- src/routes/user/cnb-login.ts | 30 ++++ src/routes/user/index.ts | 2 + src/routes/user/list.ts | 10 +- src/routes/user/me.ts | 19 +- src/routes/user/modules/cnb-services.ts | 37 ++++ src/routes/user/modules/wx-services.ts | 4 +- src/routes/user/modules/wx.ts | 2 +- src/routes/user/web-login.ts | 10 ++ src/test/cnb-login.ts | 22 +++ src/test/common.ts | 1 + 16 files changed, 392 insertions(+), 40 deletions(-) create mode 100644 src/routes/user/cnb-login.ts create mode 100644 src/routes/user/modules/cnb-services.ts create mode 100644 src/test/cnb-login.ts diff --git a/package.json b/package.json index 6e07ad2..abf5912 100644 --- a/package.json +++ b/package.json @@ -66,12 +66,13 @@ "devDependencies": { "@aws-sdk/client-s3": "^3.994.0", "@kevisual/api": "^0.0.52", + "@kevisual/cnb": "^0.0.28", "@kevisual/context": "^0.0.8", "@kevisual/local-app-manager": "0.1.32", "@kevisual/logger": "^0.0.4", "@kevisual/oss": "0.0.19", "@kevisual/permission": "^0.0.4", - "@kevisual/router": "0.0.80", + "@kevisual/router": "0.0.83", "@kevisual/types": "^0.0.12", "@kevisual/use-config": "^1.0.30", "@types/archiver": "^7.0.0", @@ -100,7 +101,7 @@ "inflight": "latest", "picomatch": "^4.0.2" }, - "packageManager": "pnpm@10.30.0", + "packageManager": "pnpm@10.30.1", "workspaces": [ "wxmsg" ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b8e602..943dd90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,6 +67,9 @@ importers: '@kevisual/api': specifier: ^0.0.52 version: 0.0.52(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@kevisual/cnb': + specifier: ^0.0.28 + version: 0.0.28(dotenv@17.3.1)(ioredis@5.9.3) '@kevisual/context': specifier: ^0.0.8 version: 0.0.8 @@ -83,8 +86,8 @@ importers: specifier: ^0.0.4 version: 0.0.4 '@kevisual/router': - specifier: 0.0.80 - version: 0.0.80 + specifier: 0.0.83 + version: 0.0.83 '@kevisual/types': specifier: ^0.0.12 version: 0.0.12 @@ -667,6 +670,9 @@ packages: '@kevisual/auth@2.0.3': resolution: {integrity: sha512-4xpijaIhlCTr/DlJaV/gmkCQeg45EO1yxWpRvUX+1jCdVbuxSR0wZrF0SD9oybnjmKWMKDNPLsXyduFjMGcItA==} + '@kevisual/cnb@0.0.28': + resolution: {integrity: sha512-mv45B68D/lliPBUXEnxbofV+Ds/KSYXuGzzG7S8yEekwp31PwRjecP2dyE0Mxe+DlhFmsSYN09liVUHcCXDbOg==} + '@kevisual/context@0.0.4': resolution: {integrity: sha512-HJeLeZQLU+7tCluSfOyvkgKLs0HjCZrdJlZgEgKRSa8XTwZfMAUt6J7qZTbrZAHBlPtX68EPu/PI8JMCeu3WAQ==} @@ -712,6 +718,9 @@ packages: '@kevisual/router@0.0.80': resolution: {integrity: sha512-rVwi6Yf411bnNm2x94lMm+s4Csw0Yb7u/aj+VJJ59iouAYhjLuL7Rs1EcARhnQf47cegBJi6zozfGHgLsLHN2w==} + '@kevisual/router@0.0.83': + resolution: {integrity: sha512-CVazzM1rXVyvU7QcMQr0/EuqacRNEGalThDDLGQcvKEVHyduJ9yWddn6kezgWFCpNlPKhzSCKkIFuZVixNVxDQ==} + '@kevisual/types@0.0.12': resolution: {integrity: sha512-zJXH2dosir3jVrQ6QG4i0+iLQeT9gJ3H+cKXs8ReWboxBSYzUZO78XssVeVrFPsJ33iaAqo4q3DWbSS1dWGn7Q==} @@ -724,6 +733,10 @@ packages: resolution: {integrity: sha512-jlFxSlXUEz93cFW+UYT5BXv/rFVgiMQnIfqRYZ0gj1hSP8PMGRqMqUoHSLfKvfRRS4jseLSvTTeEKSQpZJtURg==} engines: {node: '>=10.0.0'} + '@kevisual/ws@8.19.0': + resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==} + engines: {node: '>=10.0.0'} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -1192,6 +1205,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -1217,6 +1234,9 @@ packages: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} engines: {node: '>= 14'} + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -1240,6 +1260,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + crypto-js@4.2.0: resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} @@ -1293,6 +1316,9 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + degenerator@5.0.1: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} @@ -1305,6 +1331,9 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} @@ -1603,6 +1632,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + h3@1.15.5: + resolution: {integrity: sha512-xEyq3rSl+dhGX2Lm0+eFQIAzlDN6Fs0EcC4f7BNUmzaRX/PTzeuM+Tr2lHB8FoXggsQIeXLj8EDVgs5ywxyxmg==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -1648,6 +1680,9 @@ packages: resolution: {integrity: sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==} engines: {node: '>= 12'} + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-binary-path@2.1.0: resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} engines: {node: '>=8'} @@ -1748,6 +1783,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.2.6: + resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + engines: {node: 20 || >=22} + lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -1835,14 +1874,23 @@ packages: node-abort-controller@3.1.1: resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-gyp-build-optional-packages@5.2.2: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true + node-mock-http@1.0.4: + resolution: {integrity: sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==} + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2000,6 +2048,9 @@ packages: queue-tick@1.0.1: resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -2039,6 +2090,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + redis-errors@1.2.0: resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} engines: {node: '>=4'} @@ -2254,6 +2309,12 @@ packages: tx2@1.0.5: resolution: {integrity: sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==} + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -2264,6 +2325,68 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + unstorage@1.17.4: + resolution: {integrity: sha512-fHK0yNg38tBiJKp/Vgsq4j0JEsCmgqH58HAn707S7zGkArbZsVr/CwINoi+nh3h98BRCwKvx1K3Xg9u3VV83sw==} + peerDependencies: + '@azure/app-configuration': ^1.8.0 + '@azure/cosmos': ^4.2.0 + '@azure/data-tables': ^13.3.0 + '@azure/identity': ^4.6.0 + '@azure/keyvault-secrets': ^4.9.0 + '@azure/storage-blob': ^12.26.0 + '@capacitor/preferences': ^6 || ^7 || ^8 + '@deno/kv': '>=0.9.0' + '@netlify/blobs': ^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0 + '@planetscale/database': ^1.19.0 + '@upstash/redis': ^1.34.3 + '@vercel/blob': '>=0.27.1' + '@vercel/functions': ^2.2.12 || ^3.0.0 + '@vercel/kv': ^1 || ^2 || ^3 + aws4fetch: ^1.0.20 + db0: '>=0.2.1' + idb-keyval: ^6.2.1 + ioredis: ^5.4.2 + uploadthing: ^7.4.4 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@deno/kv': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/blob': + optional: true + '@vercel/functions': + optional: true + '@vercel/kv': + optional: true + aws4fetch: + optional: true + db0: + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + uploadthing: + optional: true + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -3034,6 +3157,38 @@ snapshots: '@kevisual/auth@2.0.3': {} + '@kevisual/cnb@0.0.28(dotenv@17.3.1)(ioredis@5.9.3)': + dependencies: + '@kevisual/query': 0.0.49 + '@kevisual/router': 0.0.80 + '@kevisual/use-config': 1.0.30(dotenv@17.3.1) + es-toolkit: 1.44.0 + nanoid: 5.1.6 + unstorage: 1.17.4(ioredis@5.9.3) + ws: '@kevisual/ws@8.19.0' + zod: 4.3.6 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - dotenv + - idb-keyval + - ioredis + - uploadthing + '@kevisual/context@0.0.4': {} '@kevisual/context@0.0.6': {} @@ -3080,6 +3235,10 @@ snapshots: dependencies: es-toolkit: 1.44.0 + '@kevisual/router@0.0.83': + dependencies: + es-toolkit: 1.44.0 + '@kevisual/types@0.0.12': {} '@kevisual/use-config@1.0.30(dotenv@17.3.1)': @@ -3089,6 +3248,8 @@ snapshots: '@kevisual/ws@8.0.0': {} + '@kevisual/ws@8.19.0': {} + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -3731,6 +3892,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@1.1.4: optional: true @@ -3756,6 +3921,8 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.5.2 + cookie-es@1.2.2: {} + core-util-is@1.0.3: {} crc-32@1.2.2: {} @@ -3777,6 +3944,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + crypto-js@4.2.0: {} culvert@0.1.2: {} @@ -3809,6 +3980,8 @@ snapshots: deep-extend@0.6.0: optional: true + defu@6.1.4: {} + degenerator@5.0.1: dependencies: ast-types: 0.13.4 @@ -3819,6 +3992,8 @@ snapshots: depd@2.0.0: {} + destr@2.0.5: {} + detect-libc@2.1.2: optional: true @@ -4053,6 +4228,18 @@ snapshots: graceful-fs@4.2.11: {} + h3@1.15.5: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.4 + radix3: 1.1.2 + ufo: 1.6.3 + uncrypto: 0.1.3 + has-flag@4.0.0: {} hasown@2.0.2: @@ -4124,6 +4311,8 @@ snapshots: jsbn: 1.1.0 sprintf-js: 1.1.3 + iron-webcrypto@1.2.1: {} + is-binary-path@2.1.0: dependencies: binary-extensions: 2.3.0 @@ -4226,6 +4415,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.6: {} + lru-cache@6.0.0: dependencies: yallist: 4.0.0 @@ -4305,13 +4496,23 @@ snapshots: node-abort-controller@3.1.1: {} + node-fetch-native@1.6.7: {} + node-gyp-build-optional-packages@5.2.2: dependencies: detect-libc: 2.1.2 optional: true + node-mock-http@1.0.4: {} + normalize-path@3.0.0: {} + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -4537,6 +4738,8 @@ snapshots: queue-tick@1.0.1: {} + radix3@1.1.2: {} + range-parser@1.2.1: {} rc@1.2.8: @@ -4591,6 +4794,8 @@ snapshots: dependencies: picomatch: 4.0.2 + readdirp@5.0.0: {} + redis-errors@1.2.0: {} redis-parser@3.0.0: @@ -4813,12 +5018,29 @@ snapshots: json-stringify-safe: 5.0.1 optional: true + ufo@1.6.3: {} + + uncrypto@0.1.3: {} + undici-types@7.16.0: {} undici-types@7.18.2: {} universalify@2.0.1: {} + unstorage@1.17.4(ioredis@5.9.3): + dependencies: + anymatch: 3.1.3 + chokidar: 5.0.0 + destr: 2.0.5 + h3: 1.15.5 + lru-cache: 11.2.6 + node-fetch-native: 1.6.7 + ofetch: 1.5.1 + ufo: 1.6.3 + optionalDependencies: + ioredis: 5.9.3 + util-deprecate@1.0.2: {} uuid@11.1.0: {} diff --git a/src/auth/models/user.ts b/src/auth/models/user.ts index fec21ca..a666514 100644 --- a/src/auth/models/user.ts +++ b/src/auth/models/user.ts @@ -7,7 +7,7 @@ import { cryptPwd } from '../oauth/salt.ts'; import { OauthUser } from '../oauth/oauth.ts'; import { db } from '../../modules/db.ts'; import { Org } from './org.ts'; - +import { UserSecret } from './user-secret.ts'; import { cfUser, cfOrgs, cfUserSecrets } from '../../db/drizzle/schema.ts'; import { eq, sql, InferSelectModel, InferInsertModel } from 'drizzle-orm'; @@ -17,6 +17,7 @@ export type UserData = { wxUnionId?: string; phone?: string; canChangeUsername?: boolean; + cnbId?: string; }; export enum UserTypes { @@ -95,7 +96,6 @@ export class User { * @returns */ static async verifyToken(token: string) { - const { UserSecret } = await import('./user-secret.ts'); return await UserSecret.verifyToken(token); } /** @@ -108,7 +108,6 @@ export class User { return { accessToken: token.accessToken, refreshToken: token.refreshToken, token: token.accessToken }; } static async getOauthUser(token: string) { - const { UserSecret } = await import('./user-secret.ts'); return await UserSecret.verifyToken(token); } /** @@ -126,7 +125,6 @@ export class User { * @returns */ static async getUserByToken(token: string) { - const { UserSecret } = await import('./user-secret.ts'); const oauthUser = await UserSecret.verifyToken(token); if (!oauthUser) { throw new CustomError('Token is invalid. get UserByToken'); @@ -176,6 +174,20 @@ export class User { return users.length > 0 ? new User(users[0]) : null; } + /** + * 根据 CNB ID 查找用户 + * @param cnbId + * @returns + */ + static async findByCnbId(cnbId: string): Promise { + const users = await db + .select() + .from(usersTable) + .where(sql`${usersTable.data}->>'cnbId' = ${cnbId}`) + .limit(1); + return users.length > 0 ? new User(users[0]) : null; + } + /** * 根据条件查找一个用户 */ @@ -193,7 +205,7 @@ export class User { const users = await query.limit(1); return users.length > 0 ? new User(users[0]) : null; } - static findByunionid(){ + static findByunionid() { } @@ -345,7 +357,7 @@ export class User { if (this.tokenUser && this.tokenUser.uid) { id = this.tokenUser.uid; } else { - throw new CustomError(400, 'Permission denied'); + throw new CustomError('Permission denied', { code: 400 }); } } const cache = await redis.get(`user:${id}:orgs`); diff --git a/src/auth/oauth/oauth.ts b/src/auth/oauth/oauth.ts index d595e8e..5880dd9 100644 --- a/src/auth/oauth/oauth.ts +++ b/src/auth/oauth/oauth.ts @@ -147,6 +147,7 @@ export class RedisTokenStore implements Store { // 计算过期时间,根据opts.expire 和 opts.loginType // 如果expire存在,则使用expire,否则使用opts.loginType 进行计算; let expire = opts?.expire; + const day = 24 * 60 * 60; // 一天的秒数 if (!expire) { switch (opts.loginType) { case 'day': @@ -170,7 +171,8 @@ export class RedisTokenStore implements Store { await this.set(accessToken, JSON.stringify(value), expire); await this.set(userPrefix + ':token:' + accessToken, accessToken, expire); - let refreshTokenExpiresIn = Math.min(expire * 7, 60 * 60 * 24 * 30, 60 * 60 * 24 * 365); // 最大为一年 + // refreshToken的过期时间比accessToken多2天,确保在accessToken过期后,refreshToken仍然有效 + let refreshTokenExpiresIn = expire + 2 * day; if (refreshToken) { // 小于7天, 则设置为7天 if (refreshTokenExpiresIn < 60 * 60 * 24 * 7) { diff --git a/src/routes/config/services/share.ts b/src/routes/config/services/share.ts index 3397cbc..95e1489 100644 --- a/src/routes/config/services/share.ts +++ b/src/routes/config/services/share.ts @@ -18,14 +18,14 @@ export class ShareConfigService { shareCacheConfig = JSON.parse(shareCacheConfigString); } catch (e) { await redis.set(`config:share:${username}:${key}`, '', 'EX', 0); // 删除缓存 - throw new CustomError(400, 'config parse error'); + throw new CustomError(400, { message: 'config parse error' }); } const owner = username; if (shareCacheConfig) { const permission = new UserPermission({ permission: (shareCacheConfig?.data as any)?.permission, owner }); const result = permission.checkPermissionSuccess(options); if (!result.success) { - throw new CustomError(403, 'no permission'); + throw new CustomError(403, { message: 'no permission' }); } return shareCacheConfig; } @@ -35,7 +35,7 @@ export class ShareConfigService { .limit(1); const user = users[0]; if (!user) { - throw new CustomError(404, 'user not found'); + throw new CustomError(404, { message: 'user not found' }); } const configs = await db.select() .from(schema.kvConfig) @@ -43,12 +43,12 @@ export class ShareConfigService { .limit(1); const config = configs[0]; if (!config) { - throw new CustomError(404, 'config not found'); + throw new CustomError(404, { message: 'config not found' }); } const permission = new UserPermission({ permission: (config?.data as any)?.permission, owner }); const result = permission.checkPermissionSuccess(options); if (!result.success) { - throw new CustomError(403, 'no permission'); + throw new CustomError(403, { message: 'no permission' }); } await redis.set(`config:share:${username}:${key}`, JSON.stringify(config), 'EX', 60 * 60 * 24 * 7); // 7天 return config; diff --git a/src/routes/user/admin/user.ts b/src/routes/user/admin/user.ts index f62d1c1..19a698a 100644 --- a/src/routes/user/admin/user.ts +++ b/src/routes/user/admin/user.ts @@ -9,18 +9,18 @@ import { eq } from 'drizzle-orm'; export const checkUsername = (username: string) => { if (username.length > 30) { - throw new CustomError(400, '用户名不能过长'); + throw new CustomError(400, { message: '用户名不能过长' }); } if (!/^[a-zA-Z0-9_@]+$/.test(username)) { - throw new CustomError(400, '用户名包含非法字符'); + throw new CustomError(400, { message: '用户名包含非法字符' }); } if (username.includes(' ')) { - throw new CustomError(400, '用户名不能包含空格'); + throw new CustomError(400, { message: '用户名不能包含空格' }); } }; export const checkUsernameShort = (username: string) => { if (username.length <= 3) { - throw new CustomError(400, '用户名不能过短'); + throw new CustomError(400, { message: '用户名不能过短' }); } }; @@ -31,13 +31,13 @@ export const toChangeName = async (opts: { id: string; newName: string; admin?: } const user = await User.findByPk(id); if (!user) { - ctx.throw(404, 'User not found'); + ctx.throw(404, { message: 'User not found' }); } const oldName = user.username; checkUsername(newName); const findUserByUsername = await User.findOne({ username: newName }); if (findUserByUsername) { - ctx.throw(400, 'Username already exists'); + ctx.throw(400, { message: 'Username already exists' }); } user.username = newName; const data = user.data || {}; @@ -65,7 +65,7 @@ export const toChangeName = async (opts: { id: string; newName: string; admin?: } } catch (error) { console.error('迁移文件数据失败', error); - ctx.throw(500, 'Failed to change username'); + ctx.throw(500, { message: 'Failed to change username' }); } return user; } @@ -79,13 +79,13 @@ app const { id, newName } = ctx.query.data || {}; try { if (!id || !newName) { - ctx.throw(400, '参数错误'); + ctx.throw(400, { message: '参数错误' }); } const user = await toChangeName({ id, newName, admin: true, ctx }); ctx.body = await user?.getInfo?.(); } catch (error) { console.error('changeName error', error); - ctx.throw(500, 'Failed to change username'); + ctx.throw(500, { message: 'Failed to change username' }); } }) .addTo(app); @@ -99,7 +99,7 @@ app .define(async (ctx) => { const { username } = ctx.query.data || {}; if (!username) { - ctx.throw(400, 'Username is required'); + ctx.throw(400, { message: 'Username is required' }); } checkUsername(username); const user = await User.findOne({ username }); @@ -121,7 +121,7 @@ app const { id, password } = ctx.query.data || {}; const user = await User.findByPk(id); if (!user) { - ctx.throw(404, 'User not found'); + ctx.throw(404, { message: 'User not found' }); } let pwd = password || nanoid(6); user.createPassword(pwd); @@ -149,7 +149,7 @@ app checkUsername(username); const findUserByUsername = await User.findOne({ username }); if (findUserByUsername) { - ctx.throw(400, 'Username already exists'); + ctx.throw(400, { message: 'Username already exists' }); } let pwd = password || nanoid(6); const user = await User.createUser(username, pwd, description); @@ -172,7 +172,7 @@ app const { id } = ctx.query.data || {}; const user = await User.findByPk(id); if (!user) { - ctx.throw(404, 'User not found'); + ctx.throw(404, { message: 'User not found' }); } await db.delete(schema.cfUser).where(eq(schema.cfUser.id, user.id)); backupUserA(user.username, user.id); diff --git a/src/routes/user/cnb-login.ts b/src/routes/user/cnb-login.ts new file mode 100644 index 0000000..ba156c7 --- /dev/null +++ b/src/routes/user/cnb-login.ts @@ -0,0 +1,30 @@ +import { app, redis } from "@/app.ts"; +import z from "zod"; +import { CnbServices } from "./modules/cnb-services.ts"; +import { createCookie } from "./me.ts"; +app + .route({ + path: 'user', + key: 'cnb-login', + description: 'cnb登陆, 根据 CNB_TOKEN 获取用户信息', + metadata: { + args: { + data: z.object({ + cnbToken: z.string().describe('cnb token'), + }), + } + } + }) + .define(async (ctx) => { + const { cnbToken } = ctx.query.data || {}; + if (!cnbToken) { + ctx.throw(400, 'CNB Token is required'); + } + const cnb = new CnbServices(cnbToken); + const token = await cnb.login(); + if (!token) { + ctx.throw(500, '登陆失败'); + } + createCookie(token, ctx); + ctx.body = token; + }).addTo(app); \ No newline at end of file diff --git a/src/routes/user/index.ts b/src/routes/user/index.ts index 33ca6ea..233bfe0 100644 --- a/src/routes/user/index.ts +++ b/src/routes/user/index.ts @@ -16,3 +16,5 @@ import './admin/user.ts'; import './secret-key/list.ts'; import './wx-login.ts' + +import './cnb-login.ts'; \ No newline at end of file diff --git a/src/routes/user/list.ts b/src/routes/user/list.ts index 422f1cf..1cca469 100644 --- a/src/routes/user/list.ts +++ b/src/routes/user/list.ts @@ -35,15 +35,15 @@ app const tokenUser = ctx.state.tokenUser; const { id, username, password, description } = ctx.query.data || {}; if (!id) { - throw new CustomError(400, 'id is required'); + throw new CustomError(400, { message: 'id is required' }); } const user = await User.findByPk(id); if (user.id !== tokenUser.id) { - throw new CustomError(403, 'Permission denied'); + throw new CustomError(403, { message: 'Permission denied' }); } if (!user) { - throw new CustomError(500, 'user not found'); + throw new CustomError(500, { message: 'user not found' }); } if (username) { user.username = username; @@ -73,12 +73,12 @@ app .define(async (ctx) => { const { username, password, description } = ctx.query.data || {}; if (!username) { - throw new CustomError(400, 'username is required'); + throw new CustomError(400, { message: 'username is required' }); } checkUsername(username); const findUserByUsername = await User.findOne({ username }); if (findUserByUsername) { - throw new CustomError(400, 'username already exists'); + throw new CustomError(400, { message: 'username already exists' }); } const pwd = password || nanoid(6); const user = await User.createUser(username, pwd, description); diff --git a/src/routes/user/me.ts b/src/routes/user/me.ts index 07179c9..1e2f53c 100644 --- a/src/routes/user/me.ts +++ b/src/routes/user/me.ts @@ -1,19 +1,24 @@ import { app } from '@/app.ts'; -import { Org } from '@/models/org.ts'; import { User } from '@/models/user.ts'; import { proxyDomain as domain } from '@/modules/domain.ts'; +import { logger } from '@/modules/logger.ts'; +import z from 'zod'; /** * 当配置了domain后,创建cookie,当get请求地址的时候,会自动带上cookie * @param token * @param ctx * @returns */ -export const createCookie = (token: any, ctx: any) => { +export const createCookie = (token: { accessToken?: string; token?: string }, ctx: any) => { if (!domain) { return; } + if (!ctx?.req) { + logger.debug('登陆用户没有请求对象,不需要创建cookie'); + return + } //TODO, 获取访问的 hostname, 如果访问的和 domain 的不一致,也创建cookie - const browser = ctx.req.headers['user-agent']; + const browser = ctx?.req?.headers['user-agent']; const isBrowser = browser.includes('Mozilla'); // 浏览器 if (isBrowser && ctx.res.cookie) { ctx.res.cookie('token', token.accessToken || token?.token, { @@ -351,6 +356,14 @@ app .route({ path: 'user', key: 'refreshToken', + description: '根据refreshToken刷新token', + metadata: { + args: { + data: z.object({ + refreshToken: z.string().describe('刷新token'), + }), + } + } }) .define(async (ctx) => { const { refreshToken } = ctx.query.data || {}; diff --git a/src/routes/user/modules/cnb-services.ts b/src/routes/user/modules/cnb-services.ts new file mode 100644 index 0000000..9fa51b8 --- /dev/null +++ b/src/routes/user/modules/cnb-services.ts @@ -0,0 +1,37 @@ +import { CNB } from '@kevisual/cnb' +import { UserModel } from '../../../auth/index.ts'; +import { CustomError } from '@kevisual/router'; + +export class CnbServices { + cnb: CNB; + constructor(token?: string) { + this.cnb = new CNB({ + token, + }); + } + + async login(): Promise> { + const cnbUserRes = await this.cnb.user.getUser(); + if (cnbUserRes.code !== 200) { + throw new CustomError('CNB Token is invalid'); + } + const cnbUser = cnbUserRes?.data; + const cnbUserId = cnbUser?.id + if (!cnbUserId) { + throw new CustomError('CNB User ID is missing'); + } + let user = await UserModel.findByCnbId(cnbUserId); + if (!user) { + const username = '@cnb-' + cnbUser.username; + // 如果用户不存在,创建一个新用户 + user = await UserModel.createUser(username, cnbUserId); + user.data = { + ...user.data, + cnbId: cnbUserId, + } + await user.save(); + } + const token = await user.createToken(); + return token; + } +} \ No newline at end of file diff --git a/src/routes/user/modules/wx-services.ts b/src/routes/user/modules/wx-services.ts index cc54d8c..555887a 100644 --- a/src/routes/user/modules/wx-services.ts +++ b/src/routes/user/modules/wx-services.ts @@ -54,7 +54,7 @@ export class WxServices { const token = await fetchToken(code, type); console.log('login token', token); if (!token.unionid) { - throw new CustomError(400, 'code is invalid, wxdata can not be found'); + throw new CustomError(400, { message: 'code is invalid, wxdata can not be found' }); } this.wxToken = token; const unionid = token.unionid; @@ -180,7 +180,7 @@ export class WxServices { async getUserInfo() { try { if (!this.wxToken) { - throw new CustomError(400, 'wxToken is not set'); + throw new CustomError(400, { message: 'wxToken is not set' }); } const openid = this.wxToken.openid; const access_token = this.wxToken.access_token; diff --git a/src/routes/user/modules/wx.ts b/src/routes/user/modules/wx.ts index 563e5b8..5374963 100644 --- a/src/routes/user/modules/wx.ts +++ b/src/routes/user/modules/wx.ts @@ -45,7 +45,7 @@ export const fetchToken = async (code: string, type: 'open' | 'mp' = 'open'): Pr appSecret = wx.appSecret; } if (!appId || !appSecret) { - throw new CustomError(500, 'appId or appSecret is not set'); + throw new CustomError(500, { message: 'appId or appSecret is not set' }); } console.log('fetchToken===', appId, appSecret, code); const wxUrl = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appId}&secret=${appSecret}&code=${code}&grant_type=authorization_code`; diff --git a/src/routes/user/web-login.ts b/src/routes/user/web-login.ts index e4b9001..c5d4b30 100644 --- a/src/routes/user/web-login.ts +++ b/src/routes/user/web-login.ts @@ -6,12 +6,21 @@ import jsonwebtoken from 'jsonwebtoken'; import { redis } from '@/app.ts'; import { createCookie, clearCookie } from './me.ts'; +import z from 'zod'; app .route({ path: 'user', key: 'webLogin', + description: 'web登录接口,配合插件使用', middleware: [authCan], + metadata: { + args: { + loginToken: z.string().describe('web登录令牌,服务端生成,客户端保持一致'), + sign: z.string().describe('签名,服务端生成,客户端保持一致'), + randomId: z.string().describe('随机字符串,服务端和客户端保持一致'), + } + } }) .define(async (ctx) => { const tokenUser = ctx.state.tokenUser; @@ -97,6 +106,7 @@ app .route({ path: 'user', key: 'checkLoginStatus', + description: '循环检查登陆状态', }) .define(async (ctx) => { const { loginToken } = ctx.query; diff --git a/src/test/cnb-login.ts b/src/test/cnb-login.ts new file mode 100644 index 0000000..7fdfccd --- /dev/null +++ b/src/test/cnb-login.ts @@ -0,0 +1,22 @@ +import { app, showMore, cnbToken } from './common.ts'; + +const res = await app.run({ + path: 'user', + key: 'cnb-login', + payload: { + data: { + cnbToken + } + } +}) +console.log(showMore(res)); + +const token = res.data.token; +const me = await app.run({ + path: 'user', + key: 'me', + payload: { + token + } +}) +console.log(showMore(me)); \ No newline at end of file diff --git a/src/test/common.ts b/src/test/common.ts index 822c458..ad41b0a 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -13,6 +13,7 @@ export { export const config = useConfig(); export const token = config.KEVISUAL_TOKEN || ''; +export const cnbToken = config.CNB_TOKEN || ''; export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); export const showRes = (res, ...args) => {