1+ import { SetCookie } from '@mjackson/headers'
12import { createId as cuid } from '@paralleldrive/cuid2'
23import { redirect } from 'react-router'
34import { GitHubStrategy } from 'remix-auth-github'
45import { z } from 'zod'
56import { cache , cachified } from '../cache.server.ts'
6- import { connectionSessionStorage } from '../connections.server.ts'
77import { type Timings } from '../timing.server.ts'
88import { MOCK_CODE_GITHUB_HEADER , MOCK_CODE_GITHUB } from './constants.ts'
99import { type AuthProvider } from './provider.ts'
@@ -24,27 +24,62 @@ const shouldMock =
2424 process . env . GITHUB_CLIENT_ID ?. startsWith ( 'MOCK_' ) ||
2525 process . env . NODE_ENV === 'test'
2626
27+ type GitHubEmailsResponse = {
28+ email : string
29+ verified : boolean
30+ primary : boolean
31+ visibility : string | null
32+ } [ ]
33+
34+ type GitHubUserResponse = {
35+ login : string
36+ id : string
37+ name : string | undefined
38+ avatar_url : string | undefined
39+ }
40+
2741export class GitHubProvider implements AuthProvider {
2842 getAuthStrategy ( ) {
2943 return new GitHubStrategy (
3044 {
31- clientID : process . env . GITHUB_CLIENT_ID ,
45+ clientId : process . env . GITHUB_CLIENT_ID ,
3246 clientSecret : process . env . GITHUB_CLIENT_SECRET ,
33- callbackURL : '/auth/github/callback' ,
47+ redirectURI : '/auth/github/callback' ,
3448 } ,
35- async ( { profile } ) => {
36- const email = profile . emails [ 0 ] ?. value . trim ( ) . toLowerCase ( )
49+ async ( { tokens } ) => {
50+ // we need to fetch the user and the emails separately, this is a change in remix-auth-github
51+ // from the previous version that supported fetching both in one call
52+ const userResponse = await fetch ( 'https://api.github.com/user' , {
53+ headers : {
54+ Accept : 'application/vnd.github+json' ,
55+ Authorization : `Bearer ${ tokens . accessToken ( ) } ` ,
56+ 'X-GitHub-Api-Version' : '2022-11-28' ,
57+ } ,
58+ } )
59+ const user = ( await userResponse . json ( ) ) as GitHubUserResponse
60+
61+ const emailsResponse = await fetch (
62+ 'https://api.github.com/user/emails' ,
63+ {
64+ headers : {
65+ Accept : 'application/vnd.github+json' ,
66+ Authorization : `Bearer ${ tokens . accessToken ( ) } ` ,
67+ 'X-GitHub-Api-Version' : '2022-11-28' ,
68+ } ,
69+ } ,
70+ )
71+ const emails = ( await emailsResponse . json ( ) ) as GitHubEmailsResponse
72+ const email = emails . find ( ( e ) => e . primary ) ?. email
3773 if ( ! email ) {
3874 throw new Error ( 'Email not found' )
3975 }
40- const username = profile . displayName
41- const imageUrl = profile . photos [ 0 ] ?. value
76+
4277 return {
78+ id : user . id ,
4379 email,
44- id : profile . id ,
45- username,
46- name : profile . name . givenName ,
47- imageUrl,
80+ name : user . name ,
81+ username : user . login ,
82+ imageUrl : user . avatar_url ,
4883 }
4984 } ,
5085 )
@@ -85,21 +120,24 @@ export class GitHubProvider implements AuthProvider {
85120 async handleMockAction ( request : Request ) {
86121 if ( ! shouldMock ) return
87122
88- const connectionSession = await connectionSessionStorage . getSession (
89- request . headers . get ( 'cookie' ) ,
90- )
91123 const state = cuid ( )
92- connectionSession . set ( 'oauth2:state' , state )
93-
94124 // allows us to inject a code when running e2e tests,
95125 // but falls back to a pre-defined 🐨 constant
96126 const code =
97127 request . headers . get ( MOCK_CODE_GITHUB_HEADER ) || MOCK_CODE_GITHUB
98128 const searchParams = new URLSearchParams ( { code, state } )
129+ let cookie = new SetCookie ( {
130+ name : 'github' ,
131+ value : searchParams . toString ( ) ,
132+ path : '/' ,
133+ sameSite : 'Lax' ,
134+ httpOnly : true ,
135+ maxAge : 60 * 10 ,
136+ secure : process . env . NODE_ENV === 'production' || undefined ,
137+ } )
99138 throw redirect ( `/auth/github/callback?${ searchParams } ` , {
100139 headers : {
101- 'set-cookie' :
102- await connectionSessionStorage . commitSession ( connectionSession ) ,
140+ 'Set-Cookie' : cookie . toString ( ) ,
103141 } ,
104142 } )
105143 }
0 commit comments