import NextAuth, { NextAuthOptions } from "next-auth"; import KeycloakProvider from "next-auth/providers/keycloak"; interface RocketChatLoginResponse { status: string; data: { authToken: string; userId: string; }; } 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; rocketChatToken: string; rocketChatUserId: string; } interface JWT { accessToken: string; refreshToken: string; accessTokenExpires: number; role: string[]; username: string; first_name: string; last_name: string; rocketChatToken: string; rocketChatUserId: string; } } declare module "next-auth/jwt" { interface JWT { accessToken: string; refreshToken: string; accessTokenExpires: number; role: string[]; username: string; first_name: string; last_name: string; rocketChatToken: string; rocketChatUserId: string; } } export const authOptions: NextAuthOptions = { providers: [ KeycloakProvider({ clientId: process.env.KEYCLOAK_CLIENT_ID!, clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!, issuer: process.env.KEYCLOAK_ISSUER!, profile(profile) { return { id: profile.sub, name: profile.name ?? profile.preferred_username, email: profile.email, first_name: profile.given_name ?? '', last_name: profile.family_name ?? '', username: profile.preferred_username ?? profile.email?.split('@')[0] ?? '', role: profile.groups || [], } }, }), ], callbacks: { async jwt({ token, account, profile }) { console.log('JWT callback called with:', { token, account, profile }); // Initial sign in if (account && profile) { // Set user data from profile token.username = profile.preferred_username || ''; token.first_name = profile.given_name || ''; token.last_name = profile.family_name || ''; token.role = profile.groups || []; token.accessToken = account.access_token; token.refreshToken = account.refresh_token; token.accessTokenExpires = account.expires_at ? account.expires_at * 1000 : Date.now() + 60 * 60 * 1000; // Create or update Leantime user try { const leantimeResponse = await fetch(`${process.env.LEANTIME_URL}/api/users`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apiKey': process.env.LEANTIME_API_KEY!, }, body: JSON.stringify({ firstname: token.first_name, lastname: token.last_name, username: token.username, email: profile.email, role: token.role.includes('admin') ? 'admin' : 'user', }), }); if (!leantimeResponse.ok) { console.error('Failed to create/update Leantime user:', await leantimeResponse.text()); } else { console.log('Successfully created/updated Leantime user'); } } catch (error) { console.error('Error creating/updating Leantime user:', error); } } // Return previous token if it's not expired if (Date.now() < (token.accessTokenExpires as number)) { return token; } // Token refresh case try { const response = await fetch( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/token`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ grant_type: "refresh_token", client_id: process.env.KEYCLOAK_CLIENT_ID!, client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, refresh_token: token.refreshToken, }), } ); const tokens = await response.json(); if (!response.ok) throw tokens; return { ...token, accessToken: tokens.access_token, refreshToken: tokens.refresh_token ?? token.refreshToken, accessTokenExpires: Date.now() + tokens.expires_in * 1000, }; } catch (error) { console.error("Error refreshing token:", error); return { ...token, error: "RefreshAccessTokenError" }; } }, async session({ session, token }) { if (token.error) { throw new Error("RefreshAccessTokenError"); } return { ...session, accessToken: token.accessToken, user: { ...session.user, id: token.sub as string, first_name: token.first_name || '', last_name: token.last_name || '', username: token.username || '', role: token.role || [], }, }; }, }, events: { async signOut({ token }) { if (token.refreshToken) { try { // Logout from Keycloak await fetch( `${process.env.KEYCLOAK_BASE_URL}/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/logout`, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", }, body: new URLSearchParams({ client_id: process.env.KEYCLOAK_CLIENT_ID!, client_secret: process.env.KEYCLOAK_CLIENT_SECRET!, refresh_token: token.refreshToken as string, }), } ); // Delete user from Leantime if (token.username) { const leantimeResponse = await fetch(`${process.env.LEANTIME_URL}/api/users/${token.username}`, { method: 'DELETE', headers: { 'apiKey': process.env.LEANTIME_API_KEY!, }, }); if (!leantimeResponse.ok) { console.error('Failed to delete Leantime user:', await leantimeResponse.text()); } else { console.log('Successfully deleted Leantime user'); } } } catch (error) { console.error("Error during logout:", error); } } }, }, }; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };