Import source: app/backend/src/lib/otp
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// 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;
|
||||
}
|
||||
Reference in New Issue
Block a user