diff --git a/backend/src/routes/auth.routes.ts b/backend/src/routes/auth.routes.ts index 3a2d31d..402fe6f 100644 --- a/backend/src/routes/auth.routes.ts +++ b/backend/src/routes/auth.routes.ts @@ -10,9 +10,67 @@ const COOKIE_OPTIONS = { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict" as const, - maxAge: 15 * 60 * 1000, // 15 min — durée de vie du token SuperOAuth + maxAge: 15 * 60 * 1000, }; +/** 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 } }; + 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); + 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, @@ -50,21 +108,7 @@ router.post("/session", async (req: Request, res: Response): Promise => { return; } - // Upsert user en DB — crée si premier login, met à jour email/nickname sinon - const oauthUser = data.data.user as { id: string; email: string | null; nickname: string }; - 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); + 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 } }); diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 43e3ba5..6dd2396 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -1,4 +1,6 @@ -import { Link } from 'react-router-dom'; +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { apiFetch } from '../lib/api'; const PROVIDERS = [ { id: 'discord', label: 'Discord' }, @@ -8,27 +10,90 @@ const PROVIDERS = [ ] as const; export default function LoginPage() { + const navigate = useNavigate(); const base = import.meta.env.VITE_SUPEROAUTH_URL; const redirectUrl = encodeURIComponent(window.location.origin + '/callback'); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + if (!email || !password || loading) return; + setLoading(true); + setError(null); + try { + await apiFetch('/auth/login', { + method: 'POST', + body: JSON.stringify({ email, password }), + }); + navigate('/', { replace: true }); + } catch { + setError('Email ou mot de passe incorrect.'); + } finally { + setLoading(false); + } + } + return ( -
+