diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 8e5f516..96ac5c2 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -10,9 +10,33 @@ 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 ( + + + + + + + ); +} + 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: () => { @@ -24,11 +48,36 @@ export default function Login() { }, }); + 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 (
@@ -50,6 +99,35 @@ export default function Login() {

Connectez-vous pour accéder à l'application

+ {/* Bouton Microsoft 365 */} + {azureAvailable && ( + <> + + +
+
+ +
+
+ ou +
+
+ + )} +
diff --git a/server/_core/index.ts b/server/_core/index.ts index d942e3b..f287c58 100644 --- a/server/_core/index.ts +++ b/server/_core/index.ts @@ -10,10 +10,11 @@ import { registerOAuthRoutes } from "./oauth"; import { appRouter } from "../routers"; import { createContext } from "./context"; import { serveStatic, setupVite } from "./vite"; -import { getAllUsers } from "../db"; +import { getAllUsers, getUserByAzureAdId, getUserByEmail, upsertUser } from "../db"; import { startEmailImportService } from "../emailImportService"; import { startFolderImportService } from "../folderImportService"; import { getImportSettingsByUser } from "../db"; +import { handleAzureCallback, isAzureAdConfigured, generateToken } from "../auth"; function isPortAvailable(port: number): Promise { return new Promise(resolve => { @@ -143,6 +144,86 @@ async function startServer() { archive.finalize(); }); + // ============= AZURE AD OAUTH2 CALLBACK ============= + app.get("/api/auth/azure/callback", async (req, res) => { + const code = req.query.code as string | undefined; + const error = req.query.error as string | undefined; + + if (error) { + console.error("[Azure AD] Erreur OAuth:", error, req.query.error_description); + res.redirect(`/login?error=${encodeURIComponent("Connexion Microsoft refusée")}`); + return; + } + + if (!code) { + res.redirect("/login?error=" + encodeURIComponent("Code OAuth manquant")); + return; + } + + if (!isAzureAdConfigured()) { + res.redirect("/login?error=" + encodeURIComponent("Azure AD non configuré")); + return; + } + + try { + const azureUser = await handleAzureCallback(code); + + // Chercher l'utilisateur par azureAdId ou par email + let user = await getUserByAzureAdId(azureUser.azureAdId); + if (!user) { + user = await getUserByEmail(azureUser.email); + } + + if (!user) { + // Créer l'utilisateur automatiquement + await upsertUser({ + email: azureUser.email, + name: azureUser.name, + azureAdId: azureUser.azureAdId, + loginMethod: "azure-ad", + isActive: 1, + role: "user", + }); + user = await getUserByEmail(azureUser.email); + } else { + // Mettre à jour l'azureAdId si manquant + if (!user.azureAdId) { + await upsertUser({ + email: user.email, + azureAdId: azureUser.azureAdId, + loginMethod: user.loginMethod, + }); + } + } + + if (!user) { + res.redirect("/login?error=" + encodeURIComponent("Impossible de créer le compte")); + return; + } + + if (user.isActive === 0) { + res.redirect("/login?error=" + encodeURIComponent("Compte inactif")); + return; + } + + // Générer le token JWT et poser le cookie + const token = generateToken(user); + res.cookie("auth_token", token, { + httpOnly: true, + secure: false, + sameSite: "lax", + path: "/", + maxAge: 7 * 24 * 60 * 60 * 1000, + }); + + console.log(`[Azure AD] Connexion réussie pour ${user.email}`); + res.redirect("/"); + } catch (err: any) { + console.error("[Azure AD] Erreur callback:", err.message); + res.redirect("/login?error=" + encodeURIComponent("Erreur d'authentification Microsoft")); + } + }); + // tRPC API app.use( "/api/trpc",