import { Router, Request, Response } from "express"; import { AppDataSource } from "../config/data-source"; import { User } from "../entities/User"; import { requireAuth, AuthenticatedRequest } from "../middleware/auth.middleware"; const router = Router(); const COOKIE_NAME = "od_token"; const REFRESH_COOKIE_NAME = "od_refresh"; const COOKIE_OPTIONS = { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict" as const, maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours }; const REFRESH_COOKIE_OPTIONS = { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict" as const, maxAge: 30 * 24 * 60 * 60 * 1000, // 30 jours }; /** Upsert user en DB depuis un profil SuperOAuth */ async function upsertUser(oauthUser: { id: string; email: string | null; nickname: string }): Promise { const userRepo = AppDataSource.getRepository(User); let dbUser = await userRepo.findOne({ where: { superOAuthId: oauthUser.id } }); if (!dbUser) { dbUser = userRepo.create({ superOAuthId: oauthUser.id, email: oauthUser.email, nickname: oauthUser.nickname }); } else { dbUser.email = oauthUser.email; dbUser.nickname = oauthUser.nickname; } await userRepo.save(dbUser); } /** * POST /api/auth/login * Proxy email/password vers SuperOAuth → pose le cookie httpOnly. */ router.post("/login", async (req: Request, res: Response): Promise => { const { email, password } = req.body as { email?: string; password?: string }; if (!email || !password) { res.status(400).json({ success: false, error: "MISSING_CREDENTIALS" }); return; } const superOAuthUrl = process.env.SUPER_OAUTH_URL; if (!superOAuthUrl) { res.status(500).json({ success: false, error: "INTERNAL_ERROR" }); return; } try { const response = await fetch(`${superOAuthUrl}/api/v1/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); const data = await response.json() as { success: boolean; data?: { user: { id: string; email: string | null; nickname: string }; tokens: { accessToken: string; refreshToken?: string } }; message?: string; }; if (!response.ok || !data.data?.tokens?.accessToken) { res.status(401).json({ success: false, error: "INVALID_CREDENTIALS" }); return; } await upsertUser(data.data.user); res.cookie(COOKIE_NAME, data.data.tokens.accessToken, COOKIE_OPTIONS); if (data.data.tokens.refreshToken) { res.cookie(REFRESH_COOKIE_NAME, data.data.tokens.refreshToken, REFRESH_COOKIE_OPTIONS); } res.json({ success: true, data: { user: data.data.user } }); } catch { res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE" }); } }); /** * POST /api/auth/session * Reçoit le token depuis le callback SuperOAuth, * le valide, puis le pose en httpOnly cookie. */ router.post("/session", async (req: Request, res: Response): Promise => { const { token } = req.body as { token?: string }; if (!token) { res.status(400).json({ success: false, error: "MISSING_TOKEN" }); return; } const superOAuthUrl = process.env.SUPER_OAUTH_URL; if (!superOAuthUrl) { res.status(500).json({ success: false, error: "INTERNAL_ERROR" }); return; } try { const response = await fetch(`${superOAuthUrl}/api/v1/auth/token/validate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); const data = await response.json() as { success: boolean; data?: { valid: boolean; user?: object }; error?: string; }; if (!response.ok || !data.data?.valid || !data.data.user) { res.status(401).json({ success: false, error: "INVALID_TOKEN" }); return; } await upsertUser(data.data.user as { id: string; email: string | null; nickname: string }); res.cookie(COOKIE_NAME, token, COOKIE_OPTIONS); res.json({ success: true, data: { user: data.data.user } }); } catch { res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE" }); } }); /** * POST /api/auth/refresh * Échange le refresh token contre un nouvel access token via SuperOAuth. */ router.post("/refresh", async (req: Request, res: Response): Promise => { const refreshToken = (req.cookies as Record)?.[REFRESH_COOKIE_NAME]; if (!refreshToken) { res.status(401).json({ success: false, error: "NO_REFRESH_TOKEN" }); return; } const superOAuthUrl = process.env.SUPER_OAUTH_URL; if (!superOAuthUrl) { res.status(500).json({ success: false, error: "INTERNAL_ERROR" }); return; } try { const response = await fetch(`${superOAuthUrl}/api/v1/auth/token/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), }); const data = await response.json() as { success: boolean; data?: { tokens: { accessToken: string; refreshToken?: string }; user?: { id: string; email: string | null; nickname: string } }; error?: string; }; if (!response.ok || !data.data?.tokens?.accessToken) { res.clearCookie(COOKIE_NAME); res.clearCookie(REFRESH_COOKIE_NAME); res.status(401).json({ success: false, error: "REFRESH_FAILED" }); return; } res.cookie(COOKIE_NAME, data.data.tokens.accessToken, COOKIE_OPTIONS); if (data.data.tokens.refreshToken) { res.cookie(REFRESH_COOKIE_NAME, data.data.tokens.refreshToken, REFRESH_COOKIE_OPTIONS); } res.json({ success: true, data: { user: data.data.user ?? null } }); } catch { res.status(500).json({ success: false, error: "AUTH_SERVICE_UNAVAILABLE" }); } }); /** * POST /api/auth/logout * Supprime le cookie de session. */ router.post("/logout", (_req: Request, res: Response): void => { res.clearCookie(COOKIE_NAME); res.clearCookie(REFRESH_COOKIE_NAME); res.json({ success: true }); }); /** * GET /api/auth/me * Retourne l'utilisateur courant (cookie ou Bearer). */ router.get("/me", requireAuth, (req: Request, res: Response): void => { const { user } = req as AuthenticatedRequest; res.json({ success: true, data: { user } }); }); /** * GET /api/auth/me/optional * Retourne l'utilisateur courant ou null si non authentifié (pas de 401). */ router.get("/me/optional", async (req: Request, res: Response): Promise => { const token = req.headers.authorization?.split(" ")[1] ?? (req.cookies as Record)?.od_token; if (!token) { res.json({ success: true, data: { user: null } }); return; } const superOAuthUrl = process.env.SUPER_OAUTH_URL; if (!superOAuthUrl) { res.json({ success: true, data: { user: null } }); return; } try { const response = await fetch(`${superOAuthUrl}/api/v1/auth/token/validate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token }), }); const data = await response.json() as { success: boolean; data?: { valid: boolean; user?: object }; }; if (!response.ok || !data.data?.valid || !data.data.user) { res.json({ success: true, data: { user: null } }); return; } res.json({ success: true, data: { user: data.data.user } }); } catch { res.json({ success: true, data: { user: null } }); } }); export default router;