Files
demat-facturation/client/src/pages/Login.tsx

185 lines
6.4 KiB
TypeScript

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Loader2 } from "lucide-react";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
const ITINOVA_LOGO = "https://d2xsxph8kpxj0f.cloudfront.net/310519663070627318/fo4DRyBgjsuiigFAgNLuWm/itinova-logo_2a2ba00a.jpg";
const SANTINOVA_LOGO = "https://d2xsxph8kpxj0f.cloudfront.net/310519663070627318/fo4DRyBgjsuiigFAgNLuWm/santinova-logo_5ae0d248.webp";
// Logo Microsoft SVG officiel
function MicrosoftLogo() {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21" width="18" height="18">
<rect x="1" y="1" width="9" height="9" fill="#f25022"/>
<rect x="11" y="1" width="9" height="9" fill="#7fba00"/>
<rect x="1" y="11" width="9" height="9" fill="#00a4ef"/>
<rect x="11" y="11" width="9" height="9" fill="#ffb900"/>
</svg>
);
}
export default function Login() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [azureLoading, setAzureLoading] = useState(false);
// Afficher les erreurs transmises via query param (ex: depuis le callback Azure)
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const error = params.get("error");
if (error) {
toast.error(decodeURIComponent(error));
// Nettoyer l'URL
window.history.replaceState({}, "", "/login");
}
}, []);
const loginMutation = trpc.auth.loginLocal.useMutation({
onSuccess: () => {
toast.success("Connexion réussie");
window.location.href = "/";
},
onError: (error) => {
toast.error(error.message || "Erreur de connexion");
},
});
const azureLoginQuery = trpc.auth.getAzureLoginUrl.useQuery(undefined, {
enabled: false,
retry: false,
});
const azureAvailableQuery = trpc.auth.isAzureAdAvailable.useQuery();
const handleLocalLogin = (e: React.FormEvent) => {
e.preventDefault();
loginMutation.mutate({ email: username, password });
};
const handleMicrosoftLogin = async () => {
setAzureLoading(true);
try {
const result = await azureLoginQuery.refetch();
if (result.data?.url) {
window.location.href = result.data.url;
} else {
toast.error("Impossible d'obtenir l'URL de connexion Microsoft");
setAzureLoading(false);
}
} catch {
toast.error("Erreur lors de la connexion Microsoft");
setAzureLoading(false);
}
};
const azureAvailable = azureAvailableQuery.data?.available ?? false;
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-blue-50 p-4">
<div className="w-full max-w-sm flex flex-col items-center gap-6">
{/* Logo Itinova en haut */}
<div className="w-full flex justify-center">
<img
src={ITINOVA_LOGO}
alt="Itinova"
className="h-24 object-contain"
/>
</div>
{/* Carte de connexion */}
<Card className="w-full shadow-lg border-0">
<CardContent className="pt-6 pb-6">
<div className="mb-6 text-center">
<h1 className="text-2xl font-bold text-slate-800">Gestion facturation</h1>
<p className="text-sm text-slate-500 mt-1">Connectez-vous pour accéder à l'application</p>
</div>
{/* Bouton Microsoft 365 */}
{azureAvailable && (
<>
<Button
type="button"
variant="outline"
className="w-full flex items-center gap-3 border-slate-300 bg-white hover:bg-slate-50 text-slate-700 font-medium mb-4"
onClick={handleMicrosoftLogin}
disabled={azureLoading}
>
{azureLoading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<MicrosoftLogo />
)}
Se connecter avec Microsoft 365
</Button>
<div className="relative mb-4">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-slate-200" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-slate-400">ou</span>
</div>
</div>
</>
)}
<form onSubmit={handleLocalLogin} 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)}
required
autoFocus
autoComplete="username"
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Mot de passe</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
autoComplete="current-password"
/>
</div>
<Button
type="submit"
className="w-full bg-[#1e3a8a] hover:bg-[#1e40af] text-white mt-2"
disabled={loginMutation.isPending}
>
{loginMutation.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
Se connecter
</Button>
</form>
</CardContent>
</Card>
{/* Logo Santinova en bas avec "powered by" */}
<div className="flex flex-col items-center gap-1">
<span className="text-xs text-slate-400 lowercase tracking-wide">powered by</span>
<img
src={SANTINOVA_LOGO}
alt="Santinova"
className="h-24 object-contain"
/>
</div>
</div>
</div>
);
}