refactor: 移除阿里云融合认证集成,保留基础设施以备后续使用
移除 fusion-auth-client、融合认证路由和阿里云 SDK 依赖, 同时保留 findOrCreatePhone、appSettings 表、auth-providers 管理端和 /auth/providers 端点等基础设施。
This commit is contained in:
parent
32dd6a3190
commit
0ca06df078
@ -22,14 +22,9 @@ OSS_ACCESS_KEY_SECRET=
|
||||
OSS_BUCKET=
|
||||
OSS_REGION=
|
||||
|
||||
# Alibaba Cloud Fusion Auth (Phase 1e)
|
||||
# 阿里云号码认证服务,用于手机号一键登录和短信验证码登录
|
||||
# RAM 子用户需授予 AliyunDypnsFullAccess 权限
|
||||
# Alibaba Cloud (Phase 1e)
|
||||
ALIYUN_ACCESS_KEY_ID=
|
||||
ALIYUN_ACCESS_KEY_SECRET=
|
||||
ALIYUN_FUSION_SCHEME_CODE_ANDROID=
|
||||
ALIYUN_FUSION_SCHEME_CODE_IOS=
|
||||
ALIYUN_FUSION_SCHEME_CODE_HARMONY=
|
||||
|
||||
# Huawei IAP (Phase 1c)
|
||||
HUAWEI_IAP_URL=
|
||||
|
||||
@ -25,14 +25,9 @@ OSS_ACCESS_KEY_SECRET=
|
||||
OSS_BUCKET=duoqi-assets
|
||||
OSS_REGION=oss-cn-hangzhou
|
||||
|
||||
# Alibaba Cloud Fusion Auth
|
||||
# 阿里云号码认证服务,用于手机号一键登录和短信验证码登录
|
||||
# RAM 子用户需授予 AliyunDypnsFullAccess 权限
|
||||
# Alibaba Cloud
|
||||
ALIYUN_ACCESS_KEY_ID=
|
||||
ALIYUN_ACCESS_KEY_SECRET=
|
||||
ALIYUN_FUSION_SCHEME_CODE_ANDROID=
|
||||
ALIYUN_FUSION_SCHEME_CODE_IOS=
|
||||
ALIYUN_FUSION_SCHEME_CODE_HARMONY=
|
||||
|
||||
# Huawei IAP
|
||||
HUAWEI_IAP_URL=https://subscr-drcn.iap.hicloud.com
|
||||
|
||||
@ -66,7 +66,7 @@ src/
|
||||
│ ├── audit-log.ts # Admin 操作审计日志
|
||||
│ └── request-logger.ts # 请求耗时日志
|
||||
├── services/ # 业务逻辑(按领域分目录)
|
||||
│ ├── auth/ # jwt, guest, huawei-id-kit, fusion-auth-client, phone
|
||||
│ ├── auth/ # jwt, guest, huawei-id-kit, phone
|
||||
│ ├── quiz/ # quiz-service(出题引擎 + 答题验证)
|
||||
│ ├── progress/ # progress, streak, xp, hearts
|
||||
│ ├── gamification/ # leaderboard, achievement
|
||||
|
||||
73
bun.lock
73
bun.lock
@ -5,9 +5,6 @@
|
||||
"": {
|
||||
"name": "duoqi-api",
|
||||
"dependencies": {
|
||||
"@alicloud/dypnsapi20170525": "^2.0.0",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"@alicloud/tea-util": "^1.4.11",
|
||||
"@fastify/cors": "^11.0.0",
|
||||
"@fastify/helmet": "^13.0.0",
|
||||
"@fastify/jwt": "^9.0.0",
|
||||
@ -39,38 +36,6 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alicloud/credentials": ["@alicloud/credentials@2.4.4", "https://registry.npmmirror.com/@alicloud/credentials/-/credentials-2.4.4.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.8.0", "httpx": "^2.3.3", "ini": "^1.3.5", "kitx": "^2.0.0" } }, "sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q=="],
|
||||
|
||||
"@alicloud/darabonba-array": ["@alicloud/darabonba-array@0.1.2", "https://registry.npmmirror.com/@alicloud/darabonba-array/-/darabonba-array-0.1.2.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1" } }, "sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ=="],
|
||||
|
||||
"@alicloud/darabonba-encode-util": ["@alicloud/darabonba-encode-util@0.0.2", "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.2.tgz", { "dependencies": { "moment": "^2.29.1" } }, "sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A=="],
|
||||
|
||||
"@alicloud/darabonba-map": ["@alicloud/darabonba-map@0.0.1", "https://registry.npmmirror.com/@alicloud/darabonba-map/-/darabonba-map-0.0.1.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1" } }, "sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw=="],
|
||||
|
||||
"@alicloud/darabonba-signature-util": ["@alicloud/darabonba-signature-util@0.0.4", "https://registry.npmmirror.com/@alicloud/darabonba-signature-util/-/darabonba-signature-util-0.0.4.tgz", { "dependencies": { "@alicloud/darabonba-encode-util": "^0.0.1" } }, "sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg=="],
|
||||
|
||||
"@alicloud/darabonba-string": ["@alicloud/darabonba-string@1.0.3", "https://registry.npmmirror.com/@alicloud/darabonba-string/-/darabonba-string-1.0.3.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1" } }, "sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA=="],
|
||||
|
||||
"@alicloud/dypnsapi20170525": ["@alicloud/dypnsapi20170525@2.0.0", "https://registry.npmmirror.com/@alicloud/dypnsapi20170525/-/dypnsapi20170525-2.0.0.tgz", { "dependencies": { "@alicloud/openapi-core": "^1.0.0", "@darabonba/typescript": "^1.0.0" } }, "sha512-eVh1dJ2HA82bBHt+YZFIBzPEYW80FK+TSpcxSR9o0W+FgfTqBaj6eeIHnN7NFhyDAD/3+HtZ146Pmvr51JEEAg=="],
|
||||
|
||||
"@alicloud/endpoint-util": ["@alicloud/endpoint-util@0.0.1", "https://registry.npmmirror.com/@alicloud/endpoint-util/-/endpoint-util-0.0.1.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "kitx": "^2.0.0" } }, "sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg=="],
|
||||
|
||||
"@alicloud/gateway-pop": ["@alicloud/gateway-pop@0.0.6", "https://registry.npmmirror.com/@alicloud/gateway-pop/-/gateway-pop-0.0.6.tgz", { "dependencies": { "@alicloud/credentials": "^2", "@alicloud/darabonba-array": "^0.1.0", "@alicloud/darabonba-encode-util": "^0.0.2", "@alicloud/darabonba-map": "^0.0.1", "@alicloud/darabonba-signature-util": "^0.0.4", "@alicloud/darabonba-string": "^1.0.2", "@alicloud/endpoint-util": "^0.0.1", "@alicloud/gateway-spi": "^0.0.8", "@alicloud/openapi-util": "^0.3.2", "@alicloud/tea-typescript": "^1.7.1", "@alicloud/tea-util": "^1.4.8" } }, "sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA=="],
|
||||
|
||||
"@alicloud/gateway-spi": ["@alicloud/gateway-spi@0.0.8", "https://registry.npmmirror.com/@alicloud/gateway-spi/-/gateway-spi-0.0.8.tgz", { "dependencies": { "@alicloud/credentials": "^2", "@alicloud/tea-typescript": "^1.7.1" } }, "sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g=="],
|
||||
|
||||
"@alicloud/openapi-client": ["@alicloud/openapi-client@0.4.15", "https://registry.npmmirror.com/@alicloud/openapi-client/-/openapi-client-0.4.15.tgz", { "dependencies": { "@alicloud/credentials": "^2.4.2", "@alicloud/gateway-spi": "^0.0.8", "@alicloud/openapi-util": "^0.3.2", "@alicloud/tea-typescript": "^1.7.1", "@alicloud/tea-util": "1.4.9", "@alicloud/tea-xml": "0.0.3" } }, "sha512-4VE0/k5ZdQbAhOSTqniVhuX1k5DUeUMZv74degn3wIWjLY6Bq+hxjaGsaHYlLZ2gA5wUrs8NcI5TE+lIQS3iiA=="],
|
||||
|
||||
"@alicloud/openapi-core": ["@alicloud/openapi-core@1.0.7", "https://registry.npmmirror.com/@alicloud/openapi-core/-/openapi-core-1.0.7.tgz", { "dependencies": { "@alicloud/credentials": "^2.4.2", "@alicloud/gateway-pop": "0.0.6", "@alicloud/gateway-spi": "^0.0.8", "@darabonba/typescript": "^1.0.2" } }, "sha512-I80PQVfmlzRiXGHwutMp2zTpiqUVv8ts30nWAfksfHUSTIapk3nj9IXaPbULMPGNV6xqEyshO2bj2a+pmwc2tQ=="],
|
||||
|
||||
"@alicloud/openapi-util": ["@alicloud/openapi-util@0.3.3", "https://registry.npmmirror.com/@alicloud/openapi-util/-/openapi-util-0.3.3.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1", "@alicloud/tea-util": "^1.3.0", "kitx": "^2.1.0", "sm3": "^1.0.3" } }, "sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A=="],
|
||||
|
||||
"@alicloud/tea-typescript": ["@alicloud/tea-typescript@1.8.0", "https://registry.npmmirror.com/@alicloud/tea-typescript/-/tea-typescript-1.8.0.tgz", { "dependencies": { "@types/node": "^12.0.2", "httpx": "^2.2.6" } }, "sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ=="],
|
||||
|
||||
"@alicloud/tea-util": ["@alicloud/tea-util@1.4.11", "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.11.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "@darabonba/typescript": "^1.0.0", "kitx": "^2.0.0" } }, "sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww=="],
|
||||
|
||||
"@alicloud/tea-xml": ["@alicloud/tea-xml@0.0.3", "https://registry.npmmirror.com/@alicloud/tea-xml/-/tea-xml-0.0.3.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1", "@types/xml2js": "^0.4.5", "xml2js": "^0.6.0" } }, "sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
|
||||
@ -81,8 +46,6 @@
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
|
||||
|
||||
"@darabonba/typescript": ["@darabonba/typescript@1.0.4", "https://registry.npmmirror.com/@darabonba/typescript/-/typescript-1.0.4.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "httpx": "^2.3.2", "lodash": "^4.17.21", "moment": "^2.30.1", "moment-timezone": "^0.5.45", "xml2js": "^0.6.2" } }, "sha512-icl8RGTw4DiWRpco6dVh21RS0IqrH4s/eEV36TZvz/e1+paogSZjaAgox7ByrlEuvG+bo5d8miq/dRlqiUaL/w=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "https://registry.npmmirror.com/@drizzle-team/brocli/-/brocli-0.10.2.tgz", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.9.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
|
||||
@ -257,8 +220,6 @@
|
||||
|
||||
"@types/uuid": ["@types/uuid@10.0.0", "https://registry.npmmirror.com/@types/uuid/-/uuid-10.0.0.tgz", {}, "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ=="],
|
||||
|
||||
"@types/xml2js": ["@types/xml2js@0.4.14", "https://registry.npmmirror.com/@types/xml2js/-/xml2js-0.4.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.1", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.58.1.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw=="],
|
||||
@ -461,8 +422,6 @@
|
||||
|
||||
"html-escaper": ["html-escaper@2.0.2", "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
|
||||
|
||||
"httpx": ["httpx@2.3.3", "https://registry.npmmirror.com/httpx/-/httpx-2.3.3.tgz", { "dependencies": { "@types/node": "^20", "debug": "^4.1.1" } }, "sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.2.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@ -473,8 +432,6 @@
|
||||
|
||||
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ini": ["ini@1.3.8", "https://registry.npmmirror.com/ini/-/ini-1.3.8.tgz", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@2.3.0", "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.3.0.tgz", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
@ -509,8 +466,6 @@
|
||||
|
||||
"keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kitx": ["kitx@2.2.0", "https://registry.npmmirror.com/kitx/-/kitx-2.2.0.tgz", { "dependencies": { "@types/node": "^22.5.4" } }, "sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg=="],
|
||||
|
||||
"levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"light-my-request": ["light-my-request@6.6.0", "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz", { "dependencies": { "cookie": "^1.0.1", "process-warning": "^4.0.0", "set-cookie-parser": "^2.6.0" } }, "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A=="],
|
||||
@ -541,8 +496,6 @@
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash": ["lodash@4.18.1", "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"long": ["long@5.3.2", "https://registry.npmmirror.com/long/-/long-5.3.2.tgz", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
@ -563,10 +516,6 @@
|
||||
|
||||
"mnemonist": ["mnemonist@0.40.3", "https://registry.npmmirror.com/mnemonist/-/mnemonist-0.40.3.tgz", { "dependencies": { "obliterator": "^2.0.4" } }, "sha512-Vjyr90sJ23CKKH/qPAgUKicw/v6pRoamxIEDFOF8uSgFME7DqPRpHgRTejWVjkdGg5dXj0/NyxZHZ9bcjH+2uQ=="],
|
||||
|
||||
"moment": ["moment@2.30.1", "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="],
|
||||
|
||||
"moment-timezone": ["moment-timezone@0.5.48", "https://registry.npmmirror.com/moment-timezone/-/moment-timezone-0.5.48.tgz", { "dependencies": { "moment": "^2.29.4" } }, "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mysql2": ["mysql2@3.20.0", "https://registry.npmmirror.com/mysql2/-/mysql2-3.20.0.tgz", { "dependencies": { "aws-ssl-profiles": "^1.1.2", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.2", "long": "^5.3.2", "lru.min": "^1.1.4", "named-placeholders": "^1.1.6", "sql-escaper": "^1.3.3" }, "peerDependencies": { "@types/node": ">= 8" } }, "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg=="],
|
||||
@ -647,8 +596,6 @@
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"sax": ["sax@1.6.0", "https://registry.npmmirror.com/sax/-/sax-1.6.0.tgz", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="],
|
||||
|
||||
"secure-json-parse": ["secure-json-parse@4.1.0", "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-4.1.0.tgz", {}, "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
@ -661,8 +608,6 @@
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"sm3": ["sm3@1.0.3", "https://registry.npmmirror.com/sm3/-/sm3-1.0.3.tgz", {}, "sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g=="],
|
||||
|
||||
"sonic-boom": ["sonic-boom@4.2.1", "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.1.tgz", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
@ -727,22 +672,12 @@
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"xml2js": ["xml2js@0.6.2", "https://registry.npmmirror.com/xml2js/-/xml2js-0.6.2.tgz", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="],
|
||||
|
||||
"xmlbuilder": ["xmlbuilder@11.0.1", "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "https://registry.npmmirror.com/zod/-/zod-3.25.76.tgz", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"@alicloud/darabonba-signature-util/@alicloud/darabonba-encode-util": ["@alicloud/darabonba-encode-util@0.0.1", "https://registry.npmmirror.com/@alicloud/darabonba-encode-util/-/darabonba-encode-util-0.0.1.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.7.1", "moment": "^2.29.1" } }, "sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw=="],
|
||||
|
||||
"@alicloud/openapi-client/@alicloud/tea-util": ["@alicloud/tea-util@1.4.9", "https://registry.npmmirror.com/@alicloud/tea-util/-/tea-util-1.4.9.tgz", { "dependencies": { "@alicloud/tea-typescript": "^1.5.1", "kitx": "^2.0.0" } }, "sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw=="],
|
||||
|
||||
"@alicloud/tea-typescript/@types/node": ["@types/node@12.20.55", "https://registry.npmmirror.com/@types/node/-/node-12.20.55.tgz", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
@ -761,10 +696,6 @@
|
||||
|
||||
"fast-json-stringify/ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"httpx/@types/node": ["@types/node@20.19.41", "https://registry.npmmirror.com/@types/node/-/node-20.19.41.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="],
|
||||
|
||||
"kitx/@types/node": ["@types/node@22.19.19", "https://registry.npmmirror.com/@types/node/-/node-22.19.19.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew=="],
|
||||
|
||||
"light-my-request/process-warning": ["process-warning@4.0.1", "https://registry.npmmirror.com/process-warning/-/process-warning-4.0.1.tgz", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="],
|
||||
|
||||
"pino/pino-abstract-transport": ["pino-abstract-transport@2.0.0", "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="],
|
||||
@ -823,10 +754,6 @@
|
||||
|
||||
"fast-json-stringify/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"httpx/@types/node/undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"kitx/@types/node/undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
|
||||
|
||||
"tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
|
||||
|
||||
@ -19,7 +19,7 @@
|
||||
|
||||
| 类型 | Header | 适用路径 |
|
||||
|------|--------|----------|
|
||||
| 无需认证 | - | `/health`, `/v1/auth/guest`, `/v1/auth/huawei`, `/v1/auth/refresh`, `/v1/auth/fusion/token`, `/v1/auth/fusion/verify`, `/v1/auth/providers`, `/v1/admin/login` |
|
||||
| 无需认证 | - | `/health`, `/v1/auth/guest`, `/v1/auth/huawei`, `/v1/auth/refresh`, `/v1/auth/providers`, `/v1/admin/login` |
|
||||
| JWT(游客) | `Authorization: Bearer <jwt_token>` | `/v1/auth/link` |
|
||||
| JWT | `Authorization: Bearer <jwt_token>` | 大多数客户端 API |
|
||||
| Admin JWT | `Authorization: Bearer <admin_jwt_token>` | `/v1/admin/*` |
|
||||
@ -256,7 +256,7 @@
|
||||
认证:无
|
||||
限流:10 次/分钟
|
||||
|
||||
当前已注册但未实现,固定返回 HTTP 501。手机号登录请使用融合认证接口(`/auth/fusion/token` + `/auth/fusion/verify`)。
|
||||
当前已注册但未实现,固定返回 HTTP 501。
|
||||
|
||||
```json
|
||||
{
|
||||
@ -269,104 +269,6 @@
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /auth/fusion/token
|
||||
|
||||
认证:无
|
||||
限流:10 次/分钟
|
||||
|
||||
用途:获取阿里云融合认证 SDK 鉴权 Token。客户端在初始化 SDK 前调用此接口。`schemeCode` 和 `durationSeconds` 由服务端配置,不暴露给客户端。
|
||||
|
||||
请求:
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `platform` | `"Android"` \| `"iOS"` \| `"Harmony"` | 是 | 客户端平台 |
|
||||
| `packageName` | string | Android 必填 | App 包名 |
|
||||
| `packageSign` | string | Android 必填 | App 包签名(MD5,去冒号小写) |
|
||||
| `bundleId` | string | iOS 必填 | App Bundle ID |
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": "Android",
|
||||
"packageName": "com.example.app",
|
||||
"packageSign": "a1b2c3d4e5f6..."
|
||||
}
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"fusionAuthToken": "FKcksloqk***********jalEc+"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
错误响应(服务未配置):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "SERVICE_UNAVAILABLE",
|
||||
"message": "Fusion auth is not configured on the server"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /auth/fusion/verify
|
||||
|
||||
认证:无
|
||||
限流:10 次/分钟
|
||||
|
||||
用途:客户端融合认证 SDK 完成认证后,将运营商返回的 verifyToken 发送到此端点。服务端用 verifyToken 向阿里云换取手机号,完成登录或自动注册。一键登录和短信验证码登录均使用此端点。
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"verifyToken": "LD108enNdlsl*******sFLKCks1=="
|
||||
}
|
||||
```
|
||||
|
||||
成功响应(同 `/auth/guest`):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"nickname": null,
|
||||
"avatarUrl": null,
|
||||
"tier": "free"
|
||||
},
|
||||
"tokens": {
|
||||
"accessToken": "jwt",
|
||||
"refreshToken": "jwt"
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
错误响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "UNAUTHORIZED",
|
||||
"message": "Phone verification failed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /auth/providers
|
||||
|
||||
认证:无
|
||||
@ -380,7 +282,7 @@
|
||||
| `platform` | `'ios' \| 'android' \| 'harmony'` | 是 | 客户端平台 |
|
||||
|
||||
平台过滤规则:
|
||||
- 一键登录 / 短信验证码:全平台
|
||||
- 短信验证码:全平台
|
||||
- 华为账号:仅 Android、Harmony
|
||||
- Apple 账号:仅 iOS
|
||||
- 微信 / QQ:全平台,enabled 由数据库配置控制
|
||||
@ -392,7 +294,6 @@
|
||||
"success": true,
|
||||
"data": {
|
||||
"providers": [
|
||||
{ "id": "fusion", "name": "一键登录", "type": "primary", "enabled": true },
|
||||
{ "id": "phone_sms", "name": "短信验证码登录", "type": "secondary", "enabled": true },
|
||||
{ "id": "apple", "name": "通过 Apple 登录", "type": "third_party", "iconKey": "apple", "enabled": true },
|
||||
{ "id": "wechat", "name": "通过微信登录", "type": "third_party", "iconKey": "wechat", "enabled": false }
|
||||
|
||||
@ -1,422 +0,0 @@
|
||||
# 阿里云融合认证集成 — 设计与实施计划
|
||||
|
||||
> Phase 1e: Phone Number Authentication
|
||||
> Created: 2026-05-27
|
||||
> Status: Implemented
|
||||
|
||||
## 概述
|
||||
|
||||
集成阿里云号码认证服务的**融合认证**方案,为多奇平台提供手机号一键登录能力。融合认证是一种校验用户手机号的云原生服务,集成了号码认证(运营商网关取号)、短信验证码、语音验证码等通信功能。
|
||||
|
||||
### 目标
|
||||
|
||||
- 新增 `POST /auth/fusion/token` 端点,为客户端 SDK 提供鉴权 Token(对齐 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5.1 节)
|
||||
- 新增 `POST /auth/fusion/verify` 端点,用运营商 verifyToken 换取手机号并完成登录(对齐 Flutter 客户端第 5.2 节)
|
||||
- 支持游客账号通过手机号关联升级为正式账号
|
||||
|
||||
### 不在范围内
|
||||
|
||||
- 短信验证码/语音验证码登录(号码认证降级到短信时由阿里云模板自动处理,服务端感知不变)
|
||||
- Flutter 客户端 SDK 集成(客户端侧独立进行,见 `duoqi-flutter/docs/FUSION_AUTH_INTEGRATION.md`)
|
||||
- 修改现有 `POST /auth/phone`(保留 501 占位,未来可用于非融合认证的手机登录)
|
||||
|
||||
---
|
||||
|
||||
## 架构分析
|
||||
|
||||
### 现有认证架构
|
||||
|
||||
项目已实现 `guest`、`huawei`、`apple` 三种登录方式,采用统一的 `findOrCreate*` 模式:
|
||||
|
||||
```
|
||||
客户端 ──▶ 路由层(Zod校验) ──▶ 服务层(业务逻辑) ──▶ 数据库(users表)
|
||||
│
|
||||
signTokens() + buildLoginResponse()
|
||||
```
|
||||
|
||||
关键设计:
|
||||
- `users` 表以 `(authType, authId)` 唯一索引标识用户
|
||||
- `authType` 枚举已包含 `'phone'`,`authId` 用于存储手机号
|
||||
- JWT payload 携带 `{ userId, authType, tier }`
|
||||
- 登录响应统一为 `{ user, tokens }` 结构
|
||||
|
||||
### 融合认证集成点
|
||||
|
||||
融合认证引入一个**两步交互**流程,比现有的华为/Apple 认证多一个"预取 Token"步骤:
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────────┐ ┌──────────────────┐
|
||||
│ 客户端 │ │ duoqi-api │ │ 阿里云 Dypnsapi │
|
||||
└────┬─────┘ └──────┬───────┘ └────────┬─────────┘
|
||||
│ │ │
|
||||
│ ① POST /auth/fusion/token │
|
||||
│ { platform, packageName?, bundleId? } │
|
||||
│───────────────────▶│ │
|
||||
│ │ ② GetFusionAuthToken │
|
||||
│ │────────────────────────▶│
|
||||
│ │◀── { authToken } ────────│
|
||||
│◀── { fusionAuthToken } ──│ │
|
||||
│ │ │
|
||||
│ [客户端 SDK 使用 authToken 完成认证] │
|
||||
│ [获得 verifyToken] │ │
|
||||
│ │ │
|
||||
│ ③ POST /auth/fusion/verify │
|
||||
│ { verifyToken } │ │
|
||||
│───────────────────▶│ │
|
||||
│ │ ④ VerifyWithFusionAuthToken
|
||||
│ │────────────────────────▶│
|
||||
│ │◀── { phoneNumber, verifyResult, phoneScore }
|
||||
│ │ │
|
||||
│ │ ⑤ 查找或创建用户,签发JWT │
|
||||
│◀── { user, tokens } │ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
> 路由路径对齐 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5 节的定义。
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策
|
||||
|
||||
### 决策 1:手机号存储格式
|
||||
|
||||
**选择:存储阿里云返回的原始手机号字符串(如 `18012341234`)**
|
||||
|
||||
理由:
|
||||
- `VerifyWithFusionAuthToken` 在号码认证通过时返回完整手机号(非掩码)
|
||||
- 完整手机号作为 `authId` 可确保用户唯一性
|
||||
- 掩码号(如 `180****1234`)仅出现在日志或审计场景,不用于身份标识
|
||||
|
||||
替代方案(未采纳):
|
||||
- 存储掩码号:无法唯一标识用户,不可行
|
||||
- 对手机号做哈希:丧失可读性,且哈希碰撞风险虽极低但非零
|
||||
|
||||
### 决策 2:authToken 有效期
|
||||
|
||||
**选择:900 秒(15 分钟,API 允许的最小值)**
|
||||
|
||||
理由:
|
||||
- 融合认证的典型交互在几秒到几十秒内完成
|
||||
- 更短的 Token 有效期意味着更小的被盗用风险窗口
|
||||
- 与项目中 access_token 1 小时、refresh_token 30 天的分级策略一致——authToken 作为临时凭证应有最短有效期
|
||||
|
||||
### 决策 3:阿里云 SDK 客户端生命周期
|
||||
|
||||
**选择:懒初始化单例模式**
|
||||
|
||||
理由:
|
||||
- 阿里云 SDK 客户端是线程安全的,可复用
|
||||
- 避免每次请求创建新客户端的开销(HTTP 连接池复用)
|
||||
- 与项目中 `db` 客户端的单例模式保持一致
|
||||
- 仅在配置了阿里云环境变量时才初始化,不影响其他认证方式
|
||||
|
||||
### 决策 4:路由设计
|
||||
|
||||
**选择:新增 `/auth/fusion/token` 和 `/auth/fusion/verify`,独立于 `/auth/phone`**
|
||||
|
||||
理由:
|
||||
- Flutter 客户端已在 `FUSION_AUTH_INTEGRATION.md` 第 5 节明确定义了这两个路径,服务端需对齐
|
||||
- `/auth/fusion/` 作为独立命名空间,语义上更准确——融合认证不只是一键取号,而是由阿里云模板编排的多种认证方式(号码认证、短信、图形验证等)
|
||||
- `/auth/phone` 保留 501 占位,未来可用于非融合认证的原生短信验证码登录等场景
|
||||
|
||||
替代方案(未采纳):
|
||||
- `POST /auth/phone/fusion-token` + 修改 `POST /auth/phone`:路径层级不合理,融合认证不等同于"手机号登录"
|
||||
- 在 `/auth/phone` 请求体中加 `action: 'getToken' | 'verify'`:语义不够明确,增加请求体复杂度
|
||||
|
||||
### 决策 5:账号关联扩展
|
||||
|
||||
**选择:在 `/auth/link` 中支持 `provider: 'phone'`**
|
||||
|
||||
理由:
|
||||
- 已有游客关联华为/Apple 的完整流程(`account-link-service.ts`)
|
||||
- 手机号关联复用相同的 `accountMigrations` 表和幂等逻辑
|
||||
- 仅需扩展 `linkSchema` 的 `provider` 枚举和 `credential` 结构
|
||||
|
||||
---
|
||||
|
||||
## 阿里云 API 参考
|
||||
|
||||
### GetFusionAuthToken
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| Action | `GetFusionAuthToken` |
|
||||
| 授权 | `dypns:GetFusionAuthToken` |
|
||||
| Endpoint | `dypnsapi.aliyuncs.com` |
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 名称 | 类型 | 必填 | 描述 | 示例值 |
|
||||
|------|------|------|------|--------|
|
||||
| SchemeCode | string | 是 | 认证方案 Code | `FA1000*************201` |
|
||||
| Platform | string | 是 | 平台:`Android` 或 `iOS` | `Android` |
|
||||
| PackageName | string | Android 必填 | App 包名 | `com.example.test` |
|
||||
| PackageSign | string | Android 必填 | App 包签名 | `47fcc************************278` |
|
||||
| BundleId | string | iOS 必填 | App bundleId | `com.example.test` |
|
||||
| DurationSeconds | long | 是 | Token 有效时长(秒),范围 900–43200 | `900` |
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Message": "成功",
|
||||
"RequestId": "CC3BB6D2-2FDF-4321-9DCE-B38165CE4C47",
|
||||
"Model": "FKcksloqk***********jalEc+",
|
||||
"Code": "OK",
|
||||
"Success": true
|
||||
}
|
||||
```
|
||||
|
||||
### VerifyWithFusionAuthToken
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|------|
|
||||
| Action | `VerifyWithFusionAuthToken` |
|
||||
| 授权 | `dypns:VerifyWithFusionAuthToken` |
|
||||
| Endpoint | `dypnsapi.aliyuncs.com` |
|
||||
|
||||
**请求参数:**
|
||||
|
||||
| 名称 | 类型 | 必填 | 描述 | 示例值 |
|
||||
|------|------|------|------|--------|
|
||||
| VerifyToken | string | 是 | 客户端 SDK 完成认证后返回的 Token | `LD108enNdlsl*******sFLKCks1==` |
|
||||
|
||||
**响应示例:**
|
||||
|
||||
```json
|
||||
{
|
||||
"Message": "示例值",
|
||||
"RequestId": "CC3BB6D2-2FDF-4321-9DCE-B38165CE4C47",
|
||||
"Model": {
|
||||
"PhoneNumber": "18012341234",
|
||||
"VerifyResult": "PASS",
|
||||
"PhoneScore": 20
|
||||
},
|
||||
"Code": "OK",
|
||||
"Success": true
|
||||
}
|
||||
```
|
||||
|
||||
**关键错误码:**
|
||||
|
||||
| HTTP 状态码 | 错误码 | 描述 |
|
||||
|-------------|--------|------|
|
||||
| 400 | `SmsCodeVerifyFail` | 短信验证码失败(号码认证降级到短信时) |
|
||||
| 400 | `Throttling.System` | 接口被限流 |
|
||||
| 400 | `VerifySchemeNotExist` | 认证方案不存在 |
|
||||
| 400 | `SchemeNotPassed` | 认证方案未通过审核 |
|
||||
| 400 | `Unsupported.Account` | 账号未开通号码认证服务 |
|
||||
| 403 | `UnauthorizedOperation` | 权限校验失败 |
|
||||
| 500 | `SystemError` | 系统异常 |
|
||||
|
||||
---
|
||||
|
||||
## 分步实施计划
|
||||
|
||||
### Phase 1:基础设施(前置条件)
|
||||
|
||||
| # | 任务 | 产出文件 | 验证标准 |
|
||||
|---|------|----------|----------|
|
||||
| 1.1 | 安装阿里云 SDK 依赖 | `package.json` | `@alicloud/dypnsapi20170525`、`@alicloud/openapi-client`、`@alicloud/tea-util` 安装成功 |
|
||||
| 1.2 | 添加环境变量 | `src/utils/config.ts`、`.env.example` | `ALIYUN_ACCESS_KEY_ID`、`ALIYUN_ACCESS_KEY_SECRET`、`ALIYUN_FUSION_SCHEME_CODE` 加入 Zod schema,可选字段 |
|
||||
| 1.3 | 类型检查通过 | — | `bun run typecheck` 无错误 |
|
||||
|
||||
### Phase 2:服务层
|
||||
|
||||
| # | 任务 | 产出文件 | 验证标准 |
|
||||
|---|------|----------|----------|
|
||||
| 2.1 | 创建阿里云客户端封装 | `src/services/auth/fusion-auth-client.ts`(新建) | 懒初始化单例,配置从 `config` 读取 |
|
||||
| 2.2 | 实现 `getFusionAuthToken` | `src/services/auth/fusion-auth-client.ts` | 接收平台参数,调用阿里云 API,返回 Token 字符串 |
|
||||
| 2.3 | 实现 `verifyWithFusionAuthToken` | `src/services/auth/fusion-auth-client.ts` | 接收 verifyToken,调用阿里云 API,返回 `{ phoneNumber, verifyResult }` |
|
||||
| 2.4 | 实现 `findOrCreatePhone` | `src/services/auth/jwt.ts` | 遵循 `findOrCreateGuest` / `findOrCreateHuawei` 相同模式 |
|
||||
| 2.5 | 类型检查通过 | — | `bun run typecheck` 无错误 |
|
||||
|
||||
### Phase 3:路由层
|
||||
|
||||
| # | 任务 | 产出文件 | 验证标准 |
|
||||
|---|------|----------|----------|
|
||||
| 3.1 | 新增 `POST /auth/fusion/token` 路由 | `src/routes/auth.ts` | 接收 `{ platform, packageName?, packageSign?, bundleId? }`,调用 `getFusionAuthToken`,返回 `{ fusionAuthToken }` |
|
||||
| 3.2 | 新增 `POST /auth/fusion/verify` 路由 | `src/routes/auth.ts` | 接收 `{ verifyToken }`,调用 `verifyWithFusionAuthToken` + `findOrCreatePhone`,返回标准 `LoginResponse` |
|
||||
| 3.3 | 扩展 `/auth/link` 支持 phone provider | `src/routes/auth.ts`、`src/services/auth/account-link-service.ts` | `linkSchema.provider` 增加 `'phone'`,credential 结构适配 |
|
||||
| 3.4 | 类型检查通过 | — | `bun run typecheck` 无错误 |
|
||||
|
||||
### Phase 4:测试
|
||||
|
||||
| # | 任务 | 产出文件 | 验证标准 |
|
||||
|---|------|----------|----------|
|
||||
| 4.1 | fusion-auth-client 单元测试 | `src/__tests__/services/fusion-auth.test.ts`(新建) | mock 阿里云 SDK,测试正常流程和错误处理 |
|
||||
| 4.2 | findOrCreatePhone 单元测试 | `src/__tests__/services/auth.test.ts` | 测试新用户创建和已有用户查找 |
|
||||
| 4.3 | 全量测试通过 | — | `bun run test` 全部通过 |
|
||||
|
||||
### Phase 5:文档
|
||||
|
||||
| # | 任务 | 产出文件 | 验证标准 |
|
||||
|---|------|----------|----------|
|
||||
| 5.1 | 更新 API 文档 | `docs/api-reference.md` | 新增 `POST /auth/fusion/token` 和 `POST /auth/fusion/verify` 端点描述 |
|
||||
| 5.2 | 更新 CLAUDE.md 项目结构 | `CLAUDE.md` | services/auth 目录列表新增 `fusion-auth-client` |
|
||||
|
||||
---
|
||||
|
||||
## 新增环境变量
|
||||
|
||||
```bash
|
||||
# .env 新增项(阿里云融合认证)
|
||||
ALIYUN_ACCESS_KEY_ID= # 阿里云 RAM 用户的 AccessKey ID
|
||||
ALIYUN_ACCESS_KEY_SECRET= # 阿里云 RAM 用户的 AccessKey Secret
|
||||
ALIYUN_FUSION_SCHEME_CODE= # 融合认证方案 Code(在阿里云控制台创建)
|
||||
```
|
||||
|
||||
配置特点:
|
||||
- 三个变量均为可选——未配置时不影响游客、华为、Apple 等其他登录方式
|
||||
- 调用融合认证相关接口时检查配置,未配置则返回 `NOT_IMPLEMENTED` 或 `SERVICE_UNAVAILABLE`
|
||||
- AccessKey 通过 RAM 子用户授权,仅需 `AliyunDypnsFullAccess` 权限
|
||||
|
||||
---
|
||||
|
||||
## 新增 API 端点定义
|
||||
|
||||
> 路由路径与 Flutter 客户端 `FUSION_AUTH_INTEGRATION.md` 第 5 节保持一致。
|
||||
|
||||
### POST /auth/fusion/token
|
||||
|
||||
认证:无
|
||||
限流:10 次/分钟
|
||||
|
||||
用途:为客户端融合认证 SDK 提供鉴权 Token。客户端在调用 `FusionAuth.init(schemeCode, authToken)` 时需要此 Token。对应阿里云 `GetFusionAuthToken` API。
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": "Android",
|
||||
"packageName": "com.duoqi.app",
|
||||
"packageSign": "47fcc************************278"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": "iOS",
|
||||
"bundleId": "com.duoqi.app"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `platform` | `'Android' \| 'iOS'` | 是 | 客户端平台 |
|
||||
| `packageName` | string | Android 必填 | App 包名 |
|
||||
| `packageSign` | string | Android 必填 | App 包签名 |
|
||||
| `bundleId` | string | iOS 必填 | App bundleId |
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"fusionAuthToken": "FKcksloqk***********jalEc+"
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
错误响应(服务未配置):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "SERVICE_UNAVAILABLE",
|
||||
"message": "Fusion auth is not configured"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### POST /auth/fusion/verify
|
||||
|
||||
认证:无
|
||||
限流:10 次/分钟
|
||||
|
||||
用途:客户端融合认证 SDK 完成认证后(`onVerifySuccess` 事件),将运营商返回的 verifyToken 发送到此端点。服务端用 verifyToken 向阿里云换取手机号,完成用户查找或创建,签发 JWT。对应阿里云 `VerifyWithFusionAuthToken` API。
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"verifyToken": "LD108enNdlsl*******sFLKCks1=="
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `verifyToken` | string | 是 | 客户端 SDK `onVerifySuccess` 事件返回的 Token |
|
||||
|
||||
成功响应(同 `/auth/guest`):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"nickname": null,
|
||||
"avatarUrl": null,
|
||||
"tier": "free"
|
||||
},
|
||||
"tokens": {
|
||||
"accessToken": "jwt",
|
||||
"refreshToken": "jwt"
|
||||
}
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
错误响应(认证失败):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "VALIDATION_ERROR",
|
||||
"message": "Phone verification failed"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
错误响应(服务未配置):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"data": null,
|
||||
"error": {
|
||||
"code": "SERVICE_UNAVAILABLE",
|
||||
"message": "Fusion auth is not configured"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 前置条件检查清单
|
||||
|
||||
在开始编码之前,需要以下准备工作:
|
||||
|
||||
- [ ] **阿里云 RAM 子用户**:创建 RAM 子用户,授予 `AliyunDypnsFullAccess` 权限,获取 AccessKey ID 和 Secret
|
||||
- [ ] **融合认证方案**:在号码认证服务控制台创建融合认证方案,获取 SchemeCode
|
||||
- [ ] **Android 签名信息**:确认 App 包名 (`PackageName`) 和签名 (`PackageSign`)
|
||||
- [ ] **iOS Bundle 信息**:确认 App bundleId
|
||||
- [ ] **Flutter 客户端状态**:确认 duoqi-flutter 是否已集成融合认证 SDK
|
||||
|
||||
---
|
||||
|
||||
## 风险与注意事项
|
||||
|
||||
1. **手机号隐私合规**:存储用户手机号需符合《个人信息保护法》,建议在隐私政策中明确告知
|
||||
2. **运营商支持范围**:号码认证(一键取号)依赖运营商网关,WiFi 环境可能降级到短信验证码
|
||||
3. **限流**:阿里云 API 有自身限流(错误码 `Throttling.System`),服务端应做好重试和降级
|
||||
4. **错误码映射**:阿里云的错误码需映射到项目统一的 `error.code` 体系
|
||||
5. **账号冲突**:同一手机号可能已通过其他方式注册(如先游客后手机号),需考虑合并策略
|
||||
@ -19,9 +19,6 @@
|
||||
"test:coverage": "vitest run --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alicloud/dypnsapi20170525": "^2.0.0",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"@alicloud/tea-util": "^1.4.11",
|
||||
"@fastify/cors": "^11.0.0",
|
||||
"@fastify/helmet": "^13.0.0",
|
||||
"@fastify/jwt": "^9.0.0",
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
describe('fusion-auth-client module structure', () => {
|
||||
it('exports getFusionAuthToken and verifyFusionAuthToken', async () => {
|
||||
const mod = await import('../../services/auth/fusion-auth-client.js');
|
||||
expect(typeof mod.getFusionAuthToken).toBe('function');
|
||||
expect(typeof mod.verifyFusionAuthToken).toBe('function');
|
||||
});
|
||||
});
|
||||
@ -18,7 +18,6 @@ async function authMiddleware(app: FastifyInstance): Promise<void> {
|
||||
'/v1/auth/guest',
|
||||
'/v1/auth/phone',
|
||||
'/v1/auth/refresh',
|
||||
'/v1/auth/fusion',
|
||||
'/v1/auth/providers',
|
||||
];
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import { db } from '../../db/client.js';
|
||||
import { appSettings } from '../../db/schema.js';
|
||||
import { like } from 'drizzle-orm';
|
||||
|
||||
const VALID_PROVIDERS = ['wechat', 'qq', 'huawei', 'apple', 'fusion', 'phone_sms'];
|
||||
const VALID_PROVIDERS = ['wechat', 'qq', 'huawei', 'apple', 'phone_sms'];
|
||||
|
||||
export async function adminAuthProvidersRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get('/', async () => {
|
||||
|
||||
@ -3,9 +3,8 @@ import { z } from 'zod';
|
||||
import { db } from '../db/client.js';
|
||||
import { users, appSettings } from '../db/schema.js';
|
||||
import { eq, like } from 'drizzle-orm';
|
||||
import { findOrCreateGuest, findOrCreateHuawei, findOrCreatePhone, refreshJwt } from '../services/auth/jwt.js';
|
||||
import { findOrCreateGuest, findOrCreateHuawei, findOrCreatePhone as _findOrCreatePhone, refreshJwt } from '../services/auth/jwt.js';
|
||||
import { verifyHuaweiToken } from '../services/auth/huawei-id-kit.js';
|
||||
import { getFusionAuthToken, verifyFusionAuthToken } from '../services/auth/fusion-auth-client.js';
|
||||
import { linkGuestAccount } from '../services/auth/account-link-service.js';
|
||||
import { NotFoundError } from '../utils/errors.js';
|
||||
import { config } from '../utils/config.js';
|
||||
@ -32,17 +31,6 @@ const linkSchema = z.object({
|
||||
clientMigrationId: z.string().min(1),
|
||||
});
|
||||
|
||||
const fusionVerifySchema = z.object({
|
||||
verifyToken: z.string().min(1),
|
||||
});
|
||||
|
||||
const fusionTokenSchema = z.object({
|
||||
platform: z.enum(['Android', 'iOS', 'Harmony']),
|
||||
packageName: z.string().optional(),
|
||||
packageSign: z.string().optional(),
|
||||
bundleId: z.string().optional(),
|
||||
});
|
||||
|
||||
const providersQuerySchema = z.object({
|
||||
platform: z.enum(['ios', 'android', 'harmony']),
|
||||
});
|
||||
@ -58,7 +46,6 @@ interface ProviderEntry {
|
||||
}
|
||||
|
||||
const PLATFORM_AVAILABILITY: Record<string, Platform[]> = {
|
||||
fusion: ['ios', 'android', 'harmony'],
|
||||
phone_sms: ['ios', 'android', 'harmony'],
|
||||
huawei: ['android', 'harmony'],
|
||||
apple: ['ios'],
|
||||
@ -67,7 +54,6 @@ const PLATFORM_AVAILABILITY: Record<string, Platform[]> = {
|
||||
};
|
||||
|
||||
const PROVIDER_METADATA: Record<string, { name: string; type: ProviderEntry['type']; iconKey?: string }> = {
|
||||
fusion: { name: '一键登录', type: 'primary' },
|
||||
phone_sms: { name: '短信验证码登录', type: 'secondary' },
|
||||
huawei: { name: '通过华为账号登录', type: 'third_party', iconKey: 'huawei' },
|
||||
apple: { name: '通过 Apple 登录', type: 'third_party', iconKey: 'apple' },
|
||||
@ -75,23 +61,13 @@ const PROVIDER_METADATA: Record<string, { name: string; type: ProviderEntry['typ
|
||||
qq: { name: '通过 QQ 登录', type: 'third_party', iconKey: 'qq' },
|
||||
};
|
||||
|
||||
const CREDENTIAL_PROVIDERS = new Set(['fusion', 'phone_sms', 'huawei', 'apple']);
|
||||
const CREDENTIAL_PROVIDERS = new Set(['phone_sms', 'huawei', 'apple']);
|
||||
const DB_TOGGLE_PROVIDERS = new Set(['wechat', 'qq']);
|
||||
|
||||
function isCredentialConfigured(providerId: string, platform?: Platform): boolean {
|
||||
function isCredentialConfigured(providerId: string, _platform?: Platform): boolean {
|
||||
switch (providerId) {
|
||||
case 'fusion':
|
||||
case 'phone_sms': {
|
||||
const hasCredentials = !!(config.ALIYUN_ACCESS_KEY_ID && config.ALIYUN_ACCESS_KEY_SECRET);
|
||||
if (!hasCredentials) return false;
|
||||
if (!platform) return true;
|
||||
const schemeCodeMap: Record<Platform, string | undefined> = {
|
||||
android: config.ALIYUN_FUSION_SCHEME_CODE_ANDROID,
|
||||
ios: config.ALIYUN_FUSION_SCHEME_CODE_IOS,
|
||||
harmony: config.ALIYUN_FUSION_SCHEME_CODE_HARMONY,
|
||||
};
|
||||
return !!schemeCodeMap[platform];
|
||||
}
|
||||
case 'phone_sms':
|
||||
return !!(config.ALIYUN_ACCESS_KEY_ID && config.ALIYUN_ACCESS_KEY_SECRET);
|
||||
case 'huawei':
|
||||
return !!(config.HUAWEI_CLIENT_ID && config.HUAWEI_CLIENT_SECRET);
|
||||
case 'apple':
|
||||
@ -166,7 +142,7 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
|
||||
return reply.send({ success: true, data: result, error: null });
|
||||
});
|
||||
|
||||
// Phase 2: Phone login (保留占位,融合认证使用 /auth/fusion/ 路径)
|
||||
// Phase 2: Phone login
|
||||
app.post('/auth/phone', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (_request, reply) => {
|
||||
return reply.status(501).send({
|
||||
success: false,
|
||||
@ -175,63 +151,6 @@ export async function authRoutes(app: FastifyInstance): Promise<void> {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Fusion Auth ──────────────────────────────────────────────────
|
||||
// Step 1: 客户端获取 SDK 鉴权 Token
|
||||
app.post('/auth/fusion/token', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (request, reply) => {
|
||||
const parsed = fusionTokenSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
data: null,
|
||||
error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message ?? 'Invalid input' },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const fusionAuthToken = await getFusionAuthToken({
|
||||
platform: parsed.data.platform,
|
||||
packageName: parsed.data.packageName,
|
||||
packageSign: parsed.data.packageSign,
|
||||
bundleId: parsed.data.bundleId,
|
||||
});
|
||||
return reply.send({ success: true, data: { fusionAuthToken }, error: null });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Failed to get fusion auth token';
|
||||
const statusCode = (err as Record<string, unknown>)?.statusCode ?? 503;
|
||||
return reply.status(statusCode as number).send({
|
||||
success: false,
|
||||
data: null,
|
||||
error: { code: 'SERVICE_UNAVAILABLE', message },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Step 2: 客户端提交 verifyToken,服务端换取手机号并完成登录/注册
|
||||
app.post('/auth/fusion/verify', { config: { rateLimit: { max: 10, timeWindow: '1 minute' } } }, async (request, reply) => {
|
||||
const parsed = fusionVerifySchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
data: null,
|
||||
error: { code: 'VALIDATION_ERROR', message: parsed.error.issues[0]?.message ?? 'Invalid input' },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const phoneNumber = await verifyFusionAuthToken(parsed.data.verifyToken);
|
||||
const result = await findOrCreatePhone(phoneNumber, app);
|
||||
return reply.send({ success: true, data: result, error: null });
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : 'Phone verification failed';
|
||||
const statusCode = (err as Record<string, unknown>)?.statusCode ?? 401;
|
||||
return reply.status(statusCode as number).send({
|
||||
success: false,
|
||||
data: null,
|
||||
error: { code: 'UNAUTHORIZED', message },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ── Login Providers ──────────────────────────────────────────────
|
||||
app.get('/auth/providers', async (request, reply) => {
|
||||
const parsed = providersQuerySchema.safeParse(request.query);
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
import { createRequire } from 'node:module';
|
||||
import * as $Dypnsapi20170525 from '@alicloud/dypnsapi20170525';
|
||||
import * as $OpenApi from '@alicloud/openapi-client';
|
||||
import * as $Util from '@alicloud/tea-util';
|
||||
import { config } from '../../utils/config.js';
|
||||
import { UnauthorizedError } from '../../utils/errors.js';
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const DypnsapiClient: any = require('@alicloud/dypnsapi20170525').default ?? require('@alicloud/dypnsapi20170525');
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let clientInstance: any = null;
|
||||
|
||||
function getClient() {
|
||||
if (!clientInstance) {
|
||||
if (!config.ALIYUN_ACCESS_KEY_ID || !config.ALIYUN_ACCESS_KEY_SECRET) {
|
||||
throw new UnauthorizedError('Fusion auth is not configured on the server');
|
||||
}
|
||||
|
||||
const openApiConfig = new $OpenApi.Config({
|
||||
accessKeyId: config.ALIYUN_ACCESS_KEY_ID,
|
||||
accessKeySecret: config.ALIYUN_ACCESS_KEY_SECRET,
|
||||
endpoint: 'dypnsapi.aliyuncs.com',
|
||||
});
|
||||
|
||||
clientInstance = new DypnsapiClient(openApiConfig);
|
||||
}
|
||||
return clientInstance;
|
||||
}
|
||||
|
||||
function requireSchemeCode(platform: string): string {
|
||||
const schemeCodeMap: Record<string, string | undefined> = {
|
||||
Android: config.ALIYUN_FUSION_SCHEME_CODE_ANDROID,
|
||||
iOS: config.ALIYUN_FUSION_SCHEME_CODE_IOS,
|
||||
Harmony: config.ALIYUN_FUSION_SCHEME_CODE_HARMONY,
|
||||
};
|
||||
const schemeCode = schemeCodeMap[platform];
|
||||
if (!schemeCode) {
|
||||
throw new UnauthorizedError(`Fusion auth scheme code not configured for platform: ${platform}`);
|
||||
}
|
||||
return schemeCode;
|
||||
}
|
||||
|
||||
export async function getFusionAuthToken(options: {
|
||||
platform: string;
|
||||
packageName?: string;
|
||||
packageSign?: string;
|
||||
bundleId?: string;
|
||||
}): Promise<string> {
|
||||
const client = getClient();
|
||||
const schemeCode = requireSchemeCode(options.platform);
|
||||
|
||||
const request = new $Dypnsapi20170525.GetFusionAuthTokenRequest({
|
||||
schemeCode,
|
||||
platform: options.platform,
|
||||
durationSeconds: 900,
|
||||
packageName: options.packageName,
|
||||
packageSign: options.packageSign,
|
||||
bundleId: options.bundleId,
|
||||
});
|
||||
|
||||
const runtime = new $Util.RuntimeOptions({});
|
||||
|
||||
try {
|
||||
const response = await client.getFusionAuthTokenWithOptions(request, runtime);
|
||||
if (!response.body?.success || !response.body?.model) {
|
||||
throw new UnauthorizedError(
|
||||
`GetFusionAuthToken failed: ${response.body?.message ?? 'unknown error'}`,
|
||||
);
|
||||
}
|
||||
return response.body.model;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof UnauthorizedError) throw error;
|
||||
const msg = error instanceof Error ? error.message : 'Unknown SDK error';
|
||||
throw new UnauthorizedError(`Failed to get fusion auth token: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyFusionAuthToken(verifyToken: string): Promise<string> {
|
||||
const client = getClient();
|
||||
|
||||
const request = new $Dypnsapi20170525.VerifyWithFusionAuthTokenRequest({
|
||||
verifyToken,
|
||||
});
|
||||
|
||||
const runtime = new $Util.RuntimeOptions({});
|
||||
|
||||
try {
|
||||
const response = await client.verifyWithFusionAuthTokenWithOptions(request, runtime);
|
||||
if (!response.body?.success || !response.body?.model) {
|
||||
throw new UnauthorizedError(
|
||||
`VerifyWithFusionAuthToken failed: ${response.body?.message ?? 'unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
const { phoneNumber, verifyResult } = response.body.model as {
|
||||
phoneNumber?: string;
|
||||
verifyResult?: string;
|
||||
};
|
||||
|
||||
if (verifyResult !== 'PASS' || !phoneNumber) {
|
||||
throw new UnauthorizedError('Phone verification failed');
|
||||
}
|
||||
|
||||
return phoneNumber;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof UnauthorizedError) throw error;
|
||||
throw new UnauthorizedError('Failed to verify fusion auth token');
|
||||
}
|
||||
}
|
||||
@ -18,9 +18,6 @@ const envSchema = z.object({
|
||||
APPLE_BUNDLE_ID: z.string().optional(),
|
||||
ALIYUN_ACCESS_KEY_ID: z.string().optional(),
|
||||
ALIYUN_ACCESS_KEY_SECRET: z.string().optional(),
|
||||
ALIYUN_FUSION_SCHEME_CODE_ANDROID: z.string().optional(),
|
||||
ALIYUN_FUSION_SCHEME_CODE_IOS: z.string().optional(),
|
||||
ALIYUN_FUSION_SCHEME_CODE_HARMONY: z.string().optional(),
|
||||
PORT: z.coerce.number().default(3000),
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
||||
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user