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',