fixing platform admin
This commit is contained in:
@@ -0,0 +1,212 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useAdminI18n } from '@/components/I18nProvider'
|
||||
import PublicShell from '@/components/PublicShell'
|
||||
|
||||
const API_BASE = '/api/v1'
|
||||
|
||||
export default function AdminLoginPage() {
|
||||
const { language } = useAdminI18n()
|
||||
const dict = {
|
||||
en: {
|
||||
loginFailed: 'Login failed',
|
||||
verifyFailed: '2FA verification failed',
|
||||
brand: 'Admin console',
|
||||
access: 'Platform operations access',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
signIn: 'Sign in',
|
||||
signingIn: 'Signing in…',
|
||||
enterCode: 'Enter the 6-digit code from your authenticator app',
|
||||
authCode: 'Authentication code',
|
||||
verify: 'Verify',
|
||||
verifying: 'Verifying…',
|
||||
back: 'Back to login',
|
||||
},
|
||||
fr: {
|
||||
loginFailed: 'Échec de connexion',
|
||||
verifyFailed: 'Échec de la vérification 2FA',
|
||||
brand: 'Console admin',
|
||||
access: 'Accès opérations plateforme',
|
||||
email: 'Email',
|
||||
password: 'Mot de passe',
|
||||
signIn: 'Connexion',
|
||||
signingIn: 'Connexion…',
|
||||
enterCode: 'Entrez le code à 6 chiffres de votre application d’authentification',
|
||||
authCode: 'Code d’authentification',
|
||||
verify: 'Vérifier',
|
||||
verifying: 'Vérification…',
|
||||
back: 'Retour à la connexion',
|
||||
},
|
||||
ar: {
|
||||
loginFailed: 'فشل تسجيل الدخول',
|
||||
verifyFailed: 'فشل التحقق الثنائي',
|
||||
brand: 'لوحة الإدارة',
|
||||
access: 'وصول عمليات المنصة',
|
||||
email: 'البريد الإلكتروني',
|
||||
password: 'كلمة المرور',
|
||||
signIn: 'تسجيل الدخول',
|
||||
signingIn: 'جارٍ تسجيل الدخول…',
|
||||
enterCode: 'أدخل الرمز المكون من 6 أرقام من تطبيق المصادقة',
|
||||
authCode: 'رمز المصادقة',
|
||||
verify: 'تحقق',
|
||||
verifying: 'جارٍ التحقق…',
|
||||
back: 'العودة إلى تسجيل الدخول',
|
||||
},
|
||||
}[language]
|
||||
const router = useRouter()
|
||||
const [step, setStep] = useState<'credentials' | 'totp'>('credentials')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [totp, setTotp] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
async function handleCredentials(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password }),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (res.status === 401 && json?.error === 'totp_required') {
|
||||
setStep('totp')
|
||||
return
|
||||
}
|
||||
if (!res.ok) throw new Error(json?.message ?? dict.loginFailed)
|
||||
if (json.data?.token) {
|
||||
localStorage.setItem('admin_token', json.data.token)
|
||||
router.push('/dashboard')
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTotp(e: React.FormEvent) {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/admin/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email, password, totpCode: totp }),
|
||||
})
|
||||
const json = await res.json()
|
||||
if (!res.ok) throw new Error(json?.message ?? dict.verifyFailed)
|
||||
if (json.data?.token) {
|
||||
localStorage.setItem('admin_token', json.data.token)
|
||||
router.push('/dashboard')
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PublicShell>
|
||||
<main className="flex flex-1 items-center justify-center px-4">
|
||||
<div className="w-full max-w-md">
|
||||
<div className="mb-8 text-center">
|
||||
<Link href="/" className="text-xs font-semibold uppercase tracking-[0.2em] text-emerald-400">RentalDriveGo</Link>
|
||||
<h1 className="mt-3 text-3xl font-black tracking-tight">{dict.brand}</h1>
|
||||
<p className="mt-1 text-sm text-zinc-400">{dict.access}</p>
|
||||
</div>
|
||||
|
||||
<div className="panel p-8">
|
||||
{error && (
|
||||
<div className="mb-6 p-3 rounded-xl border border-red-900/50 bg-red-950/50 text-sm text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'credentials' ? (
|
||||
<form onSubmit={handleCredentials} className="space-y-5">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-1.5">{dict.email}</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 placeholder:text-zinc-500 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
||||
placeholder="admin@rentaldrivego.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-1.5">{dict.password}</label>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="w-full px-3 py-2.5 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 text-sm focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2.5 px-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-semibold transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? dict.signingIn : dict.signIn}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<form onSubmit={handleTotp} className="space-y-5">
|
||||
<div className="text-center mb-2">
|
||||
<div className="inline-flex h-12 w-12 items-center justify-center rounded-full bg-emerald-900/40 mb-3">
|
||||
<svg className="h-6 w-6 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm text-zinc-300">{dict.enterCode}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-zinc-300 mb-1.5">{dict.authCode}</label>
|
||||
<input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]{6}"
|
||||
maxLength={6}
|
||||
required
|
||||
value={totp}
|
||||
onChange={(e) => setTotp(e.target.value.replace(/\D/g, ''))}
|
||||
className="w-full px-3 py-2.5 rounded-xl bg-zinc-800 border border-zinc-700 text-zinc-100 text-center text-2xl tracking-[0.5em] placeholder:text-zinc-500 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:border-transparent"
|
||||
placeholder="000000"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full py-2.5 px-4 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white text-sm font-semibold transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? dict.verifying : dict.verify}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setStep('credentials'); setTotp(''); setError(null) }}
|
||||
className="w-full text-sm text-zinc-500 hover:text-zinc-300 transition-colors"
|
||||
>
|
||||
{dict.back}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</PublicShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user