Checkpoint: Ajout de la connexion OAuth2 Microsoft 365 (Azure AD) : route callback /api/auth/azure/callback, bouton Microsoft sur la page de login, variables d'environnement Azure AD configurées

This commit is contained in:
Manus
2026-07-07 05:54:46 -04:00
parent d4c9bf635a
commit 8ee10602d3
2 changed files with 161 additions and 2 deletions

View File

@@ -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 (
<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: () => {
@@ -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 (
<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">
@@ -50,6 +99,35 @@ export default function Login() {
<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>

View File

@@ -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<boolean> {
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",