-
Notifications
You must be signed in to change notification settings - Fork 464
Expand file tree
/
Copy pathgithub.server.ts
More file actions
158 lines (147 loc) · 4.41 KB
/
github.server.ts
File metadata and controls
158 lines (147 loc) · 4.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import { SetCookie } from '@mjackson/headers'
import { createId as cuid } from '@paralleldrive/cuid2'
import { redirect } from 'react-router'
import { GitHubStrategy } from 'remix-auth-github'
import { z } from 'zod'
import { cache, cachified } from '#app/utils/cache.server.ts'
import { type Timings } from '../timing.server.ts'
import { MOCK_CODE_GITHUB_HEADER, MOCK_CODE_GITHUB } from './constants.ts'
import { type AuthProvider } from './provider.ts'
const GitHubUserSchema = z.object({ login: z.string() })
const GitHubUserParseResult = z
.object({
success: z.literal(true),
data: GitHubUserSchema,
})
.or(
z.object({
success: z.literal(false),
}),
)
const shouldMock =
process.env.GITHUB_CLIENT_ID?.startsWith('MOCK_') ||
process.env.NODE_ENV === 'test'
const GitHubEmailSchema = z.object({
email: z.string(),
verified: z.boolean(),
primary: z.boolean(),
visibility: z.string().nullable(),
})
const GitHubEmailsResponseSchema = z.array(GitHubEmailSchema)
const GitHubUserResponseSchema = z.object({
login: z.string(),
id: z.number().or(z.string()),
name: z.string().optional(),
avatar_url: z.string().optional(),
})
export class GitHubProvider implements AuthProvider {
getAuthStrategy() {
if (
!process.env.GITHUB_CLIENT_ID ||
!process.env.GITHUB_CLIENT_SECRET ||
!process.env.GITHUB_REDIRECT_URI
) {
console.log(
'GitHub OAuth strategy not available because environment variables are not set',
)
return null
}
return new GitHubStrategy(
{
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
redirectURI: process.env.GITHUB_REDIRECT_URI,
},
async ({ tokens }) => {
// we need to fetch the user and the emails separately, this is a change in remix-auth-github
// from the previous version that supported fetching both in one call
const userResponse = await fetch('https://api.github.com/user', {
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${tokens.accessToken()}`,
'X-GitHub-Api-Version': '2022-11-28',
},
})
const rawUser = await userResponse.json()
const user = GitHubUserResponseSchema.parse(rawUser)
const emailsResponse = await fetch(
'https://api.github.com/user/emails',
{
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${tokens.accessToken()}`,
'X-GitHub-Api-Version': '2022-11-28',
},
},
)
const rawEmails = await emailsResponse.json()
const emails = GitHubEmailsResponseSchema.parse(rawEmails)
const email = emails.find((e) => e.primary)?.email
if (!email) {
throw new Error('Email not found')
}
return {
id: user.id,
email,
name: user.name,
username: user.login,
imageUrl: user.avatar_url,
}
},
)
}
async resolveConnectionData(
providerId: string,
{ timings }: { timings?: Timings } = {},
) {
const result = await cachified({
key: `connection-data:github:${providerId}`,
cache,
timings,
ttl: 1000 * 60,
swr: 1000 * 60 * 60 * 24 * 7,
async getFreshValue(context) {
const response = await fetch(
`https://api.github.com/user/${providerId}`,
{ headers: { Authorization: `token ${process.env.GITHUB_TOKEN}` } },
)
const rawJson = await response.json()
const result = GitHubUserSchema.safeParse(rawJson)
if (!result.success) {
// if it was unsuccessful, then we should kick it out of the cache
// asap and try again.
context.metadata.ttl = 0
}
return result
},
checkValue: GitHubUserParseResult,
})
return {
displayName: result.success ? result.data.login : 'Unknown',
link: result.success ? `https://github.com/${result.data.login}` : null,
} as const
}
async handleMockAction(request: Request) {
if (!shouldMock) return
const state = cuid()
// allows us to inject a code when running e2e tests,
// but falls back to a pre-defined 🐨 constant
const code =
request.headers.get(MOCK_CODE_GITHUB_HEADER) || MOCK_CODE_GITHUB
const searchParams = new URLSearchParams({ code, state })
let cookie = new SetCookie({
name: 'github',
value: searchParams.toString(),
path: '/',
sameSite: 'Lax',
httpOnly: true,
maxAge: 60 * 10,
secure: process.env.NODE_ENV === 'production' || undefined,
})
throw redirect(`/auth/github/callback?${searchParams}`, {
headers: {
'Set-Cookie': cookie.toString(),
},
})
}
}