feat(auth): mobile-only register/login with OTP verification

- Register and login by mobile number; email is now an optional
  contact field only (never used to authenticate)
- After registration, the phone is verified via a 6-digit SMS code
- Login supports both password and one-time-code (OTP) methods
- Phone OTP delivered via Kavenegar (verify/lookup); API key in env
- Account page: edit name/optional email, change password, and
  change mobile number with OTP re-verification
- Codes are hashed, expire in 5m, capped at 5 attempts, rate-limited
- Seed gives the admin a verified phone so mobile login still works

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
keyhan
2026-06-16 16:40:08 +03:30
parent ce6813db99
commit 37c103fa20
31 changed files with 1756 additions and 143 deletions
+71 -4
View File
@@ -119,11 +119,20 @@ const fa = {
backHome: 'بازگشت به خانه',
login: {
title: 'خوش آمدی',
subtitle: ه حساب ابربان خود وارد شو',
subtitle: ا شماره موبایل وارد حساب ابربان شو',
altPrompt: 'هنوز حساب نداری؟',
altLabel: 'ثبت‌نام کن',
email: 'ایمیل',
tabPassword: 'با رمز عبور',
tabOtp: 'با رمز یکبارمصرف',
phone: 'شماره موبایل',
password: 'رمز عبور',
otpHint: 'یک کد تأیید به شماره موبایلت پیامک می‌شود.',
sendCode: 'ارسال کد',
sending: 'در حال ارسال…',
codeSent: 'کد تأیید پیامک شد',
verifyNeeded: 'برای ادامه، شماره موبایلت را تأیید کن.',
otpTitle: 'تأیید شماره موبایل',
otpSubtitle: 'کد ۶ رقمی پیامک‌شده را وارد کن',
submit: 'ورود',
submitting: 'در حال ورود…',
success: 'با موفقیت وارد شدی!',
@@ -138,13 +147,25 @@ const fa = {
firstNamePlaceholder: 'مثلاً علی',
lastName: 'نام خانوادگی',
lastNamePlaceholder: 'مثلاً رضایی',
email: 'ایمیل',
phone: 'شماره موبایل',
emailOptional: 'ایمیل (اختیاری)',
password: 'رمز عبور',
passwordPlaceholder: 'حداقل ۸ کاراکتر',
submit: 'ساخت حساب',
submitting: 'در حال ساخت حساب…',
success: 'حساب با موفقیت ساخته شد!',
codeSent: 'کد تأیید به موبایلت پیامک شد',
success: 'حساب با موفقیت ساخته و تأیید شد!',
error: 'ثبت‌نام ناموفق بود',
verifyError: 'تأیید کد ناموفق بود',
},
otp: {
sentTo: 'کد تأیید به این شماره پیامک شد:',
codeLabel: 'کد تأیید',
verify: 'تأیید و ادامه',
verifying: 'در حال بررسی…',
back: 'بازگشت',
resend: 'ارسال مجدد کد',
resendIn: 'ارسال مجدد تا {s} ثانیه',
},
},
@@ -390,6 +411,7 @@ const fa = {
wallet: 'کیف‌پول',
invoices: 'فاکتورها',
tickets: 'تیکت‌ها',
account: 'حساب من',
users: 'کاربران',
allApplications: 'همهٔ اپلیکیشن‌ها',
billingPlans: 'پلن‌های صورت‌حساب',
@@ -401,6 +423,48 @@ const fa = {
},
dashboard: {
account: {
title: 'حساب من',
subtitle: 'اطلاعات حساب، شماره موبایل و رمز عبورت را مدیریت کن.',
personalInfo: 'اطلاعات شخصی',
firstName: 'نام',
lastName: 'نام خانوادگی',
emailOptional: 'ایمیل (اختیاری)',
emailHint: 'ایمیل فقط برای ارتباط است و در ورود استفاده نمی‌شود.',
save: 'ذخیره',
saving: 'در حال ذخیره…',
savedName: 'اطلاعات با موفقیت ذخیره شد',
verified: 'تأیید شده',
unverified: 'تأیید نشده',
notSet: 'ثبت نشده',
verifyNow: 'تأیید',
currentPassword: 'رمز عبور فعلی',
passwordReason: 'برای تغییر شماره موبایل، رمز فعلی لازم است.',
sendCode: 'ارسال کد',
sending: 'در حال ارسال…',
codeSent: 'کد تأیید پیامک شد',
codeSentTo: 'کد تأیید به این شماره پیامک شد:',
enterCode: 'کد تأیید',
confirm: 'تأیید',
verifying: 'در حال بررسی…',
resend: 'ارسال مجدد کد',
cancel: 'انصراف',
phoneUpdated: 'شماره موبایل با موفقیت به‌روزرسانی شد',
changePassword: 'تغییر رمز عبور',
newPassword: 'رمز عبور جدید',
confirmPassword: 'تکرار رمز عبور جدید',
passwordMismatch: 'رمز عبور جدید و تکرار آن یکسان نیستند',
passwordChanged: 'رمز عبور با موفقیت تغییر کرد',
passwordHint: 'حداقل ۸ کاراکتر',
errorGeneric: 'انجام عملیات ناموفق بود',
phone: {
label: 'شماره موبایل',
empty: 'شماره‌ای ثبت نشده',
add: 'افزودن شماره',
change: 'تغییر شماره',
newLabel: 'شماره موبایل جدید',
},
},
status: {
running: 'در حال اجرا',
pending: 'در انتظار',
@@ -678,6 +742,9 @@ const fa = {
notAvailableMessage: 'Elasticsearch مرکزی روی کلاستر مستقر نشده است. هنگام انتشار اپ، افزونهٔ لاگینگ را فعال کن و از مدیر بخواه استک لاگینگ را مستقر کند.',
checkingConnection: 'در حال بررسی اتصال…',
retryingAuto: 'هر چند ثانیه به‌صورت خودکار تلاش می‌شود.',
elasticDisabledTitle: 'لاگ‌گیری Elasticsearch فعال نیست',
elasticDisabledMessage: 'برای این اپلیکیشن هنگام ساخت، گزینهٔ Elasticsearch را فعال نکرده‌ای؛ به همین دلیل لاگ متمرکز در دسترس نیست. هنگام انتشار یک اپ جدید افزونهٔ Elasticsearch را فعال کن تا لاگ‌های آن اینجا نمایش داده شود.',
elasticDisabledMessageAll: 'هیچ‌کدام از اپلیکیشن‌هایت هنگام ساخت، Elasticsearch را فعال نکرده‌اند؛ به همین دلیل لاگ متمرکز در دسترس نیست. هنگام انتشار یک اپ جدید افزونهٔ Elasticsearch را فعال کن تا لاگ‌های آن اینجا نمایش داده شود.',
},
users: {
title: 'مدیریت کاربران',