Skip to content

Commit 1d00e19

Browse files
committed
make third party auth generic
1 parent cb4b3e2 commit 1d00e19

14 files changed

+332
-141
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ SENTRY_DSN="your-dsn"
1111
# if they aren't then the real github api will be attempted
1212
GITHUB_CLIENT_ID="MOCK_GITHUB_CLIENT_ID"
1313
GITHUB_CLIENT_SECRET="MOCK_GITHUB_CLIENT_SECRET"
14+
GITHUB_TOKEN="MOCK_GITHUB_TOKEN"

app/routes/_auth+/auth.github.callback.test.ts renamed to app/routes/_auth+/auth.$provider.callback.test.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ import { GITHUB_PROVIDER_NAME } from '../../utils/github-auth.server.ts'
1919
import { invariant } from '../../utils/misc.tsx'
2020
import { sessionStorage } from '../../utils/session.server.ts'
2121
import { twoFAVerificationType } from '../settings+/profile.two-factor.tsx'
22-
import { ROUTE_PATH, loader } from './auth.github.callback.ts'
22+
import { loader } from './auth.$provider.callback.ts'
23+
24+
const ROUTE_PATH = '/auth/github/callback'
25+
const PARAMS = { provider: 'github' }
2326

2427
test('a new user goes to onboarding', async () => {
2528
const request = await setupRequest()
26-
const response = await loader({ request, params: {}, context: {} }).catch(
29+
const response = await loader({ request, params: PARAMS, context: {} }).catch(
2730
e => e,
2831
)
2932
expect(response).toHaveRedirect('/onboarding/github')
@@ -36,7 +39,7 @@ test('when auth fails, send the user to login with a toast', async () => {
3639
}),
3740
)
3841
const request = await setupRequest()
39-
const response = await loader({ request, params: {}, context: {} }).catch(
42+
const response = await loader({ request, params: PARAMS, context: {} }).catch(
4043
e => e,
4144
)
4245
invariant(response instanceof Response, 'response should be a Response')
@@ -54,7 +57,7 @@ test('when auth fails, send the user to login with a toast', async () => {
5457
test('when a user is logged in, it creates the connection', async () => {
5558
const session = await setupUser()
5659
const request = await setupRequest(session.id)
57-
const response = await loader({ request, params: {}, context: {} })
60+
const response = await loader({ request, params: PARAMS, context: {} })
5861
expect(response).toHaveRedirect('/settings/profile/connections')
5962
await expect(response).toSendToast(
6063
expect.objectContaining({
@@ -86,7 +89,7 @@ test(`when a user is logged in and has already connected, it doesn't do anything
8689
},
8790
})
8891
const request = await setupRequest(session.id)
89-
const response = await loader({ request, params: {}, context: {} })
92+
const response = await loader({ request, params: PARAMS, context: {} })
9093
expect(response).toHaveRedirect('/settings/profile/connections')
9194
expect(response).toSendToast(
9295
expect.objectContaining({
@@ -100,7 +103,7 @@ test('when a user exists with the same email, create connection and make session
100103
const email = primaryGitHubEmail.email.toLowerCase()
101104
const { userId } = await setupUser({ ...createUser(), email })
102105
const request = await setupRequest()
103-
const response = await loader({ request, params: {}, context: {} })
106+
const response = await loader({ request, params: PARAMS, context: {} })
104107

105108
expect(response).toHaveRedirect('/')
106109

@@ -140,7 +143,7 @@ test('gives an error if the account is already connected to another user', async
140143
})
141144
const session = await setupUser()
142145
const request = await setupRequest(session.id)
143-
const response = await loader({ request, params: {}, context: {} })
146+
const response = await loader({ request, params: PARAMS, context: {} })
144147
expect(response).toHaveRedirect('/settings/profile/connections')
145148
await expect(response).toSendToast(
146149
expect.objectContaining({
@@ -162,7 +165,7 @@ test('if a user is not logged in, but the connection exists, make a session', as
162165
},
163166
})
164167
const request = await setupRequest()
165-
const response = await loader({ request, params: {}, context: {} })
168+
const response = await loader({ request, params: PARAMS, context: {} })
166169
expect(response).toHaveRedirect('/')
167170
await expect(response).toHaveSessionForUser(userId)
168171
})
@@ -185,7 +188,7 @@ test('if a user is not logged in, but the connection exists and they have enable
185188
},
186189
})
187190
const request = await setupRequest()
188-
const response = await loader({ request, params: {}, context: {} })
191+
const response = await loader({ request, params: PARAMS, context: {} })
189192
const searchParams = new URLSearchParams({
190193
type: twoFAVerificationType,
191194
target: userId,

app/routes/_auth+/auth.github.callback.ts renamed to app/routes/_auth+/auth.$provider.callback.ts

Lines changed: 20 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,55 +4,36 @@ import {
44
getSessionExpirationDate,
55
getUserId,
66
} from '../../utils/auth.server.ts'
7+
import { handleMockCallback } from '../../utils/connections.server.ts'
8+
import { ProviderNameSchema, providerLabels } from '../../utils/connections.tsx'
79
import { prisma } from '../../utils/db.server.ts'
8-
import { GITHUB_PROVIDER_NAME } from '../../utils/github-auth.server.ts'
910
import { combineHeaders } from '../../utils/misc.tsx'
1011
import {
1112
destroyRedirectToHeader,
1213
getRedirectCookieValue,
1314
} from '../../utils/redirect-cookie.server.ts'
14-
import { sessionStorage } from '../../utils/session.server.ts'
1515
import {
1616
createToastHeaders,
1717
redirectWithToast,
1818
} from '../../utils/toast.server.ts'
1919
import { verifySessionStorage } from '../../utils/verification.server.ts'
2020
import { handleNewSession } from './login.tsx'
2121
import {
22-
githubIdKey,
2322
onboardingEmailSessionKey,
2423
prefilledProfileKey,
25-
} from './onboarding_.github.tsx'
26-
27-
export const ROUTE_PATH = '/auth/github/callback'
24+
providerIdKey,
25+
} from './onboarding_.$provider.tsx'
2826

2927
const destroyRedirectTo = { 'set-cookie': destroyRedirectToHeader }
3028

31-
export async function loader({ request }: DataFunctionArgs) {
32-
const reqUrl = new URL(request.url)
29+
export async function loader({ request, params }: DataFunctionArgs) {
30+
const providerName = ProviderNameSchema.parse(params.provider)
31+
request = await handleMockCallback(providerName, request)
3332
const redirectTo = getRedirectCookieValue(request)
34-
debugger
35-
36-
// normally you *really* want to avoid including test/dev code in your source
37-
// but this is one of those cases where it's worth it to make the dev
38-
// experience better. The fact is it's basically impossible to test these
39-
// kinds of integrations.
40-
if (process.env.GITHUB_CLIENT_ID?.startsWith('MOCK_')) {
41-
const cookieSession = await sessionStorage.getSession(
42-
request.headers.get('cookie'),
43-
)
44-
const state = cookieSession.get('oauth2:state') ?? 'MOCK_STATE'
45-
cookieSession.set('oauth2:state', state)
46-
reqUrl.searchParams.set('state', state)
47-
request.headers.set(
48-
'cookie',
49-
await sessionStorage.commitSession(cookieSession),
50-
)
51-
request = new Request(reqUrl.toString(), request)
52-
}
33+
const label = providerLabels[providerName]
5334

5435
const authResult = await authenticator
55-
.authenticate(GITHUB_PROVIDER_NAME, request, { throwOnError: true })
36+
.authenticate(providerName, request, { throwOnError: true })
5637
.then(
5738
data => ({ success: true, data }) as const,
5839
error => ({ success: false, error }) as const,
@@ -64,7 +45,7 @@ export async function loader({ request }: DataFunctionArgs) {
6445
'/login',
6546
{
6647
title: 'Auth Failed',
67-
description: 'There was an error authenticating with GitHub.',
48+
description: `There was an error authenticating with ${label}.`,
6849
type: 'error',
6950
},
7051
{ headers: destroyRedirectTo },
@@ -87,7 +68,7 @@ export async function loader({ request }: DataFunctionArgs) {
8768
'/settings/profile/connections',
8869
{
8970
title: 'Already Connected',
90-
description: `Your "${profile.username}" GitHub account is already connected.`,
71+
description: `Your "${profile.username}" ${label} account is already connected.`,
9172
},
9273
{ headers: destroyRedirectTo },
9374
)
@@ -96,18 +77,18 @@ export async function loader({ request }: DataFunctionArgs) {
9677
'/settings/profile/connections',
9778
{
9879
title: 'Already Connected',
99-
description: `The "${profile.username}" GitHub account is already connected to another account.`,
80+
description: `The "${profile.username}" ${label} account is already connected to another account.`,
10081
},
10182
{ headers: destroyRedirectTo },
10283
)
10384
}
10485
}
10586

106-
// If we're already logged in, then link the GitHub account
87+
// If we're already logged in, then link the account
10788
if (userId) {
10889
await prisma.connection.create({
10990
data: {
110-
providerName: GITHUB_PROVIDER_NAME,
91+
providerName,
11192
providerId: profile.id,
11293
userId,
11394
},
@@ -117,7 +98,7 @@ export async function loader({ request }: DataFunctionArgs) {
11798
{
11899
title: 'Connected',
119100
type: 'success',
120-
description: `Your "${profile.username}" GitHub account has been connected.`,
101+
description: `Your "${profile.username}" ${label} account has been connected.`,
121102
},
122103
{ headers: destroyRedirectTo },
123104
)
@@ -128,7 +109,7 @@ export async function loader({ request }: DataFunctionArgs) {
128109
return makeSession({ request, userId: existingConnection.userId })
129110
}
130111

131-
// if the github email matches a user in the db, then link the account and
112+
// if the email matches a user in the db, then link the account and
132113
// make a new session
133114
const user = await prisma.user.findUnique({
134115
select: { id: true },
@@ -137,7 +118,7 @@ export async function loader({ request }: DataFunctionArgs) {
137118
if (user) {
138119
await prisma.connection.create({
139120
data: {
140-
providerName: GITHUB_PROVIDER_NAME,
121+
providerName,
141122
providerId: profile.id,
142123
userId: user.id,
143124
},
@@ -147,7 +128,7 @@ export async function loader({ request }: DataFunctionArgs) {
147128
{
148129
headers: await createToastHeaders({
149130
title: 'Connected',
150-
description: `Your "${profile.username}" GitHub account has been connected.`,
131+
description: `Your "${profile.username}" ${label} account has been connected.`,
151132
}),
152133
},
153134
)
@@ -163,9 +144,9 @@ export async function loader({ request }: DataFunctionArgs) {
163144
email: profile.email.toLowerCase(),
164145
username: profile.username?.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase(),
165146
})
166-
verifySession.set(githubIdKey, profile.id)
147+
verifySession.set(providerIdKey, profile.id)
167148
const onboardingRedirect = [
168-
'/onboarding/github',
149+
`/onboarding/${providerName}`,
169150
redirectTo ? new URLSearchParams({ redirectTo }) : null,
170151
]
171152
.filter(Boolean)

app/routes/_auth+/auth.github.ts renamed to app/routes/_auth+/auth.$provider.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { redirect, type DataFunctionArgs } from '@remix-run/node'
22
import { authenticator } from '../../utils/auth.server.ts'
3-
import { GITHUB_PROVIDER_NAME } from '../../utils/github-auth.server.ts'
3+
import { handleMockAction } from '../../utils/connections.server.ts'
4+
import { ProviderNameSchema } from '../../utils/connections.tsx'
45
import { getReferrerRoute } from '../../utils/misc.tsx'
56
import { getRedirectCookieHeader } from '../../utils/redirect-cookie.server.ts'
67

78
export async function loader() {
89
return redirect('/login')
910
}
1011

11-
export async function action({ request }: DataFunctionArgs) {
12+
export async function action({ request, params }: DataFunctionArgs) {
13+
const providerName = ProviderNameSchema.parse(params.provider)
1214
const formData = await request.formData()
1315
const rawRedirectTo = formData.get('redirectTo')
1416
const redirectTo =
@@ -18,13 +20,9 @@ export async function action({ request }: DataFunctionArgs) {
1820

1921
const redirectToCookie = getRedirectCookieHeader(redirectTo)
2022

21-
if (process.env.GITHUB_CLIENT_ID?.startsWith('MOCK_')) {
22-
return redirect(`/auth/github/callback?code=MOCK_CODE&state=MOCK_STATE`, {
23-
headers: redirectToCookie ? { 'set-cookie': redirectToCookie } : {},
24-
})
25-
}
23+
await handleMockAction(providerName, redirectToCookie)
2624
try {
27-
return await authenticator.authenticate(GITHUB_PROVIDER_NAME, request)
25+
return await authenticator.authenticate(providerName, request)
2826
} catch (error: unknown) {
2927
if (error instanceof Response && redirectToCookie) {
3028
error.headers.append('set-cookie', redirectToCookie)

app/routes/_auth+/login.tsx

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ import {
1919
requireAnonymous,
2020
sessionKey,
2121
} from '../../utils/auth.server.ts'
22+
import {
23+
ProviderConnectionForm,
24+
providerNames,
25+
} from '../../utils/connections.tsx'
2226
import { prisma } from '../../utils/db.server.ts'
2327
import {
2428
combineResponseInits,
@@ -229,7 +233,6 @@ export async function action({ request }: DataFunctionArgs) {
229233
export default function LoginPage() {
230234
const actionData = useActionData<typeof action>()
231235
const isPending = useIsPending()
232-
const isGitHubSubmitting = useIsPending({ formAction: '/auth/github' })
233236
const [searchParams] = useSearchParams()
234237
const redirectTo = searchParams.get('redirectTo')
235238

@@ -314,24 +317,16 @@ export default function LoginPage() {
314317
</StatusButton>
315318
</div>
316319
</Form>
317-
<Form
318-
className="mt-5 flex items-center justify-center gap-2 border-t-2 border-border pt-3"
319-
action="/auth/github"
320-
method="POST"
321-
>
322-
<input
323-
type="hidden"
324-
name="redirectTo"
325-
value={redirectTo ?? '/'}
326-
/>
327-
<StatusButton
328-
type="submit"
329-
className="w-full"
330-
status={isGitHubSubmitting ? 'pending' : 'idle'}
331-
>
332-
Login with GitHub
333-
</StatusButton>
334-
</Form>
320+
<div className="mt-5 flex flex-col gap-5 border-b-2 border-t-2 border-border py-3">
321+
{providerNames.map(providerName => (
322+
<ProviderConnectionForm
323+
key={providerName}
324+
type="Login"
325+
providerName={providerName}
326+
redirectTo={redirectTo}
327+
/>
328+
))}
329+
</div>
335330
<div className="flex items-center justify-center gap-2 pt-6">
336331
<span className="text-muted-foreground">New here?</span>
337332
<Link

0 commit comments

Comments
 (0)