duoqi-api/src/services/auth/aliyun-sms.ts
Wang Zhuoxuan 0317c34099
All checks were successful
CI/CD Pipeline / Unit Tests (push) Successful in 19s
CI/CD Pipeline / Build & Deploy Test (push) Has been skipped
CI/CD Pipeline / Build & Deploy Production (push) Successful in 1m18s
fix: handle Aliyun SMS validation errors
2026-06-04 14:12:56 +08:00

150 lines
5.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { createRequire } from 'node:module';
import { $OpenApiUtil } from '@alicloud/openapi-core';
import { RuntimeOptions } from '@darabonba/typescript';
import { config } from '../../utils/config.js';
import { AppError, ValidationError } from '../../utils/errors.js';
// 阿里云 SDK 是 Darabonba 生成的 CJS 模块ESM import 在不同运行时下行为不一致
// (Bun / Bun node 兼容模式 / Node.js 对 __esModule 和 exports.default 解析不同)
// 使用 createRequire 直接走 CJS require 路径,绕开所有 ESM/CJS 互操作问题
const require = createRequire(import.meta.url);
const DypnsClient = require('@alicloud/dypnsapi20170525').default;
const { SendSmsVerifyCodeRequest, CheckSmsVerifyCodeRequest } = require('@alicloud/dypnsapi20170525/dist/models/model.js');
type SmsOperation = 'send' | 'verify';
type AliyunSdkError = {
code?: unknown;
message?: unknown;
data?: {
Code?: unknown;
Message?: unknown;
};
};
function createClient() {
const openApiConfig = new $OpenApiUtil.Config({
accessKeyId: config.ALIYUN_ACCESS_KEY_ID,
accessKeySecret: config.ALIYUN_ACCESS_KEY_SECRET,
endpoint: 'dypnsapi.aliyuncs.com',
});
return new DypnsClient(openApiConfig);
}
function assertSmsConfigured(): void {
if (
!config.ALIYUN_ACCESS_KEY_ID ||
!config.ALIYUN_ACCESS_KEY_SECRET ||
!config.ALIYUN_SMS_SIGN_NAME ||
!config.ALIYUN_SMS_TEMPLATE_CODE
) {
throw new AppError('SMS service is not configured on the server', 503, 'SERVICE_UNAVAILABLE');
}
}
function getString(value: unknown): string | undefined {
return typeof value === 'string' && value.length > 0 ? value : undefined;
}
function invalidVerifyCodeError(): AppError {
return new AppError('验证码无效或已过期', 401, 'INVALID_VERIFY_CODE');
}
function mapAliyunError(code: string, message: string, operation: SmsOperation): AppError {
switch (code) {
case 'MOBILE_NUMBER_ILLEGAL':
return new ValidationError('Invalid phone number');
case 'isv.ValidateFail':
return operation === 'verify'
? invalidVerifyCodeError()
: new AppError('SMS service error: verification failed', 502, 'SMS_SERVICE_ERROR');
case 'BUSINESS_LIMIT_CONTROL':
case 'FREQUENCY_FAIL':
return new AppError('Too many SMS requests, please try again later', 429, 'RATE_LIMITED');
case 'isv.BUSINESS_LIMIT_CONTROL':
return new AppError('SMS daily limit reached for this number', 429, 'RATE_LIMITED');
case 'biz.FREQUENCY':
return new AppError('Too many SMS requests, please try again later', 429, 'RATE_LIMITED');
case 'InternalError':
return new AppError('SMS provider temporarily unavailable, please try again', 503, 'SMS_PROVIDER_ERROR');
case 'INVALID_PARAMETERS':
return new ValidationError('Invalid request parameters');
default:
return new AppError(
`SMS service error: ${message || code}`,
502,
'SMS_SERVICE_ERROR',
);
}
}
export function mapAliyunException(error: unknown, operation: SmsOperation): AppError {
if (error instanceof AppError) return error;
if (typeof error === 'object' && error !== null) {
const sdkError = error as AliyunSdkError;
const code = getString(sdkError.data?.Code) ?? getString(sdkError.code);
const message = getString(sdkError.data?.Message) ?? getString(sdkError.message);
if (code) {
return mapAliyunError(code, message ?? code, operation);
}
}
return new AppError('SMS service error: request failed', 502, 'SMS_SERVICE_ERROR');
}
const RUNTIME_OPTIONS = new RuntimeOptions({
readTimeout: 10000,
connectTimeout: 5000,
});
export async function sendCode(phoneNumber: string): Promise<void> {
assertSmsConfigured();
const request = new SendSmsVerifyCodeRequest({
phoneNumber,
signName: config.ALIYUN_SMS_SIGN_NAME,
templateCode: config.ALIYUN_SMS_TEMPLATE_CODE,
templateParam: config.ALIYUN_SMS_TEMPLATE_PARAM,
});
const response = await createClient().sendSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS).catch((error: unknown) => {
throw mapAliyunException(error, 'send');
});
if (!response.body || response.body.code !== 'OK') {
throw mapAliyunError(
response.body?.code ?? 'UNKNOWN',
response.body?.message ?? 'Unknown SMS error',
'send',
);
}
}
export async function verifyCode(phoneNumber: string, code: string): Promise<void> {
assertSmsConfigured();
const request = new CheckSmsVerifyCodeRequest({
phoneNumber,
verifyCode: code,
caseAuthPolicy: 1,
});
const response = await createClient().checkSmsVerifyCodeWithOptions(request, RUNTIME_OPTIONS).catch((error: unknown) => {
throw mapAliyunException(error, 'verify');
});
if (!response.body || response.body.code !== 'OK') {
throw mapAliyunError(
response.body?.code ?? 'UNKNOWN',
response.body?.message ?? 'Unknown SMS verification error',
'verify',
);
}
if (response.body.model?.verifyResult !== 'PASS') {
throw invalidVerifyCodeError();
}
}