feat(auth): wire Aliyun SMS provider for phone OTP login
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:
iye 2026-05-13 14:56:47 +08:00
parent 15af8e1781
commit 0a7c1ec130
6 changed files with 436 additions and 19 deletions

View File

@ -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
View File

@ -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):

View File

@ -1,4 +1,5 @@
allowBuilds:
'@alicloud/openapi-core': false
'@prisma/client': true
'@prisma/engines': true
esbuild: true

View File

@ -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));
// 缓存到 Redis5 分钟过期)
// 缓存到 Redis5 分钟过期)。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();

View File

@ -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
View 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 };
}
}