diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts index eb32e495..80fddd0e 100644 --- a/app/api/auth/[...nextauth]/route.ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,26 +1,11 @@ import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; import { prisma } from '@/lib/prisma'; +import { ExtendedJWT, ExtendedSession, ServiceToken, invalidateServiceTokens } from '@/lib/session'; +import { Session } from "next-auth"; declare module "next-auth" { - interface Session { - user: { - id: string; - name?: string | null; - email?: string | null; - image?: string | null; - username: string; - first_name: string; - last_name: string; - role: string[]; - }; - accessToken: string; - refreshToken?: string; - rocketChatToken?: string | null; - rocketChatUserId?: string | null; - error?: string; - } - + interface Session extends ExtendedSession {} interface JWT { accessToken?: string; refreshToken?: string; @@ -29,8 +14,15 @@ declare module "next-auth" { username?: string; first_name?: string; last_name?: string; - name?: string; - email?: string; + name?: string | null; + email?: string | null; + serviceTokens: { + rocketChat?: ServiceToken; + leantime?: ServiceToken; + calendar?: ServiceToken; + mail?: ServiceToken; + [key: string]: ServiceToken | undefined; + }; } } @@ -45,9 +37,9 @@ function getRequiredEnvVar(name: string): string { export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ - clientId: getRequiredEnvVar("KEYCLOAK_CLIENT_ID"), - clientSecret: getRequiredEnvVar("KEYCLOAK_CLIENT_SECRET"), - issuer: getRequiredEnvVar("KEYCLOAK_ISSUER"), + clientId: process.env.KEYCLOAK_CLIENT_ID!, + clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, + issuer: process.env.KEYCLOAK_ISSUER!, profile(profile) { return { id: profile.sub, @@ -111,20 +103,8 @@ export const authOptions: NextAuthOptions = { } try { - console.log('Attempting to create/update user:', { - id: user.id, - email: user.email - }); - - // First check if user exists - const existingUser = await prisma.user.findUnique({ - where: { id: user.id } - }); - - console.log('Existing user check:', existingUser); - // Create or update user in local database - const result = await prisma.user.upsert({ + await prisma.user.upsert({ where: { id: user.id }, update: { email: user.email, @@ -137,7 +117,6 @@ export const authOptions: NextAuthOptions = { }, }); - console.log('User upsert result:', result); return true; } catch (error) { console.error('Error in signIn callback:', error); @@ -145,40 +124,62 @@ export const authOptions: NextAuthOptions = { } }, async session({ session, token }) { - if (session?.user && token.sub) { - session.user.id = token.sub; - session.user.name = token.name ?? null; - session.user.email = token.email ?? null; - session.user.username = token.username ?? ''; - session.user.first_name = token.first_name ?? ''; - session.user.last_name = token.last_name ?? ''; - session.user.role = token.role ?? []; - session.accessToken = token.accessToken ?? ''; - session.refreshToken = token.refreshToken ?? ''; - session.rocketChatToken = token.rocketChatToken ?? null; - session.rocketChatUserId = token.rocketChatUserId ?? null; - if (token.error) { - session.error = token.error; - } + const extendedSession = session as ExtendedSession; + const extendedToken = token as ExtendedJWT; + + if (extendedSession?.user && extendedToken.sub) { + extendedSession.user.id = extendedToken.sub; + extendedSession.user.username = extendedToken.username ?? ''; + extendedSession.user.first_name = extendedToken.first_name ?? ''; + extendedSession.user.last_name = extendedToken.last_name ?? ''; + extendedSession.user.role = extendedToken.role ?? []; + extendedSession.accessToken = extendedToken.accessToken ?? ''; + extendedSession.refreshToken = extendedToken.refreshToken; + extendedSession.serviceTokens = extendedToken.serviceTokens ?? {}; } - return session; + + return extendedSession; }, async jwt({ token, user, account }) { + const extendedToken = token as ExtendedJWT; + if (user) { - token.sub = user.id; - token.name = user.name; - token.email = user.email; - token.username = user.username; - token.first_name = user.first_name; - token.last_name = user.last_name; - token.role = user.role; + extendedToken.role = user.role; + extendedToken.username = user.username; + extendedToken.first_name = user.first_name; + extendedToken.last_name = user.last_name; } + if (account) { - token.accessToken = account.access_token; - token.refreshToken = account.refresh_token; - token.accessTokenExpires = account.expires_at ? account.expires_at * 1000 : 0; + extendedToken.accessToken = account.access_token; + extendedToken.refreshToken = account.refresh_token; + extendedToken.accessTokenExpires = account.expires_at; + extendedToken.serviceTokens = {}; + } + + return extendedToken; + } + }, + events: { + async signOut({ token }) { + const extendedToken = token as ExtendedJWT; + if (extendedToken.sub) { + await invalidateServiceTokens({ + user: { + id: extendedToken.sub, + name: extendedToken.name ?? null, + email: extendedToken.email ?? null, + username: extendedToken.username ?? '', + first_name: extendedToken.first_name ?? '', + last_name: extendedToken.last_name ?? '', + role: extendedToken.role ?? [], + }, + accessToken: extendedToken.accessToken ?? '', + refreshToken: extendedToken.refreshToken, + serviceTokens: extendedToken.serviceTokens ?? {}, + expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), + } as ExtendedSession); } - return token; } }, pages: { diff --git a/components/main-nav.tsx b/components/main-nav.tsx index 0d8044e2..261cd733 100644 --- a/components/main-nav.tsx +++ b/components/main-nav.tsx @@ -349,10 +349,37 @@ export function MainNav() { ))} signOut({ - callbackUrl: '/signin', - redirect: true - })} + onClick={async () => { + try { + // First sign out from NextAuth + await signOut({ + callbackUrl: '/signin', + redirect: false + }); + + // Then redirect to Keycloak logout with proper parameters + const keycloakLogoutUrl = new URL( + `${process.env.NEXT_PUBLIC_KEYCLOAK_ISSUER}/protocol/openid-connect/logout` + ); + + // Add required parameters + keycloakLogoutUrl.searchParams.append( + 'post_logout_redirect_uri', + window.location.origin + ); + keycloakLogoutUrl.searchParams.append( + 'id_token_hint', + session?.accessToken || '' + ); + + // Redirect to Keycloak logout + window.location.href = keycloakLogoutUrl.toString(); + } catch (error) { + console.error('Error during logout:', error); + // Fallback to simple redirect if something goes wrong + window.location.href = '/signin'; + } + }} > Déconnexion diff --git a/lib/session.ts b/lib/session.ts new file mode 100644 index 00000000..5737b2c5 --- /dev/null +++ b/lib/session.ts @@ -0,0 +1,99 @@ +import { Session } from "next-auth"; +import { JWT } from "next-auth/jwt"; +import { DefaultSession } from "next-auth"; + +export interface ServiceToken { + token: string; + userId: string; + expiresAt: number; +} + +export interface ExtendedSession extends DefaultSession { + user: { + id: string; + name?: string | null; + email?: string | null; + image?: string | null; + username: string; + first_name: string; + last_name: string; + role: string[]; + }; + accessToken: string; + refreshToken?: string; + serviceTokens: { + rocketChat?: ServiceToken; + leantime?: ServiceToken; + calendar?: ServiceToken; + mail?: ServiceToken; + [key: string]: ServiceToken | undefined; + }; + expires: string; +} + +export interface ExtendedJWT extends JWT { + accessToken?: string; + refreshToken?: string; + accessTokenExpires?: number; + role?: string[]; + username?: string; + first_name?: string; + last_name?: string; + name?: string | null; + email?: string | null; + serviceTokens: { + rocketChat?: ServiceToken; + leantime?: ServiceToken; + calendar?: ServiceToken; + mail?: ServiceToken; + [key: string]: ServiceToken | undefined; + }; +} + +export async function invalidateServiceTokens(session: ExtendedSession) { + const serviceEndpoints = { + rocketChat: `${process.env.NEXT_PUBLIC_IFRAME_PAROLE_URL?.split('/channel')[0]}/api/v1/logout`, + leantime: `${process.env.LEANTIME_API_URL}/api/jsonrpc`, + // Add other service endpoints as needed + }; + + const invalidatePromises = Object.entries(session.serviceTokens).map(async ([service, token]) => { + if (!token) return; + + try { + const endpoint = serviceEndpoints[service as keyof typeof serviceEndpoints]; + if (!endpoint) return; + + await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...(service === 'rocketChat' ? { + 'X-Auth-Token': token.token, + 'X-User-Id': token.userId, + } : {}), + ...(service === 'leantime' ? { + 'X-API-Key': process.env.LEANTIME_TOKEN!, + } : {}), + }, + body: service === 'leantime' ? JSON.stringify({ + jsonrpc: '2.0', + method: 'leantime.rpc.auth.logout', + id: 1 + }) : undefined, + }); + } catch (error) { + console.error(`Error invalidating ${service} token:`, error); + } + }); + + await Promise.all(invalidatePromises); +} + +export function clearAllCookies() { + const cookies = document.cookie.split(';'); + for (const cookie of cookies) { + const [name] = cookie.split('='); + document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; + } +} \ No newline at end of file