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
@@ -120,11 +120,20 @@ const en: Dictionary = {
backHome: 'Back to home',
login: {
title: 'Welcome back',
subtitle: 'Sign in to your Abrban account',
subtitle: 'Sign in to your Abrban account with your mobile',
altPrompt: 'Dont have an account yet?',
altLabel: 'Sign up',
email: 'Email',
tabPassword: 'With password',
tabOtp: 'With one-time code',
phone: 'Mobile number',
password: 'Password',
otpHint: 'A verification code will be texted to your mobile.',
sendCode: 'Send code',
sending: 'Sending…',
codeSent: 'Verification code sent',
verifyNeeded: 'Verify your mobile number to continue.',
otpTitle: 'Verify mobile number',
otpSubtitle: 'Enter the 6-digit code we texted you',
submit: 'Sign in',
submitting: 'Signing in…',
success: 'Signed in successfully!',
@@ -139,13 +148,25 @@ const en: Dictionary = {
firstNamePlaceholder: 'e.g. Ali',
lastName: 'Last name',
lastNamePlaceholder: 'e.g. Rezaei',
email: 'Email',
phone: 'Mobile number',
emailOptional: 'Email (optional)',
password: 'Password',
passwordPlaceholder: 'At least 8 characters',
submit: 'Create account',
submitting: 'Creating account…',
success: 'Account created successfully!',
codeSent: 'A verification code was texted to your mobile',
success: 'Account created and verified!',
error: 'Sign up failed',
verifyError: 'Code verification failed',
},
otp: {
sentTo: 'A verification code was texted to:',
codeLabel: 'Verification code',
verify: 'Verify and continue',
verifying: 'Verifying…',
back: 'Back',
resend: 'Resend code',
resendIn: 'Resend in {s}s',
},
},
@@ -391,6 +412,7 @@ const en: Dictionary = {
wallet: 'Wallet',
invoices: 'Invoices',
tickets: 'Tickets',
account: 'My Account',
users: 'Users',
allApplications: 'All Applications',
billingPlans: 'Billing Plans',
@@ -402,6 +424,48 @@ const en: Dictionary = {
},
dashboard: {
account: {
title: 'My Account',
subtitle: 'Manage your account details, mobile number and password.',
personalInfo: 'Personal information',
firstName: 'First name',
lastName: 'Last name',
emailOptional: 'Email (optional)',
emailHint: 'Email is for contact only and is not used to sign in.',
save: 'Save',
saving: 'Saving…',
savedName: 'Profile saved successfully',
verified: 'Verified',
unverified: 'Unverified',
notSet: 'Not set',
verifyNow: 'Verify',
currentPassword: 'Current password',
passwordReason: 'Your current password is required to change your mobile number.',
sendCode: 'Send code',
sending: 'Sending…',
codeSent: 'Verification code sent',
codeSentTo: 'A verification code was texted to:',
enterCode: 'Verification code',
confirm: 'Confirm',
verifying: 'Verifying…',
resend: 'Resend code',
cancel: 'Cancel',
phoneUpdated: 'Mobile number updated successfully',
changePassword: 'Change password',
newPassword: 'New password',
confirmPassword: 'Confirm new password',
passwordMismatch: 'New password and confirmation do not match',
passwordChanged: 'Password changed successfully',
passwordHint: 'At least 8 characters',
errorGeneric: 'Operation failed',
phone: {
label: 'Mobile number',
empty: 'No number set',
add: 'Add number',
change: 'Change number',
newLabel: 'New mobile number',
},
},
status: {
running: 'Running',
pending: 'Pending',
@@ -679,6 +743,9 @@ const en: Dictionary = {
notAvailableMessage: 'Central Elasticsearch is not deployed on the cluster. Enable the logging addon when deploying an app, and ask an administrator to deploy the logging stack.',
checkingConnection: 'Checking connection…',
retryingAuto: 'Retrying automatically every few seconds.',
elasticDisabledTitle: 'Elasticsearch logging is off',
elasticDisabledMessage: 'You did not enable Elasticsearch for this application when creating it, so centralized logs are unavailable. Enable the Elasticsearch addon when deploying a new app to see its logs here.',
elasticDisabledMessageAll: 'None of your applications have Elasticsearch enabled, so centralized logs are unavailable. Enable the Elasticsearch addon when deploying a new app to see its logs here.',
},
users: {
title: 'User Management',
+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: 'مدیریت کاربران',