import NextAuth from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; import jwt_decode from 'jsonwebtoken'; import logger from '@/utils/logger'; const options = { secret: process.env.AUTH_SECRET, providers: [ CredentialsProvider({ name: 'Credentials', credentials: { email: { label: 'Email', type: 'email' }, password: { label: 'Password', type: 'password' }, }, authorize: async (credentials) => { // URL calculée ici (pas au niveau module) pour garantir que NEXT_PUBLIC_API_URL est chargé const loginUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/login`; try { const res = await fetch(loginUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', // Connection: close évite le SocketError undici lié au keep-alive vers Daphne Connection: 'close', }, body: JSON.stringify({ email: credentials.email, password: credentials.password, }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body?.errorMessage || 'Identifiants invalides'); } const user = await res.json(); return user || null; } catch (error) { logger.error('Authorize error:', error.message); throw new Error(error.message || 'Invalid credentials'); } }, }), ], session: { strategy: 'jwt', maxAge: 60 * 60, // 1 Hour // 0 = réécrire le cookie à chaque fois que le token change (indispensable avec // un access token Django de 15 min, sinon le cookie expiré reste en place) updateAge: 0, }, cookies: { sessionToken: { name: 'n3wtschool_session_token', options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NODE_ENV === 'production', }, }, }, callbacks: { async jwt({ token, user }) { // Si c'est la première connexion if (user && user?.token) { return { ...token, token: user.token, refresh: user.refresh, tokenExpires: jwt_decode.decode(user.token).exp * 1000, }; } // Vérifier si le token n'est pas expiré if (Date.now() < token.tokenExpires) { return token; } // Token Django expiré (lifetime = 15 min), essayer de le rafraîchir logger.info('JWT: access token expiré, tentative de refresh'); if (!token.refresh) { logger.error('JWT: refresh token absent dans la session'); return { ...token, error: 'RefreshTokenError' }; } const refreshUrl = `${process.env.NEXT_PUBLIC_API_URL}/Auth/refreshJWT`; if (!process.env.NEXT_PUBLIC_API_URL) { logger.error('JWT: NEXT_PUBLIC_API_URL non défini, refresh impossible'); return { ...token, error: 'RefreshTokenError' }; } try { const res = await fetch(refreshUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', // Connection: close évite le SocketError undici lié au keep-alive vers Daphne Connection: 'close', }, body: JSON.stringify({ refresh: token.refresh }), }); if (!res.ok) { const body = await res.json().catch(() => ({})); logger.error('JWT: refresh échoué', { status: res.status, body }); throw new Error(`Refresh HTTP ${res.status}`); } const response = await res.json(); if (!response?.token) { logger.error('JWT: réponse refresh sans token', { response }); throw new Error('Réponse refresh invalide'); } logger.info('JWT: refresh réussi'); return { ...token, token: response.token, refresh: response.refresh, tokenExpires: jwt_decode.decode(response.token).exp * 1000, error: undefined, }; } catch (error) { logger.error('JWT: refresh token failed', { message: error.message }); return { ...token, error: 'RefreshTokenError' }; } }, async session({ session, token }) { if (token?.error === 'RefreshTokenError') { session.error = 'RefreshTokenError'; return session; } if (token && token?.token) { const { user_id, email, roles, roleIndexLoginDefault } = jwt_decode.decode(token.token); session.user = { ...session.user, token: token.token, refresh: token.refresh, user_id: user_id, email: email, roles: roles, roleIndexLoginDefault: roleIndexLoginDefault, }; } return session; }, }, pages: { signIn: '/[locale]/users/login', }, csrf: true, }; export default (req, res) => NextAuth(req, res, options);