Checkpoint: Ajout d'une page de connexion locale permettant aux utilisateurs de se connecter avec leur identifiant et mot de passe (username/password) au lieu de l'OAuth Manus. Implémentation complète avec route d'authentification backend, hashage bcrypt, session JWT, et reconnaissance automatique des sessions locales vs OAuth. Tests unitaires et manuels réussis avec l'utilisateur adminServFormation.

This commit is contained in:
Manus Sandbox
2025-11-23 04:59:54 -05:00
parent b2e7505afb
commit fe8b8e90c4
11 changed files with 488 additions and 7 deletions

View File

@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
import { GraduationCap, Calendar, Users, CheckCircle, Mail, Download } from "lucide-react";
import { useLocation } from "wouter";
import { useLocation, Link } from "wouter";
import { useEffect } from "react";
export default function Home() {
@@ -71,9 +71,16 @@ export default function Home() {
<h1 className="text-xl font-bold text-gray-900">{APP_TITLE}</h1>
</div>
{!isAuthenticated && (
<Button onClick={() => window.location.href = getLoginUrl()}>
Connexion Administrateur
</Button>
<div className="flex gap-2">
<Link href="/login">
<Button variant="outline">
Connexion locale
</Button>
</Link>
<Button onClick={() => window.location.href = getLoginUrl()}>
Connexion OAuth
</Button>
</div>
)}
</div>
</header>

115
client/src/pages/Login.tsx Normal file
View File

@@ -0,0 +1,115 @@
import { useState } from "react";
import { useLocation } from "wouter";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { toast } from "sonner";
import { APP_LOGO, APP_TITLE } from "@/const";
import { Loader2 } from "lucide-react";
export default function Login() {
const [, setLocation] = useLocation();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!username || !password) {
toast.error("Veuillez remplir tous les champs");
return;
}
setLoading(true);
try {
const response = await fetch("/api/auth/local/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
const data = await response.json();
if (response.ok && data.success) {
toast.success("Connexion réussie !");
// Rediriger vers le tableau de bord
setLocation("/admin");
} else {
toast.error(data.message || "Identifiant ou mot de passe incorrect");
}
} catch (error) {
console.error("Erreur lors de la connexion:", error);
toast.error("Erreur lors de la connexion");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-blue-50 to-indigo-100 p-4">
<Card className="w-full max-w-md">
<CardHeader className="space-y-4">
<div className="flex justify-center">
<img src={APP_LOGO} alt="Logo" className="h-16 w-auto" />
</div>
<div className="text-center">
<CardTitle className="text-2xl font-bold">{APP_TITLE}</CardTitle>
<CardDescription className="mt-2">
Connectez-vous avec votre identifiant et mot de passe
</CardDescription>
</div>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Identifiant</Label>
<Input
id="username"
type="text"
placeholder="Votre identifiant"
value={username}
onChange={(e) => setUsername(e.target.value)}
disabled={loading}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<Input
id="password"
type="password"
placeholder="Votre mot de passe"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
required
/>
</div>
<Button type="submit" className="w-full" disabled={loading}>
{loading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Connexion en cours...
</>
) : (
"Se connecter"
)}
</Button>
</form>
<div className="mt-6 text-center text-sm text-gray-600">
<p>Vous n'avez pas de compte ?</p>
<p className="mt-1">Contactez l'administrateur</p>
</div>
</CardContent>
</Card>
</div>
);
}