e6460de4d0
Build & Deploy / Build & Push Docker Image (push) Failing after 7s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 26s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Storefront Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
193 lines
8.8 KiB
TypeScript
193 lines
8.8 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { useState, Suspense } from 'react'
|
|
import { useSearchParams, useRouter } from 'next/navigation'
|
|
import PublicShell from '@/components/PublicShell'
|
|
import { ADMIN_API_BASE } from '@/lib/api'
|
|
|
|
export default function AdminResetPasswordPage() {
|
|
return (
|
|
<Suspense>
|
|
<AdminResetPasswordContent />
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
function AdminResetPasswordContent() {
|
|
const searchParams = useSearchParams()
|
|
const router = useRouter()
|
|
const token = searchParams.get('token')
|
|
|
|
const [password, setPassword] = useState('')
|
|
const [showPassword, setShowPassword] = useState(false)
|
|
const [confirm, setConfirm] = useState('')
|
|
const [showConfirm, setShowConfirm] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
const [done, setDone] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
|
|
e.preventDefault()
|
|
if (password.length < 8) { setError('Password must be at least 8 characters.'); return }
|
|
if (password !== confirm) { setError('Passwords do not match.'); return }
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch(`${ADMIN_API_BASE}/admin/auth/reset-password`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ token, password }),
|
|
})
|
|
const json = await res.json()
|
|
if (!res.ok) {
|
|
if (json?.error === 'invalid_token') throw new Error('This reset link is invalid or has expired.')
|
|
throw new Error('Something went wrong. Please try again.')
|
|
}
|
|
setDone(true)
|
|
} catch (err: any) {
|
|
setError(err.message ?? 'Something went wrong.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
if (!token) {
|
|
return (
|
|
<AdminResetShell>
|
|
<p className="text-sm text-zinc-400 text-center">No reset token found.</p>
|
|
<div className="mt-4 text-center">
|
|
<Link href="/forgot-password" className="text-sm text-emerald-400 hover:text-emerald-300">
|
|
Request a new reset link
|
|
</Link>
|
|
</div>
|
|
</AdminResetShell>
|
|
)
|
|
}
|
|
|
|
if (done) {
|
|
return (
|
|
<AdminResetShell>
|
|
<div className="space-y-3 text-center">
|
|
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-emerald-900/40">
|
|
<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="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</div>
|
|
<p className="text-sm text-zinc-300 font-semibold">Password updated</p>
|
|
<p className="text-xs text-zinc-500">You can now sign in with your new password.</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => router.push('/login')}
|
|
className="mt-2 inline-flex w-full justify-center rounded-full bg-stone-900 px-6 py-3 text-sm font-semibold text-white transition hover:bg-orange-700 dark:bg-orange-400 dark:text-white dark:hover:bg-orange-300"
|
|
>
|
|
Sign in
|
|
</button>
|
|
</div>
|
|
</AdminResetShell>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<AdminResetShell>
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
{error && (
|
|
<div className="rounded-xl border border-red-900/50 bg-red-950/50 px-4 py-3 text-sm text-red-400">{error}</div>
|
|
)}
|
|
<div>
|
|
<label className="mb-1.5 block text-sm font-medium text-zinc-300">New password</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showPassword ? 'text' : 'password'}
|
|
required
|
|
minLength={8}
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
placeholder="••••••••"
|
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 pr-10 text-sm text-stone-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-[#07101e]/80 dark:text-stone-100"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword((v) => !v)}
|
|
className="absolute inset-y-0 right-3 flex items-center text-stone-400 hover:text-stone-700 dark:text-stone-500 dark:hover:text-stone-200"
|
|
tabIndex={-1}
|
|
>
|
|
{showPassword ? (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="mb-1.5 block text-sm font-medium text-zinc-300">Confirm password</label>
|
|
<div className="relative">
|
|
<input
|
|
type={showConfirm ? 'text' : 'password'}
|
|
required
|
|
minLength={8}
|
|
value={confirm}
|
|
onChange={(e) => setConfirm(e.target.value)}
|
|
placeholder="••••••••"
|
|
className="w-full rounded-2xl border border-stone-200 bg-white px-4 py-3 pr-10 text-sm text-stone-900 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-orange-500 dark:border-blue-800 dark:bg-[#07101e]/80 dark:text-stone-100"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowConfirm((v) => !v)}
|
|
className="absolute inset-y-0 right-3 flex items-center text-stone-400 hover:text-stone-700 dark:text-stone-500 dark:hover:text-stone-200"
|
|
tabIndex={-1}
|
|
>
|
|
{showConfirm ? (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
|
|
</svg>
|
|
) : (
|
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
|
|
</svg>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="inline-flex w-full justify-center rounded-full bg-stone-900 px-6 py-3 text-sm font-semibold text-white transition hover:bg-orange-700 disabled:opacity-50 dark:bg-orange-400 dark:text-white dark:hover:bg-orange-300"
|
|
>
|
|
{loading ? 'Resetting…' : 'Reset password'}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="mt-6 text-center">
|
|
<Link href="/login" className="text-sm text-zinc-500 hover:text-zinc-300 transition-colors">
|
|
Back to sign in
|
|
</Link>
|
|
</div>
|
|
</AdminResetShell>
|
|
)
|
|
}
|
|
|
|
function AdminResetShell({ children }: { children: React.ReactNode }) {
|
|
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-orange-700 dark:text-orange-300">RentalDriveGo</Link>
|
|
<h1 className="mt-3 text-3xl font-black tracking-tight">Reset password</h1>
|
|
</div>
|
|
<div className="panel p-8">{children}</div>
|
|
</div>
|
|
</main>
|
|
</PublicShell>
|
|
)
|
|
}
|