feat(auth): wire Aliyun SMS provider for phone OTP login
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m33s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m33s
接入流程:
- src/lib/sms.ts: 封装 sendOtpSms(phone, code), 走 dysmsapi.aliyuncs.com 全局端点
- /api/auth/send-otp:
* 生成 6 位验证码 → Redis 5min TTL
* 调 Aliyun SDK 发送; OK → 200, isv.* 错误 → 422, 其它 → 500
* SMS_NOT_CONFIGURED 时 dev 仍能 console.log 验证码联调
- auth.ts verifyOtp:
* dev 万能码 123456 保留
* 否则 redis.get(sms:otp:phone) 比对, 通过后 del 防重放
* Redis 未配置时 prod 拒绝, dev 接受任意 6 位
环境变量 (.env.local, 不入仓库):
- SMS_ACCESS_KEY / SMS_SECRET_KEY (RAM 子账号)
- SMS_SIGN_NAME (例: 广州气元科技)
- SMS_TEMPLATE_CODE (例: SMS_506210397)
依赖:
+ @alicloud/dysmsapi20170525
+ @alicloud/openapi-client
+ @alicloud/tea-util
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
15af8e1781
commit
0a7c1ec130
@ -14,6 +14,9 @@
|
||||
"db:seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@alicloud/dysmsapi20170525": "^4.5.1",
|
||||
"@alicloud/openapi-client": "^0.4.15",
|
||||
"@alicloud/tea-util": "^1.4.11",
|
||||
"@auth/prisma-adapter": "^2.11.2",
|
||||
"@prisma/client": "^6.19.3",
|
||||
"clsx": "^2.1.1",
|
||||
|
||||
302
pnpm-lock.yaml
generated
302
pnpm-lock.yaml
generated
@ -8,6 +8,15 @@ importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@alicloud/dysmsapi20170525':
|
||||
specifier: ^4.5.1
|
||||
version: 4.5.1
|
||||
'@alicloud/openapi-client':
|
||||
specifier: ^0.4.15
|
||||
version: 0.4.15
|
||||
'@alicloud/tea-util':
|
||||
specifier: ^1.4.11
|
||||
version: 1.4.11
|
||||
'@auth/prisma-adapter':
|
||||
specifier: ^2.11.2
|
||||
version: 2.11.2(@prisma/client@6.19.3(prisma@6.19.3(typescript@6.0.3))(typescript@6.0.3))
|
||||
@ -84,6 +93,60 @@ importers:
|
||||
|
||||
packages:
|
||||
|
||||
'@alicloud/credentials@2.4.4':
|
||||
resolution: {integrity: sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q==}
|
||||
|
||||
'@alicloud/darabonba-array@0.1.2':
|
||||
resolution: {integrity: sha512-ZPuQ+bJyjrd8XVVm55kl+ypk7OQoi1ZH/DiToaAEQaGvgEjrTcvQkg71//vUX/6cvbLIF5piQDvhrLb+lUEIPQ==}
|
||||
|
||||
'@alicloud/darabonba-encode-util@0.0.1':
|
||||
resolution: {integrity: sha512-Sl5vCRVAYMqwmvXpJLM9hYoCHOMsQlGxaWSGhGWulpKk/NaUBArtoO1B0yHruJf1C5uHhEJIaylYcM48icFHgw==}
|
||||
|
||||
'@alicloud/darabonba-encode-util@0.0.2':
|
||||
resolution: {integrity: sha512-mlsNctkeqmR0RtgE1Rngyeadi5snLOAHBCWEtYf68d7tyKskosXDTNeZ6VCD/UfrUu4N51ItO8zlpfXiOgeg3A==}
|
||||
|
||||
'@alicloud/darabonba-map@0.0.1':
|
||||
resolution: {integrity: sha512-2ep+G3YDvuI+dRYVlmER1LVUQDhf9kEItmVB/bbEu1pgKzelcocCwAc79XZQjTcQGFgjDycf3vH87WLDGLFMlw==}
|
||||
|
||||
'@alicloud/darabonba-signature-util@0.0.4':
|
||||
resolution: {integrity: sha512-I1TtwtAnzLamgqnAaOkN0IGjwkiti//0a7/auyVThdqiC/3kyafSAn6znysWOmzub4mrzac2WiqblZKFcN5NWg==}
|
||||
|
||||
'@alicloud/darabonba-string@1.0.3':
|
||||
resolution: {integrity: sha512-NyWwrU8cAIesWk3uHL1Q7pTDTqLkCI/0PmJXC4/4A0MFNAZ9Ouq0iFBsRqvfyUujSSM+WhYLuTfakQXiVLkTMA==}
|
||||
|
||||
'@alicloud/dysmsapi20170525@4.5.1':
|
||||
resolution: {integrity: sha512-6l0N6M+uV2l+KxF5XOiZ3DZoDlrZ2ZxFDKkr424KJm0A6kSsQlRNV5eiJoMkB6h11KYf0BD5/0lXzW8SkPtGMA==}
|
||||
|
||||
'@alicloud/endpoint-util@0.0.1':
|
||||
resolution: {integrity: sha512-+pH7/KEXup84cHzIL6UJAaPqETvln4yXlD9JzlrqioyCSaWxbug5FUobsiI6fuUOpw5WwoB3fWAtGbFnJ1K3Yg==}
|
||||
|
||||
'@alicloud/gateway-pop@0.0.6':
|
||||
resolution: {integrity: sha512-KF4I+JvfYuLKc3fWeWYIZ7lOVJ9jRW0sQXdXidZn1DKZ978ncfGf7i0LBfONGk4OxvNb/HD3/0yYhkgZgPbKtA==}
|
||||
|
||||
'@alicloud/gateway-spi@0.0.8':
|
||||
resolution: {integrity: sha512-KM7fu5asjxZPmrz9sJGHJeSU+cNQNOxW+SFmgmAIrITui5hXL2LB+KNRuzWmlwPjnuA2X3/keq9h6++S9jcV5g==}
|
||||
|
||||
'@alicloud/openapi-client@0.4.15':
|
||||
resolution: {integrity: sha512-4VE0/k5ZdQbAhOSTqniVhuX1k5DUeUMZv74degn3wIWjLY6Bq+hxjaGsaHYlLZ2gA5wUrs8NcI5TE+lIQS3iiA==}
|
||||
|
||||
'@alicloud/openapi-core@1.0.7':
|
||||
resolution: {integrity: sha512-I80PQVfmlzRiXGHwutMp2zTpiqUVv8ts30nWAfksfHUSTIapk3nj9IXaPbULMPGNV6xqEyshO2bj2a+pmwc2tQ==}
|
||||
|
||||
'@alicloud/openapi-util@0.3.3':
|
||||
resolution: {integrity: sha512-vf0cQ/q8R2U7ZO88X5hDiu1yV3t/WexRj+YycWxRutkH/xVXfkmpRgps8lmNEk7Ar+0xnY8+daN2T+2OyB9F4A==}
|
||||
|
||||
'@alicloud/tea-typescript@1.8.0':
|
||||
resolution: {integrity: sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==}
|
||||
|
||||
'@alicloud/tea-util@1.4.11':
|
||||
resolution: {integrity: sha512-HyPEEQ8F0WoZegiCp7sVdrdm6eBOB+GCvGl4182u69LDFktxfirGLcAx3WExUr1zFWkq2OSmBroTwKQ4w/+Yww==}
|
||||
|
||||
'@alicloud/tea-util@1.4.9':
|
||||
resolution: {integrity: sha512-S0wz76rGtoPKskQtRTGqeuqBHFj8BqUn0Vh+glXKun2/9UpaaaWmuJwcmtImk6bJZfLYEShDF/kxDmDJoNYiTw==}
|
||||
|
||||
'@alicloud/tea-xml@0.0.3':
|
||||
resolution: {integrity: sha512-+/9GliugjrLglsXVrd1D80EqqKgGpyA0eQ6+1ZdUOYCaRguaSwz44trX3PaxPu/HhIPJg9PsGQQ3cSLXWZjbAA==}
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@ -174,6 +237,9 @@ packages:
|
||||
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@darabonba/typescript@1.0.4':
|
||||
resolution: {integrity: sha512-icl8RGTw4DiWRpco6dVh21RS0IqrH4s/eEV36TZvz/e1+paogSZjaAgox7ByrlEuvG+bo5d8miq/dRlqiUaL/w==}
|
||||
|
||||
'@emnapi/core@1.10.0':
|
||||
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
|
||||
|
||||
@ -800,9 +866,15 @@ packages:
|
||||
'@types/json5@0.0.29':
|
||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||
|
||||
'@types/node@12.0.2':
|
||||
resolution: {integrity: sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==}
|
||||
|
||||
'@types/node@20.19.40':
|
||||
resolution: {integrity: sha512-xxx6M2IpSTnnKcR0cMvIiohkiCx20/oRPtWGbenFygKCGl3zqUzdNjQ/1V4solq1LU+dgv0nQzeGOuqkqZGg0Q==}
|
||||
|
||||
'@types/node@22.19.19':
|
||||
resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==}
|
||||
|
||||
'@types/react-dom@19.2.3':
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
peerDependencies:
|
||||
@ -811,6 +883,9 @@ packages:
|
||||
'@types/react@19.2.14':
|
||||
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
|
||||
|
||||
'@types/xml2js@0.4.14':
|
||||
resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.2':
|
||||
resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@ -1616,6 +1691,9 @@ packages:
|
||||
hermes-parser@0.25.1:
|
||||
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
||||
|
||||
httpx@2.3.3:
|
||||
resolution: {integrity: sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
@ -1632,6 +1710,10 @@ packages:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
ini@1.3.5:
|
||||
resolution: {integrity: sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==}
|
||||
deprecated: Please update to ini >=1.3.6 to avoid a prototype pollution issue
|
||||
|
||||
internal-slot@1.1.0:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@ -1808,6 +1890,9 @@ packages:
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
kitx@2.2.0:
|
||||
resolution: {integrity: sha512-tBMwe6AALTBQJb0woQDD40734NKzb0Kzi3k7wQj9ar3AbP9oqhoVrdXPh7rk2r00/glIgd0YbToIUJsnxWMiIg==}
|
||||
|
||||
language-subtag-registry@0.3.20:
|
||||
resolution: {integrity: sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg==}
|
||||
|
||||
@ -1906,6 +1991,9 @@ packages:
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
lodash@4.18.1:
|
||||
resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||
hasBin: true
|
||||
@ -1955,6 +2043,12 @@ packages:
|
||||
resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
moment-timezone@0.5.45:
|
||||
resolution: {integrity: sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==}
|
||||
|
||||
moment@2.30.1:
|
||||
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
|
||||
|
||||
motion-dom@12.38.0:
|
||||
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
|
||||
|
||||
@ -2254,6 +2348,10 @@ packages:
|
||||
resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
sax@1.6.0:
|
||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
@ -2310,6 +2408,9 @@ packages:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
sm3@1.0.3:
|
||||
resolution: {integrity: sha512-KyFkIfr8QBlFG3uc3NaljaXdYcsbRy1KrSfc4tsQV8jW68jAktGeOcifu530Vx/5LC+PULHT0Rv8LiI8Gw+c1g==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@ -2515,6 +2616,14 @@ packages:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
xml2js@0.6.2:
|
||||
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
|
||||
xmlbuilder@11.0.1:
|
||||
resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
@ -2547,6 +2656,146 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alicloud/credentials@2.4.4':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
httpx: 2.3.3
|
||||
ini: 1.3.5
|
||||
kitx: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/darabonba-array@0.1.2':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/darabonba-encode-util@0.0.1':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
moment: 2.30.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/darabonba-encode-util@0.0.2':
|
||||
dependencies:
|
||||
moment: 2.30.1
|
||||
|
||||
'@alicloud/darabonba-map@0.0.1':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/darabonba-signature-util@0.0.4':
|
||||
dependencies:
|
||||
'@alicloud/darabonba-encode-util': 0.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/darabonba-string@1.0.3':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/dysmsapi20170525@4.5.1':
|
||||
dependencies:
|
||||
'@alicloud/openapi-core': 1.0.7
|
||||
'@darabonba/typescript': 1.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/endpoint-util@0.0.1':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
kitx: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/gateway-pop@0.0.6':
|
||||
dependencies:
|
||||
'@alicloud/credentials': 2.4.4
|
||||
'@alicloud/darabonba-array': 0.1.2
|
||||
'@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.3
|
||||
'@alicloud/endpoint-util': 0.0.1
|
||||
'@alicloud/gateway-spi': 0.0.8
|
||||
'@alicloud/openapi-util': 0.3.3
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
'@alicloud/tea-util': 1.4.11
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/gateway-spi@0.0.8':
|
||||
dependencies:
|
||||
'@alicloud/credentials': 2.4.4
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/openapi-client@0.4.15':
|
||||
dependencies:
|
||||
'@alicloud/credentials': 2.4.4
|
||||
'@alicloud/gateway-spi': 0.0.8
|
||||
'@alicloud/openapi-util': 0.3.3
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
'@alicloud/tea-util': 1.4.9
|
||||
'@alicloud/tea-xml': 0.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/openapi-core@1.0.7':
|
||||
dependencies:
|
||||
'@alicloud/credentials': 2.4.4
|
||||
'@alicloud/gateway-pop': 0.0.6
|
||||
'@alicloud/gateway-spi': 0.0.8
|
||||
'@darabonba/typescript': 1.0.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/openapi-util@0.3.3':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
'@alicloud/tea-util': 1.4.11
|
||||
kitx: 2.2.0
|
||||
sm3: 1.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/tea-typescript@1.8.0':
|
||||
dependencies:
|
||||
'@types/node': 12.0.2
|
||||
httpx: 2.3.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/tea-util@1.4.11':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
'@darabonba/typescript': 1.0.4
|
||||
kitx: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/tea-util@1.4.9':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
kitx: 2.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alicloud/tea-xml@0.0.3':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
'@types/xml2js': 0.4.14
|
||||
xml2js: 0.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@auth/core@0.41.2':
|
||||
@ -2666,6 +2915,17 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.28.5
|
||||
|
||||
'@darabonba/typescript@1.0.4':
|
||||
dependencies:
|
||||
'@alicloud/tea-typescript': 1.8.0
|
||||
httpx: 2.3.3
|
||||
lodash: 4.18.1
|
||||
moment: 2.30.1
|
||||
moment-timezone: 0.5.45
|
||||
xml2js: 0.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@emnapi/core@1.10.0':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.2.1
|
||||
@ -3128,10 +3388,16 @@ snapshots:
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
|
||||
'@types/node@12.0.2': {}
|
||||
|
||||
'@types/node@20.19.40':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.19.19':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/react-dom@19.2.3(@types/react@19.2.14)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.14
|
||||
@ -3140,6 +3406,10 @@ snapshots:
|
||||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/xml2js@0.4.14':
|
||||
dependencies:
|
||||
'@types/node': 20.19.40
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@ -4115,6 +4385,13 @@ snapshots:
|
||||
dependencies:
|
||||
hermes-estree: 0.25.1
|
||||
|
||||
httpx@2.3.3:
|
||||
dependencies:
|
||||
'@types/node': 20.19.40
|
||||
debug: 4.4.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
ignore@7.0.5: {}
|
||||
@ -4126,6 +4403,8 @@ snapshots:
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
ini@1.3.5: {}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@ -4316,6 +4595,10 @@ snapshots:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
kitx@2.2.0:
|
||||
dependencies:
|
||||
'@types/node': 22.19.19
|
||||
|
||||
language-subtag-registry@0.3.20: {}
|
||||
|
||||
language-tags@1.0.9:
|
||||
@ -4386,6 +4669,8 @@ snapshots:
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash@4.18.1: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 3.0.0
|
||||
@ -4429,6 +4714,12 @@ snapshots:
|
||||
|
||||
minipass@7.1.3: {}
|
||||
|
||||
moment-timezone@0.5.45:
|
||||
dependencies:
|
||||
moment: 2.30.1
|
||||
|
||||
moment@2.30.1: {}
|
||||
|
||||
motion-dom@12.38.0:
|
||||
dependencies:
|
||||
motion-utils: 12.36.0
|
||||
@ -4728,6 +5019,8 @@ snapshots:
|
||||
es-errors: 1.3.0
|
||||
is-regex: 1.2.1
|
||||
|
||||
sax@1.6.0: {}
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@6.3.1: {}
|
||||
@ -4824,6 +5117,8 @@ snapshots:
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
sm3@1.0.3: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
stable-hash@0.0.5: {}
|
||||
@ -5111,6 +5406,13 @@ snapshots:
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.2.0
|
||||
|
||||
xml2js@0.6.2:
|
||||
dependencies:
|
||||
sax: 1.6.0
|
||||
xmlbuilder: 11.0.1
|
||||
|
||||
xmlbuilder@11.0.1: {}
|
||||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
zod-validation-error@4.0.2(zod@4.4.3):
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
allowBuilds:
|
||||
'@alicloud/openapi-core': false
|
||||
'@prisma/client': true
|
||||
'@prisma/engines': true
|
||||
esbuild: true
|
||||
|
||||
@ -3,6 +3,7 @@ import { z } from "zod";
|
||||
import { rateLimit } from "@/lib/rate-limit";
|
||||
import { getClientIp } from "@/lib/current-user";
|
||||
import { getRedis } from "@/lib/redis";
|
||||
import { sendOtpSms } from "@/lib/sms";
|
||||
import { ok, ERR } from "@/lib/api-response";
|
||||
|
||||
const Body = z.object({
|
||||
@ -37,21 +38,40 @@ export async function POST(req: NextRequest) {
|
||||
// 生成 6 位验证码
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000));
|
||||
|
||||
// 缓存到 Redis(5 分钟过期)
|
||||
// 缓存到 Redis(5 分钟过期)。Redis 未配置时 dev 仍能通过万能码 123456 走完整流程
|
||||
const redis = getRedis();
|
||||
if (redis) {
|
||||
await redis.set(`sms:otp:${phone}`, code, "EX", 300);
|
||||
}
|
||||
|
||||
// TODO[团队]: 接入真实短信服务(阿里云 / 火山引擎 SMS)
|
||||
// - 模板:${SMS_TEMPLATE_CODE}
|
||||
// - 签名:${SMS_SIGN_NAME}
|
||||
// - 参数:{ code, expireMin: 5 }
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log(`[dev-otp] 发送给 ${phone}: ${code}(开发环境也接受万能码 123456)`);
|
||||
// 调阿里云短信发送
|
||||
const sms = await sendOtpSms(phone, code);
|
||||
|
||||
if (sms.ok) {
|
||||
console.log(`[sms] sent phone=${phone} bizId=${sms.bizId}`);
|
||||
return ok({ message: "验证码已发送", expiresIn: 300 });
|
||||
}
|
||||
|
||||
return ok({ message: "验证码已发送", expiresIn: 300 });
|
||||
// 失败处理:
|
||||
// - SMS 未配置且非生产 → 控制台打 code, 仍返回成功(开发态联调)
|
||||
// - 阿里云明确返回参数 / 触发流控 → 422
|
||||
// - 其它 → 500
|
||||
if (sms.errorCode === "SMS_NOT_CONFIGURED") {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.log(`[dev-otp] SMS 未配置, 验证码 ${phone}: ${code}(也可用万能码 123456)`);
|
||||
return ok({ message: "验证码已发送", expiresIn: 300 });
|
||||
}
|
||||
console.error("[sms] 生产环境短信未配置, 检查 SMS_* 环境变量");
|
||||
return ERR.INTERNAL("短信服务未配置");
|
||||
}
|
||||
|
||||
console.error(
|
||||
`[sms] 发送失败 phone=${phone} code=${sms.errorCode} message=${sms.errorMessage}`,
|
||||
);
|
||||
if (sms.errorCode?.startsWith("isv.")) {
|
||||
return ERR.VALIDATION(sms.errorMessage ?? "短信发送失败");
|
||||
}
|
||||
return ERR.INTERNAL("短信发送失败");
|
||||
} catch (e) {
|
||||
console.error("[POST /api/auth/send-otp]", e);
|
||||
return ERR.INTERNAL();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import NextAuth, { type NextAuthConfig } from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { prisma } from "./prisma";
|
||||
import { getRedis } from "./redis";
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
@ -101,8 +102,10 @@ export const authConfig: NextAuthConfig = {
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth(authConfig);
|
||||
|
||||
/**
|
||||
* 校验 OTP 验证码(开发态:固定接受 "123456")。
|
||||
* 生产态:从 Redis 读取并比对,校验后删除避免重放。
|
||||
* 校验 OTP 验证码。
|
||||
* - 开发态万能码 "123456" 始终通过(仅 NODE_ENV !== "production")
|
||||
* - 否则从 Redis 读取并比对,校验通过后立即 del 避免重放
|
||||
* - Redis 未配置时:dev 接受任意 6 位(联调用),prod 直接拒绝
|
||||
*/
|
||||
async function verifyOtp(phone: string, code: string): Promise<boolean> {
|
||||
if (process.env.NODE_ENV !== "production" && code === "123456") {
|
||||
@ -110,14 +113,19 @@ async function verifyOtp(phone: string, code: string): Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO[团队]: 接入 Redis
|
||||
// const redis = getRedis();
|
||||
// if (!redis) return false;
|
||||
// const key = `sms:otp:${phone}`;
|
||||
// const stored = await redis.get(key);
|
||||
// if (!stored || stored !== code) return false;
|
||||
// await redis.del(key);
|
||||
// return true;
|
||||
const redis = getRedis();
|
||||
if (!redis) {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
// dev 联调态:Redis 没配置时也能走完整流程
|
||||
return /^\d{6}$/.test(code);
|
||||
}
|
||||
console.error("[auth] 生产环境 Redis 未配置,OTP 校验直接拒绝");
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
const key = `sms:otp:${phone}`;
|
||||
const stored = await redis.get(key);
|
||||
if (!stored || stored !== code) return false;
|
||||
await redis.del(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
83
src/lib/sms.ts
Normal file
83
src/lib/sms.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 阿里云短信服务客户端
|
||||
*
|
||||
* 接入: dysmsapi.aliyuncs.com (短信服务在国内是全局服务,所有调用走 cn-hangzhou)
|
||||
* 文档: https://help.aliyun.com/document_detail/419298.html
|
||||
*
|
||||
* 环境变量:
|
||||
* SMS_ACCESS_KEY RAM 子账号 AccessKey ID
|
||||
* SMS_SECRET_KEY RAM 子账号 AccessKey Secret
|
||||
* SMS_SIGN_NAME 已审核通过的签名 (例: 广州气元科技)
|
||||
* SMS_TEMPLATE_CODE 已审核通过的模板 Code (例: SMS_506210397)
|
||||
*
|
||||
* 模板内容必须严格使用 ${code} 占位符,例如:
|
||||
* "您的验证码是 ${code},5 分钟内有效,请勿泄露。"
|
||||
*/
|
||||
import Dysmsapi, * as $Dysmsapi from "@alicloud/dysmsapi20170525";
|
||||
import * as $OpenApi from "@alicloud/openapi-client";
|
||||
import * as $Util from "@alicloud/tea-util";
|
||||
|
||||
let client: Dysmsapi | null = null;
|
||||
|
||||
function getClient(): Dysmsapi | null {
|
||||
if (client) return client;
|
||||
const accessKeyId = process.env.SMS_ACCESS_KEY;
|
||||
const accessKeySecret = process.env.SMS_SECRET_KEY;
|
||||
if (!accessKeyId || !accessKeySecret) return null;
|
||||
const config = new $OpenApi.Config({ accessKeyId, accessKeySecret });
|
||||
config.endpoint = "dysmsapi.aliyuncs.com";
|
||||
client = new Dysmsapi(config);
|
||||
return client;
|
||||
}
|
||||
|
||||
export interface SendOtpResult {
|
||||
ok: boolean;
|
||||
/** 阿里云返回的 BizId,排查问题时给阿里云工单用 */
|
||||
bizId?: string;
|
||||
/** 阿里云错误码 / 失败原因 */
|
||||
errorCode?: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 6 位验证码短信。
|
||||
* 返回 ok=true 表示阿里云已受理 (用户收到短信通常在 30 秒内);
|
||||
* ok=false 时 errorCode 给出原因 (如 isv.MOBILE_NUMBER_ILLEGAL / isv.BUSINESS_LIMIT_CONTROL 等)。
|
||||
*
|
||||
* 若 SMS_ACCESS_KEY/SMS_SECRET_KEY 未配置,函数直接返回 ok=false 且 errorCode='SMS_NOT_CONFIGURED',
|
||||
* 调用方可根据该信号 fallback 到 dev console.log 输出验证码。
|
||||
*/
|
||||
export async function sendOtpSms(
|
||||
phone: string,
|
||||
code: string,
|
||||
): Promise<SendOtpResult> {
|
||||
const c = getClient();
|
||||
const signName = process.env.SMS_SIGN_NAME;
|
||||
const templateCode = process.env.SMS_TEMPLATE_CODE;
|
||||
if (!c || !signName || !templateCode) {
|
||||
return { ok: false, errorCode: "SMS_NOT_CONFIGURED" };
|
||||
}
|
||||
|
||||
const req = new $Dysmsapi.SendSmsRequest({
|
||||
phoneNumbers: phone,
|
||||
signName,
|
||||
templateCode,
|
||||
templateParam: JSON.stringify({ code }),
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await c.sendSmsWithOptions(req, new $Util.RuntimeOptions({}));
|
||||
const body = resp.body;
|
||||
if (body?.code === "OK") {
|
||||
return { ok: true, bizId: body.bizId };
|
||||
}
|
||||
return {
|
||||
ok: false,
|
||||
errorCode: body?.code ?? "UNKNOWN",
|
||||
errorMessage: body?.message,
|
||||
};
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
return { ok: false, errorCode: "EXCEPTION", errorMessage: msg };
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user