@@ -242,15 +242,14 @@ async function startServer() {
242242 // ============================================================================
243243 // API Authentication Endpoints
244244 // ============================================================================
245- // Webapp Integration Pattern:
246- // 1. Webapp redirects browser to /login?redirect=<webapp-callback-url>
247- // 2. OAuth flow completes, user redirected back to webapp callback
248- // 3. Webapp calls GET /api/auth/session to get sessionId
249- // 4. Webapp sets sessionId as httpOnly cookie
250- // 5. Subsequent requests: proxy converts cookie to SESSION header
251- //
252- // Deprecated endpoints (remove after webapp migration complete):
253- // - POST /api/auth/logout -> use GET /user/logout or GET /api/user/logout
245+ // Webapp Integration Pattern (API-based):
246+ // 1. Webapp calls GET /api/auth/url?redirect_uri=<callback> to get OAuth URL
247+ // 2. Webapp redirects user to GitHub OAuth URL
248+ // 3. GitHub redirects to webapp callback with ?code=
249+ // 4. Webapp calls POST /api/auth/callback with {code, redirect_uri}
250+ // 5. Core exchanges code for token, creates session, returns {sessionId}
251+ // 6. Webapp sets sessionId as httpOnly cookie
252+ // 7. Subsequent requests: proxy converts cookie to SESSION header
254253 // ============================================================================
255254
256255 app . get ( '/api/auth/status' , async function ( req , res ) {
@@ -328,6 +327,108 @@ async function startServer() {
328327 } ) ;
329328 } ) ;
330329
330+ // Returns GitHub OAuth URL for webapp to redirect to
331+ app . get ( '/api/auth/url' , function ( req , res ) {
332+ const redirectUri = req . query . redirect_uri ;
333+ if ( ! redirectUri ) {
334+ return res . status ( 400 ) . json ( { error : 'redirect_uri is required' } ) ;
335+ }
336+
337+ const oauthUrl =
338+ `https://github.com/login/oauth/authorize?` +
339+ `client_id=${ process . env . GITHUB_CLIENT_ID } ` +
340+ `&redirect_uri=${ encodeURIComponent ( redirectUri ) } ` +
341+ `&scope=public_repo,read:org,admin:repo_hook` ;
342+
343+ res . json ( { url : oauthUrl } ) ;
344+ } ) ;
345+
346+ // Exchanges OAuth code for session - webapp calls this after GitHub callback
347+ app . post ( '/api/auth/callback' , express . json ( ) , async function ( req , res ) {
348+ const { code, redirect_uri } = req . body ;
349+
350+ if ( ! code || ! redirect_uri ) {
351+ return res
352+ . status ( 400 )
353+ . json ( { error : 'code and redirect_uri are required' } ) ;
354+ }
355+
356+ try {
357+ // Exchange code for access token
358+ const tokenResponse = await fetch (
359+ 'https://github.com/login/oauth/access_token' ,
360+ {
361+ method : 'POST' ,
362+ headers : {
363+ Accept : 'application/json' ,
364+ 'Content-Type' : 'application/json' ,
365+ } ,
366+ body : JSON . stringify ( {
367+ client_id : process . env . GITHUB_CLIENT_ID ,
368+ client_secret : process . env . GITHUB_CLIENT_SECRET ,
369+ code,
370+ redirect_uri,
371+ } ) ,
372+ }
373+ ) ;
374+
375+ if ( ! tokenResponse . ok ) {
376+ throw new Error (
377+ `GitHub token exchange failed: ${ tokenResponse . status } `
378+ ) ;
379+ }
380+
381+ const tokenData = await tokenResponse . json ( ) ;
382+ if ( tokenData . error || ! tokenData . access_token ) {
383+ return res . status ( 401 ) . json ( {
384+ error : tokenData . error_description || 'Failed to obtain access token' ,
385+ } ) ;
386+ }
387+
388+ // Fetch GitHub user info
389+ const userResponse = await fetch ( 'https://api.github.com/user' , {
390+ headers : {
391+ Authorization : `Bearer ${ tokenData . access_token } ` ,
392+ Accept : 'application/vnd.github.v3+json' ,
393+ } ,
394+ } ) ;
395+
396+ if ( ! userResponse . ok ) {
397+ throw new Error ( `Failed to fetch GitHub user: ${ userResponse . status } ` ) ;
398+ }
399+
400+ const githubUser = await userResponse . json ( ) ;
401+ if ( ! githubUser . id ) {
402+ return res . status ( 401 ) . json ( { error : 'Invalid GitHub user response' } ) ;
403+ }
404+
405+ // Find or create user
406+ let user = await User . findByGithubUserId ( githubUser . id ) ;
407+ if ( user ) {
408+ user = await User . update ( user . _id , {
409+ githubAccessToken : tokenData . access_token ,
410+ } ) ;
411+ } else {
412+ user = await User . create ( {
413+ githubUserId : githubUser . id ,
414+ githubAccessToken : tokenData . access_token ,
415+ } ) ;
416+ }
417+
418+ // Create session
419+ req . session . userId = user . _id . toString ( ) ;
420+
421+ // Return session ID for webapp to set as httpOnly cookie
422+ res . json ( {
423+ authenticated : true ,
424+ sessionId : req . sessionID ,
425+ } ) ;
426+ } catch ( error ) {
427+ console . error ( 'OAuth callback error:' , error ) ;
428+ res . status ( 500 ) . json ( { error : 'Authentication failed' } ) ;
429+ }
430+ } ) ;
431+
331432 app . get ( '/v1/user' , async function ( req , res ) {
332433 if ( ! req . session . userId ) {
333434 return res . status ( 401 ) . end ( ) ;
0 commit comments