-
Notifications
You must be signed in to change notification settings - Fork 280
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore(nuxt): Allow custom middleware handler and options (#4655)
- Loading branch information
1 parent
23a9160
commit 82a5502
Showing
8 changed files
with
268 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@clerk/nuxt": patch | ||
--- | ||
|
||
Allow custom middleware with options |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
import { expect, test } from '@playwright/test'; | ||
|
||
import type { Application } from '../../models/application'; | ||
import { appConfigs } from '../../presets'; | ||
import { createTestUtils } from '../../testUtils'; | ||
|
||
test.describe('custom middleware @nuxt', () => { | ||
test.describe.configure({ mode: 'parallel' }); | ||
let app: Application; | ||
|
||
test.beforeAll(async () => { | ||
app = await appConfigs.nuxt.node | ||
.clone() | ||
.setName('nuxt-custom-middleware') | ||
.addFile( | ||
'nuxt.config.js', | ||
() => `export default defineNuxtConfig({ | ||
modules: ['@clerk/nuxt'], | ||
devtools: { enabled: false }, | ||
clerk: { | ||
skipServerMiddleware: true | ||
} | ||
});`, | ||
) | ||
.addFile( | ||
'server/middleware/clerk.js', | ||
() => `import { clerkMiddleware } from '@clerk/nuxt/server'; | ||
export default clerkMiddleware((event) => { | ||
const { userId } = event.context.auth | ||
if (!userId && event.path === '/api/me') { | ||
throw createError({ | ||
statusCode: 401, | ||
statusMessage: 'You are not authorized to access this resource.' | ||
}) | ||
} | ||
}); | ||
`, | ||
) | ||
.addFile( | ||
'pages/me.vue', | ||
() => `<script setup> | ||
const { data, error } = await useFetch('/api/me'); | ||
</script> | ||
<template> | ||
<div v-if="data">Hello, {{ data.firstName }}</div> | ||
<div v-else-if="error">{{ error.statusCode }}: {{ error.statusMessage }}</div> | ||
<div v-else>Unknown status</div> | ||
</template>`, | ||
) | ||
.commit(); | ||
|
||
await app.setup(); | ||
await app.withEnv(appConfigs.envs.withCustomRoles); | ||
await app.dev(); | ||
}); | ||
|
||
test.afterAll(async () => { | ||
await app.teardown(); | ||
}); | ||
|
||
test('guard API route with custom middleware', async ({ page, context }) => { | ||
const u = createTestUtils({ app, page, context }); | ||
const fakeUser = u.services.users.createFakeUser(); | ||
await u.services.users.createBapiUser(fakeUser); | ||
|
||
// Verify unauthorized access is blocked | ||
await u.page.goToAppHome(); | ||
await u.po.expect.toBeSignedOut(); | ||
await u.page.goToRelative('/me'); | ||
await expect(u.page.getByText('401: You are not authorized to access this resource')).toBeVisible(); | ||
|
||
// Sign in flow | ||
await u.page.goToRelative('/sign-in'); | ||
await u.po.signIn.waitForMounted(); | ||
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); | ||
await u.po.expect.toBeSignedIn(); | ||
await u.page.waitForAppUrl('/'); | ||
|
||
// Verify authorized access works | ||
await u.page.goToRelative('/me'); | ||
await expect(u.page.getByText(`Hello, ${fakeUser.firstName}`)).toBeVisible(); | ||
|
||
await fakeUser.deleteIfExists(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
106 changes: 106 additions & 0 deletions
106
packages/nuxt/src/runtime/server/__tests__/clerkMiddleware.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import { createApp, eventHandler, setResponseHeader, toWebHandler } from 'h3'; | ||
import { vi } from 'vitest'; | ||
|
||
import { clerkMiddleware } from '../clerkMiddleware'; | ||
|
||
const AUTH_RESPONSE = { | ||
userId: 'user_2jZSstSbxtTndD9P7q4kDl0VVZa', | ||
sessionId: 'sess_2jZSstSbxtTndD9P7q4kDl0VVZa', | ||
}; | ||
|
||
const MOCK_OPTIONS = { | ||
secretKey: 'sk_test_xxxxxxxxxxxxxxxxxx', | ||
publishableKey: 'pk_test_xxxxxxxxxxxxx', | ||
signInUrl: '/foo', | ||
signUpUrl: '/bar', | ||
}; | ||
|
||
vi.mock('#imports', () => { | ||
return { | ||
useRuntimeConfig: () => ({}), | ||
}; | ||
}); | ||
|
||
const authenticateRequestMock = vi.fn().mockResolvedValue({ | ||
toAuth: () => AUTH_RESPONSE, | ||
headers: new Headers(), | ||
}); | ||
|
||
vi.mock('../clerkClient', () => { | ||
return { | ||
clerkClient: () => ({ | ||
authenticateRequest: authenticateRequestMock, | ||
telemetry: { record: vi.fn() }, | ||
}), | ||
}; | ||
}); | ||
|
||
describe('clerkMiddleware(params)', () => { | ||
test('renders route as normally when used without params', async () => { | ||
const app = createApp(); | ||
const handler = toWebHandler(app); | ||
app.use(clerkMiddleware()); | ||
app.use( | ||
'/', | ||
eventHandler(event => event.context.auth), | ||
); | ||
const response = await handler(new Request(new URL('/', 'http://localhost'))); | ||
|
||
expect(response.status).toBe(200); | ||
expect(await response.json()).toEqual(AUTH_RESPONSE); | ||
}); | ||
|
||
test('renders route as normally when used with options param', async () => { | ||
const app = createApp(); | ||
const handler = toWebHandler(app); | ||
app.use(clerkMiddleware(MOCK_OPTIONS)); | ||
app.use( | ||
'/', | ||
eventHandler(event => event.context.auth), | ||
); | ||
const response = await handler(new Request(new URL('/', 'http://localhost'))); | ||
|
||
expect(response.status).toBe(200); | ||
expect(authenticateRequestMock).toHaveBeenCalledWith(expect.any(Request), expect.objectContaining(MOCK_OPTIONS)); | ||
expect(await response.json()).toEqual(AUTH_RESPONSE); | ||
}); | ||
|
||
test('executes handler and renders route when used with a custom handler', async () => { | ||
const app = createApp(); | ||
const handler = toWebHandler(app); | ||
app.use( | ||
clerkMiddleware(event => { | ||
setResponseHeader(event, 'a-custom-header', '1'); | ||
}), | ||
); | ||
app.use( | ||
'/', | ||
eventHandler(event => event.context.auth), | ||
); | ||
const response = await handler(new Request(new URL('/', 'http://localhost'))); | ||
|
||
expect(response.status).toBe(200); | ||
expect(response.headers.get('a-custom-header')).toBe('1'); | ||
expect(await response.json()).toEqual(AUTH_RESPONSE); | ||
}); | ||
|
||
test('executes handler and renders route when used with a custom handler and options', async () => { | ||
const app = createApp(); | ||
const handler = toWebHandler(app); | ||
app.use( | ||
clerkMiddleware(event => { | ||
setResponseHeader(event, 'a-custom-header', '1'); | ||
}, MOCK_OPTIONS), | ||
); | ||
app.use( | ||
'/', | ||
eventHandler(event => event.context.auth), | ||
); | ||
const response = await handler(new Request(new URL('/', 'http://localhost'))); | ||
|
||
expect(response.status).toBe(200); | ||
expect(response.headers.get('a-custom-header')).toBe('1'); | ||
expect(authenticateRequestMock).toHaveBeenCalledWith(expect.any(Request), expect.objectContaining(MOCK_OPTIONS)); | ||
expect(await response.json()).toEqual(AUTH_RESPONSE); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { defineConfig } from 'vitest/config'; | ||
|
||
export default defineConfig({ | ||
test: { | ||
globals: true, | ||
}, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters