Open Source Team Metrics based on PRs
at main 10 kB view raw
1import NextAuth from "next-auth" 2import GitHub from "next-auth/providers/github" 3import { NextAuthConfig } from "next-auth" 4import { findUserByEmail, createUser, updateUser, getUserOrganizations, findUserById } from "@/lib/repositories/user-repository" 5import { createGitHubClient } from "@/lib/github" 6import type { JWT } from "next-auth/jwt" 7import { execute } from "@/lib/db" 8import { GitHubService } from "@/lib/services" 9 10// Use fixed demo secret for consistent JWT handling 11function ensureNextAuthSecret(): string { 12 if (process.env.NEXTAUTH_SECRET) { 13 return process.env.NEXTAUTH_SECRET; 14 } 15 16 // Use a fixed demo secret to avoid JWT decryption issues 17 const DEMO_SECRET = 'demo-secret-for-pr-cat-this-is-only-for-demo-mode-not-production-use-64chars'; 18 19 // Set it in process.env so NextAuth can find it 20 process.env.NEXTAUTH_SECRET = DEMO_SECRET; 21 console.log('🔐 Using fixed demo secret for consistent JWT handling'); 22 23 return DEMO_SECRET; 24} 25 26// Generate the secret before NextAuth config 27const NEXTAUTH_SECRET = ensureNextAuthSecret(); 28 29// Extend the Session interface to include accessToken and organizations 30// and GitHub profile fields 31// (You may want to move this to a types file for larger projects) 32declare module "next-auth" { 33 interface Session { 34 accessToken?: string; 35 user: { 36 id: string; 37 name?: string | null; 38 email?: string | null; 39 image?: string | null; 40 login?: string; 41 html_url?: string; 42 avatar_url?: string; 43 }; 44 organizations?: { 45 id: number; 46 github_id: number; 47 name: string; 48 avatar_url: string | null; 49 }[]; 50 // Added flags for onboarding and setup status 51 newUser?: boolean; 52 hasGithubApp?: boolean; 53 } 54} 55 56declare module "next-auth/jwt" { 57 interface JWT { 58 login?: string; 59 html_url?: string; 60 avatar_url?: string; 61 accessToken?: string; 62 } 63} 64 65// Don't run migrations automatically at startup to avoid Edge Runtime errors 66// Instead, we'll run them in API routes that are Node.js compatible 67 68interface GitHubProfile { 69 login?: string; 70 html_url?: string; 71 avatar_url?: string; 72} 73 74// Utility functions for cleaner callbacks 75async function getOrganizationsForUser(userId: string, accessToken?: string) { 76 try { 77 let organizations = await getUserOrganizations(userId) 78 79 if ((!organizations || organizations.length === 0) && accessToken) { 80 const githubClient = createGitHubClient(accessToken) 81 const githubOrgs = await githubClient.getUserOrganizations() 82 // Map GitHub orgs to the expected session format (not full Organization type) 83 const mappedOrgs = githubOrgs.map(org => ({ 84 id: 0, 85 github_id: org.id, 86 name: org.login, 87 avatar_url: org.avatar_url, 88 })) 89 return mappedOrgs 90 } 91 92 return organizations || [] 93 } catch (error) { 94 console.error("Error fetching organizations:", error) 95 return [] 96 } 97} 98 99async function setSessionFlags(userId: string, organizations?: any[]) { 100 try { 101 const user = await findUserById(userId) 102 const newUser = user?.created_at 103 ? new Date(user.created_at).getTime() > Date.now() - 5 * 60 * 1000 104 : true 105 106 const hasGithubApp = !!(organizations && organizations.length > 0) 107 108 return { newUser, hasGithubApp } 109 } catch (error) { 110 console.error("Error setting session flags:", error) 111 return { newUser: false, hasGithubApp: false } 112 } 113} 114 115async function upsertUser(githubId: string, userData: any) { 116 const { name, email, image } = userData 117 118 const { rowsAffected } = await execute( 119 `INSERT INTO users (id, name, email, image, created_at, updated_at) 120 VALUES (?, ?, ?, ?, datetime("now"), datetime("now")) 121 ON CONFLICT(id) DO UPDATE SET 122 name = excluded.name, 123 email = excluded.email, 124 image = excluded.image, 125 updated_at = datetime("now")`, 126 [githubId, name, email, image] 127 ) 128 129 return rowsAffected > 0 130} 131 132export const config = { 133 providers: [ 134 GitHub({ 135 clientId: process.env.GITHUB_OAUTH_CLIENT_ID || 'demo-client-id', 136 clientSecret: process.env.GITHUB_OAUTH_CLIENT_SECRET || 'demo-client-secret', 137 authorization: { 138 params: { 139 scope: 'read:user user:email repo read:org', 140 }, 141 }, 142 // Try without explicit PKCE configuration 143 // checks: ["pkce"], 144 }), 145 ], 146 pages: { 147 signIn: "/sign-in", 148 signOut: "/", 149 error: "/error", 150 }, 151 callbacks: { 152 authorized({ request, auth }) { 153 const { pathname } = request.nextUrl; 154 155 // Allow dashboard access in demo mode (when no real GitHub credentials configured) 156 if (pathname.startsWith("/dashboard")) { 157 const hasGitHubCredentials = process.env.GITHUB_OAUTH_CLIENT_ID && 158 process.env.GITHUB_OAUTH_CLIENT_SECRET && 159 process.env.GITHUB_OAUTH_CLIENT_ID !== 'demo-client-id'; 160 161 if (!hasGitHubCredentials) { 162 // Demo mode: allow dashboard access without authentication 163 console.log('🎯 Demo mode: Allowing dashboard access without authentication'); 164 return true; 165 } 166 167 // Production mode: require authentication 168 return !!auth; 169 } 170 return true; 171 }, 172 async session({ session, token }) { 173 if (!token.sub) { 174 console.warn("Session callback: token.sub is missing"); 175 return session; 176 } 177 178 // Check if we're in demo mode (no real GitHub config) 179 const isDemoMode = !process.env.GITHUB_OAUTH_CLIENT_ID || process.env.GITHUB_OAUTH_CLIENT_ID === 'demo-client-id'; 180 181 if (isDemoMode) { 182 // Demo mode: use simple session without database calls 183 session.user.id = token.sub; 184 session.user.login = token.login || 'demo-user'; 185 session.user.html_url = token.html_url || 'https://github.com/demo-user'; 186 session.user.avatar_url = token.avatar_url || '/api/placeholder/avatar/demo'; 187 session.organizations = []; 188 session.newUser = false; 189 session.hasGithubApp = false; 190 return session; 191 } 192 193 // Real mode: full session handling 194 session.user.id = token.sub; 195 session.user.login = token.login; 196 session.user.html_url = token.html_url; 197 session.user.avatar_url = token.avatar_url; 198 199 if (token.accessToken) { 200 session.accessToken = token.accessToken as string; 201 202 // Safely fetch organizations with error handling to prevent session failures 203 try { 204 session.organizations = await getOrganizationsForUser(session.user.id, session.accessToken); 205 } catch (error) { 206 console.error("Session callback: Failed to fetch organizations", error); 207 // Degrade gracefully - set empty organizations array 208 session.organizations = []; 209 } 210 } 211 212 // Set session flags with error handling 213 try { 214 const flags = await setSessionFlags(session.user.id, session.organizations); 215 session.newUser = flags.newUser; 216 session.hasGithubApp = flags.hasGithubApp; 217 } catch (error) { 218 console.error("Session callback: Failed to set session flags", error); 219 // Degrade gracefully with default values 220 session.newUser = false; 221 session.hasGithubApp = false; 222 } 223 224 return session; 225 }, 226 async jwt({ token, account, profile }) { 227 if (profile && typeof profile.id !== 'undefined' && profile.id !== null) { 228 token.sub = profile.id.toString(); 229 const gh = profile as GitHubProfile; 230 token.login = gh.login; 231 token.html_url = gh.html_url; 232 token.avatar_url = gh.avatar_url; 233 } 234 if (account) { 235 token.accessToken = account.access_token; 236 } 237 return token; 238 }, 239 async signIn({ user, account, profile }) { 240 // Skip database operations if GitHub credentials not properly configured (demo mode) 241 const hasGitHubCredentials = process.env.GITHUB_OAUTH_CLIENT_ID && 242 process.env.GITHUB_OAUTH_CLIENT_SECRET && 243 process.env.GITHUB_OAUTH_CLIENT_ID !== 'demo-client-id'; 244 245 if (!hasGitHubCredentials) { 246 // Demo mode: allow sign-in without database operations 247 console.log('🎯 Demo mode: Allowing sign-in without database operations'); 248 user.id = 'demo-user-123'; 249 return true; 250 } 251 252 // Production mode: full sign-in process with database operations 253 if (!profile || typeof profile.id === 'undefined' || profile.id === null) { 254 console.error("SignIn: Missing required profile.id"); 255 return false; 256 } 257 258 const githubId = profile.id.toString(); 259 260 try { 261 // Upsert user 262 await upsertUser(githubId, { 263 name: user.name ?? profile.login ?? null, 264 // Email may be null/undefined if not provided by GitHub; allow null in DB 265 email: user.email ?? null, 266 image: user.image ?? profile.avatar_url ?? null 267 }); 268 269 user.id = githubId; 270 271 // Sync organizations if we have an access token 272 if (account?.access_token) { 273 try { 274 const githubService = new GitHubService(account.access_token); 275 await githubService.syncUserOrganizations(githubId); 276 } catch (syncError) { 277 console.error(`Organization sync failed for user ${githubId}:`, syncError); 278 } 279 } 280 281 return true; 282 } catch (error) { 283 console.error(`SignIn failed for user ${githubId}:`, error); 284 return false; 285 } 286 }, 287 }, 288 session: { 289 strategy: "jwt", 290 }, 291 // Add additional security configuration 292 secret: NEXTAUTH_SECRET, 293 trustHost: true, 294 // Add cookie configuration to help with PKCE 295 cookies: { 296 pkceCodeVerifier: { 297 name: 'next-auth.pkce.code_verifier', 298 options: { 299 httpOnly: true, 300 sameSite: 'none', 301 path: '/', 302 secure: true 303 } 304 } 305 }, 306} satisfies NextAuthConfig; 307 308export const { handlers, auth, signIn, signOut } = NextAuth(config); 309 310// Ensure this file doesn't execute in Edge Runtime 311export const runtime = 'nodejs';