Checkpoint: Connexion Microsoft 365 (Azure AD OAuth2) : migration DB azureAdId, helpers backend, route callback, bouton Login, page AzureCallback, tests OK

This commit is contained in:
Manus
2026-07-07 10:29:03 -04:00
parent 2d0de2a30d
commit 465e33c3b8
16 changed files with 1529 additions and 5 deletions

View File

@@ -11,8 +11,9 @@ import { serveStatic, setupVite } from "./vite";
import { runFullImport } from "../importer";
import uploadRoutes from "../uploadRoutes";
import scheduledRoutes from "../scheduledRoutes";
import { ensureAdminExists } from "../localAuth";
import { getSetting, purgeOldArticles } from "../db";
import { ensureAdminExists, generateLocalToken } from "../localAuth";
import { isAzureAdConfigured, getAzureAuthUrl, handleAzureCallback } from "../azureAuth";
import { getLocalUserByAzureAdId, getLocalUserByEmail, upsertLocalUserAzure, getSetting, purgeOldArticles } from "../db";
import { runRssFetch } from "../rssEngine";
function isPortAvailable(port: number): Promise<boolean> {
@@ -126,6 +127,87 @@ async function startServer() {
registerOAuthRoutes(app);
app.use(uploadRoutes);
app.use(scheduledRoutes);
// ─── 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) {
res.redirect(`/login?error=${encodeURIComponent("Connexion Microsoft refus\u00e9e")}`);
return;
}
if (!code) {
res.redirect("/login?error=" + encodeURIComponent("Code OAuth manquant"));
return;
}
if (!isAzureAdConfigured()) {
res.redirect("/login?error=" + encodeURIComponent("Azure AD non configur\u00e9"));
return;
}
try {
const azureUser = await handleAzureCallback(code);
// Chercher par azureAdId puis par email
let user = await getLocalUserByAzureAdId(azureUser.azureAdId);
if (!user) user = await getLocalUserByEmail(azureUser.email);
if (!user) {
// Cr\u00e9er automatiquement avec r\u00f4le "user"
await upsertLocalUserAzure({
email: azureUser.email,
name: azureUser.name,
azureAdId: azureUser.azureAdId,
role: "user",
});
user = await getLocalUserByEmail(azureUser.email);
} else if (!user.azureAdId) {
// Lier le compte existant \u00e0 Azure AD
await upsertLocalUserAzure({
email: user.email ?? azureUser.email,
azureAdId: azureUser.azureAdId,
});
}
if (!user || !user.isActive) {
res.redirect("/login?error=" + encodeURIComponent("Compte inactif ou introuvable"));
return;
}
// Mettre \u00e0 jour lastSignedIn
const db = await (await import("../db")).getDb();
if (db) {
const { localUsers } = await import("../../drizzle/schema");
const { eq } = await import("drizzle-orm");
await db.update(localUsers).set({ lastSignedIn: new Date() }).where(eq(localUsers.id, user.id));
}
// G\u00e9n\u00e9rer le token JWT local et le stocker dans le cookie
const token = await generateLocalToken(user.id, user.role);
res.cookie("veille_local_auth", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
path: "/",
maxAge: 7 * 24 * 60 * 60 * 1000,
});
// Retourner les infos user en JSON pour que le frontend hydrate LocalAuthContext
const userPayload = JSON.stringify({
id: user.id,
name: user.name,
username: user.username ?? null,
email: user.email ?? null,
role: user.role,
});
// Rediriger vers une page de callback qui hydrate le contexte
res.redirect(`/azure-callback?user=${encodeURIComponent(userPayload)}`);
} catch (err: any) {
console.error("[Azure AD] Erreur callback:", err.message);
res.redirect("/login?error=" + encodeURIComponent("Erreur d'authentification Microsoft"));
}
});
app.use(
"/api/trpc",
createExpressMiddleware({ router: appRouter, createContext })