Open Source Team Metrics based on PRs
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';