204 lines
8.9 KiB
TypeScript
204 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState, useTransition, Suspense } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { ShieldCheck, Lock, Mail, Eye, EyeOff, ArrowRight, Loader2, AlertCircle } from "lucide-react";
|
|
import toast from "react-hot-toast";
|
|
import { loginAdminAction } from "@/app/actions/auth";
|
|
|
|
function LoginForm() {
|
|
const router = useRouter();
|
|
const searchParams = useSearchParams();
|
|
const from = searchParams.get("from") || "/dashboard";
|
|
|
|
const [identifier, setIdentifier] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [rememberMe, setRememberMe] = useState(true);
|
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
const [isPending, startTransition] = useTransition();
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setErrorMessage(null);
|
|
|
|
if (!identifier.trim() || !password) {
|
|
setErrorMessage("Veuillez remplir tous les champs.");
|
|
return;
|
|
}
|
|
|
|
startTransition(async () => {
|
|
try {
|
|
const res = await loginAdminAction({
|
|
identifier: identifier.trim(),
|
|
password,
|
|
rememberMe,
|
|
});
|
|
|
|
if (!res.success) {
|
|
setErrorMessage(res.error || "Échec de l'authentification.");
|
|
toast.error(res.error || "Erreur lors de la connexion");
|
|
return;
|
|
}
|
|
|
|
toast.success(`Bienvenue, ${res.user?.name || "Administrateur"} !`);
|
|
// Full page redirect ensures middleware & server cookies are freshly synchronized
|
|
window.location.href = from.startsWith("/") && !from.startsWith("//") && !from.startsWith("/\\") ? from : "/dashboard";
|
|
} catch (err) {
|
|
console.error("Login client error:", err);
|
|
setErrorMessage("Une erreur imprévue est survenue. Veuillez réessayer.");
|
|
toast.error("Erreur serveur");
|
|
}
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex items-center justify-center p-4 sm:p-6 bg-[#0b0f19] relative overflow-hidden">
|
|
{/* Background ambient lighting */}
|
|
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[500px] h-[500px] bg-indigo-600/15 rounded-full blur-[120px] pointer-events-none" />
|
|
<div className="absolute bottom-10 right-10 w-80 h-80 bg-violet-600/10 rounded-full blur-[100px] pointer-events-none" />
|
|
|
|
<div className="w-full max-w-md relative z-10">
|
|
{/* Card Header & Brand */}
|
|
<div className="text-center mb-8">
|
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-gradient-to-tr from-indigo-600 to-violet-500 shadow-xl shadow-indigo-500/25 mb-4 ring-4 ring-indigo-500/20">
|
|
<ShieldCheck className="w-9 h-9 text-white" />
|
|
</div>
|
|
<h1 className="text-3xl font-extrabold text-white tracking-tight">AfroAdmin</h1>
|
|
<p className="text-sm text-slate-400 mt-2">
|
|
Portail d'administration sécurisé Afrohub
|
|
</p>
|
|
</div>
|
|
|
|
{/* Card Form */}
|
|
<div className="bg-slate-900/80 backdrop-blur-xl border border-slate-800/80 rounded-2xl shadow-2xl p-6 sm:p-8">
|
|
{errorMessage && (
|
|
<div className="mb-6 p-4 rounded-xl bg-red-950/40 border border-red-800/60 flex items-start gap-3 text-red-200 text-sm animate-shake">
|
|
<AlertCircle className="w-5 h-5 text-red-400 shrink-0 mt-0.5" />
|
|
<div className="flex-1">{errorMessage}</div>
|
|
</div>
|
|
)}
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-5">
|
|
{/* Identifier (Email / Username) */}
|
|
<div>
|
|
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wider mb-2">
|
|
Email ou Identifiant
|
|
</label>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Mail className="w-5 h-5" />
|
|
</div>
|
|
<input
|
|
id="admin-identifier"
|
|
type="text"
|
|
autoComplete="username"
|
|
required
|
|
placeholder="admin@afrohub.com"
|
|
value={identifier}
|
|
onChange={(e) => setIdentifier(e.target.value)}
|
|
disabled={isPending}
|
|
className="w-full pl-11 pr-4 py-3 bg-slate-800/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 text-sm focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-all disabled:opacity-50"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Password */}
|
|
<div>
|
|
<div className="flex items-center justify-between mb-2">
|
|
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wider">
|
|
Mot de passe
|
|
</label>
|
|
</div>
|
|
<div className="relative">
|
|
<div className="absolute inset-y-0 left-0 pl-3.5 flex items-center pointer-events-none text-slate-400">
|
|
<Lock className="w-5 h-5" />
|
|
</div>
|
|
<input
|
|
id="admin-password"
|
|
type={showPassword ? "text" : "password"}
|
|
autoComplete="current-password"
|
|
required
|
|
placeholder="••••••••••••"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
disabled={isPending}
|
|
className="w-full pl-11 pr-11 py-3 bg-slate-800/80 border border-slate-700/80 rounded-xl text-white placeholder-slate-500 text-sm focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500/20 transition-all disabled:opacity-50"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-slate-400 hover:text-slate-200 focus:outline-none transition-colors"
|
|
aria-label={showPassword ? "Masquer le mot de passe" : "Afficher le mot de passe"}
|
|
>
|
|
{showPassword ? <EyeOff className="w-5 h-5" /> : <Eye className="w-5 h-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Remember Me */}
|
|
<div className="flex items-center justify-between text-sm">
|
|
<label className="flex items-center gap-2 cursor-pointer select-none text-slate-400 hover:text-slate-300">
|
|
<input
|
|
type="checkbox"
|
|
checked={rememberMe}
|
|
onChange={(e) => setRememberMe(e.target.checked)}
|
|
disabled={isPending}
|
|
className="w-4 h-4 rounded border-slate-700 bg-slate-800 text-indigo-600 focus:ring-indigo-500/30 focus:ring-offset-0"
|
|
/>
|
|
<span>Mémoriser ma session (30 jours)</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Submit Button */}
|
|
<button
|
|
type="submit"
|
|
disabled={isPending}
|
|
className="w-full mt-2 py-3.5 px-4 bg-gradient-to-r from-indigo-600 to-indigo-500 hover:from-indigo-500 hover:to-indigo-400 text-white font-medium rounded-xl shadow-lg shadow-indigo-600/30 hover:shadow-indigo-600/40 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 transition-all flex items-center justify-center gap-2 disabled:opacity-60 disabled:cursor-not-allowed cursor-pointer"
|
|
>
|
|
{isPending ? (
|
|
<>
|
|
<Loader2 className="w-5 h-5 animate-spin" />
|
|
<span>Vérification en cours...</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<span>Connexion à l'espace Admin</span>
|
|
<ArrowRight className="w-4 h-4 ml-1" />
|
|
</>
|
|
)}
|
|
</button>
|
|
</form>
|
|
|
|
{/* Security footnote */}
|
|
<div className="mt-8 pt-6 border-t border-slate-800 text-center">
|
|
<div className="flex items-center justify-center gap-1.5 text-xs text-slate-500">
|
|
<Lock className="w-3.5 h-3.5 text-emerald-400" />
|
|
<span>Chiffrement SHA-256 de session • Accès strictement restreint</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Brand copyright */}
|
|
<p className="text-center text-xs text-slate-600 mt-6">
|
|
© {new Date().getFullYear()} Afrohub. Tous droits réservés.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function LoginPage() {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<div className="min-h-screen flex items-center justify-center bg-[#0b0f19] text-white">
|
|
<Loader2 className="w-8 h-8 animate-spin text-indigo-500" />
|
|
</div>
|
|
}
|
|
>
|
|
<LoginForm />
|
|
</Suspense>
|
|
);
|
|
}
|