Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
72 lines
2.4 KiB
TypeScript
72 lines
2.4 KiB
TypeScript
// OTP helpers — 6-digit codes stored in Redis with 5-min TTL.
|
|
// SMS sending is a stub until a provider (Kavenegar, SMS.ir, etc.) is configured.
|
|
|
|
import { randomInt } from 'node:crypto';
|
|
import { redis } from '../redis.js';
|
|
|
|
const OTP_TTL_SECONDS = 5 * 60;
|
|
const OTP_MAX_ATTEMPTS = 3;
|
|
|
|
const otpKey = (phone: string) => `otp:${phone}`;
|
|
const otpAttemptsKey = (phone: string) => `otp:attempts:${phone}`;
|
|
|
|
function generate6(): string {
|
|
return String(100000 + randomInt(900000));
|
|
}
|
|
|
|
type OtpSendResult = { sent: true; provider: string } | { sent: false; reason: 'sms_not_configured' | 'unsupported_provider' | 'provider_error'; provider?: string };
|
|
|
|
async function sendOtpSms(phone: string, code: string): Promise<OtpSendResult> {
|
|
const provider = process.env.SMS_PROVIDER;
|
|
if (!provider || !process.env.SMS_API_KEY) {
|
|
return { sent: false, reason: 'sms_not_configured' };
|
|
}
|
|
if (provider !== 'kavenegar') return { sent: false, reason: 'unsupported_provider', provider };
|
|
|
|
const sender = process.env.SMS_SENDER;
|
|
const params = new URLSearchParams({
|
|
receptor: phone,
|
|
message: `کد تایید تکناو: ${code}`,
|
|
});
|
|
if (sender) params.set('sender', sender);
|
|
|
|
try {
|
|
const res = await fetch(`https://api.kavenegar.com/v1/${process.env.SMS_API_KEY}/sms/send.json`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
body: params,
|
|
});
|
|
if (!res.ok) return { sent: false, reason: 'provider_error', provider };
|
|
return { sent: true, provider };
|
|
} catch {
|
|
return { sent: false, reason: 'provider_error', provider };
|
|
}
|
|
}
|
|
|
|
export async function issueOtp(phone: string): Promise<OtpSendResult> {
|
|
const code = generate6();
|
|
const result = await sendOtpSms(phone, code);
|
|
if (result.sent) {
|
|
await redis.set(otpKey(phone), code, 'EX', OTP_TTL_SECONDS);
|
|
await redis.del(otpAttemptsKey(phone));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
export async function verifyOtp(phone: string, code: string): Promise<boolean> {
|
|
const stored = await redis.get(otpKey(phone));
|
|
if (!stored) return false;
|
|
|
|
const attempts = Number(await redis.incr(otpAttemptsKey(phone)));
|
|
if (attempts === 1) await redis.expire(otpAttemptsKey(phone), OTP_TTL_SECONDS);
|
|
if (attempts > OTP_MAX_ATTEMPTS) {
|
|
await redis.del(otpKey(phone));
|
|
return false;
|
|
}
|
|
|
|
if (stored !== code) return false;
|
|
await redis.del(otpKey(phone));
|
|
await redis.del(otpAttemptsKey(phone));
|
|
return true;
|
|
}
|