Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: protected routes defined from nuxt config #47

Open
wants to merge 46 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
be1f98e
feat: protected routes defined from nuxt config
DanielRivers Jan 26, 2024
3ca8701
feat: update to use the routerules
DanielRivers Feb 3, 2024
c86d113
deps: dependancy bump
DanielRivers Feb 3, 2024
f4f02ea
fix: implementation issues
DanielRivers Mar 14, 2024
38bfded
Merge branch 'main' into feat/protected-server-routes
DanielRivers Mar 14, 2024
b0e5d28
chore: switch to use addTypeTemplate from nuxt/kit
DanielRivers Mar 14, 2024
0ebd60c
fix: rejectNavigation uses input params
DanielRivers Mar 14, 2024
a9d3d08
Merge branch 'feat/protected-server-routes' of github.com:DanielRiver…
DanielRivers Mar 14, 2024
a2ea453
feat: update to nuxt 3.11 appMiddleware syntax
DanielRivers Mar 18, 2024
317c160
fix: add missing imports
DanielRivers Mar 18, 2024
2282186
fix: missing import
DanielRivers Mar 18, 2024
8dc31d3
Merge remote-tracking branch 'origin/main' into feat/protected-server…
danielroe Apr 10, 2024
788c9be
Merge remote-tracking branch 'origin/main' into feat/protected-server…
danielroe Apr 10, 2024
3470253
fix: move route rule definition into type template
danielroe Apr 10, 2024
548b6dd
fix: use `getRouteRules` utility
danielroe Apr 10, 2024
224b2a6
chore: fix
danielroe Apr 10, 2024
d280c59
perf: simplify route middleware
danielroe Apr 11, 2024
cd98cf0
fix: remove extra chain
danielroe Apr 11, 2024
0895155
Merge branch 'main' into feat/protected-server-routes
danielroe Apr 11, 2024
0d816b1
fix: crash in auth-logged-in middleware
DanielRivers Jun 10, 2024
e1e7dc0
Merge branch 'main' into feat/protected-server-routes
DanielRivers Aug 15, 2024
64da960
chore: lint fixes
DanielRivers Aug 15, 2024
6f8c40c
feat: add access check for client side
DanielRivers Aug 19, 2024
afa80e4
chore: lint fix
DanielRivers Aug 19, 2024
fc93889
chore: fix lint
DanielRivers Aug 19, 2024
97435e6
Merge branch 'main' into feat/protected-server-routes
DanielRivers Aug 19, 2024
9cf1b71
Merge branch 'main' into feat/protected-server-routes
DanielRivers Aug 19, 2024
b0bb18c
chore: lint following merge
DanielRivers Aug 19, 2024
96c3625
Merge remote-tracking branch 'origin/main' into feat/protected-server…
danielroe Aug 20, 2024
4e868c8
Merge branch 'main' into feat/protected-server-routes
DanielRivers Aug 20, 2024
1aafaa9
feat: only add the access endpoint when there are Kinde route rules
DanielRivers Aug 21, 2024
95ff862
feat: add support to change access endpoint path
DanielRivers Aug 21, 2024
8bd0b2f
fix: handle redirect correctly
DanielRivers Aug 21, 2024
de3f47a
Merge branch 'main' into feat/protected-server-routes
DanielRivers Aug 29, 2024
e907547
Merge branch 'main' into feat/protected-server-routes
DanielRivers Aug 29, 2024
595a97e
Merge branch 'main' into feat/protected-server-routes
DanielRivers Sep 19, 2024
e45a4ef
Merge branch 'main' into feat/protected-server-routes
DanielRivers Sep 24, 2024
d908019
Merge branch 'main' into feat/protected-server-routes
DanielRivers Oct 19, 2024
2daf137
Merge branch 'main' into feat/protected-server-routes
DanielRivers Jan 28, 2025
af14ea7
chore: lint fix for playground nuxt config
DanielRivers Jan 28, 2025
3d2b83f
Merge remote-tracking branch 'origin/main' into feat/protected-server…
danielroe Feb 9, 2025
b990a86
chore: revert unneeded changes to playground
danielroe Feb 9, 2025
8a48866
chore: remove unneeded script block
danielroe Feb 9, 2025
e4e1509
fix: revert change to endpoint types
danielroe Feb 9, 2025
7d476c5
perf: use `entries.some`
danielroe Feb 9, 2025
e80d3d1
fix: avoid JSON.stringify + lint
danielroe Feb 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,22 @@ export default defineNuxtConfig({
compatibilityDate: '2024-08-19',
modules: ['@nuxtjs/kinde'],
devtools: { enabled: true },
routeRules: {
'/protected': {
appMiddleware: ['auth-logged-in'],
kinde: {
permissions: ['example_permission'],
redirectUrl: '/',
},
},
'/dashboard': {
appMiddleware: ['auth-logged-in'],
kinde: {
redirectUrl: '/',
},
},
},
experimental: {
inlineRouteRules: true,
},
})
4 changes: 0 additions & 4 deletions playground/pages/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@
</template>

<script setup lang="ts">
definePageMeta({
middleware: ['auth-logged-in'],
})

const client = useKindeClient()

const { data: permissions } = await useAsyncData(async () => {
Expand Down
19 changes: 19 additions & 0 deletions playground/pages/protected.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<template>
<div class="container">
<div class="card start-hero">
<p class="text-body-2 start-hero-intro">
Protected Route!
</p>
<p>Congratulations you have the correct permissions to see this page</p>
</div>

<section class="next-steps-section">
<h2 class="text-heading-1">
Next steps for you
</h2>
</section>
</div>
</template>

<script setup lang="ts">
</script>
6 changes: 6 additions & 0 deletions playground/server/api/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default defineEventHandler((event) => {
console.log(event.context.kinde)
return {
hello: 'world',
}
})
32 changes: 31 additions & 1 deletion src/module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { randomUUID } from 'node:crypto'
import { readFile, writeFile } from 'node:fs/promises'

import { addServerHandler, defineNuxtModule, addPlugin, createResolver, addRouteMiddleware, addImports, addComponent, addTemplate } from '@nuxt/kit'
import { addServerHandler, defineNuxtModule, addPlugin, createResolver, addRouteMiddleware, addImports, addComponent, addTemplate, addTypeTemplate } from '@nuxt/kit'
import { defu } from 'defu'
import type { CookieSerializeOptions } from 'cookie-es'
import { join } from 'pathe'
Expand All @@ -26,6 +26,7 @@ export interface ModuleOptions {
logout?: string
register?: string
health?: string
access?: string
}
authDomain?: string
clientId?: string
Expand Down Expand Up @@ -146,6 +147,13 @@ export default defineNuxtModule<ModuleOptions>({
|| resolver.resolve('./runtime/server/api/logout.get'),
})

addServerHandler({
route: '/api/access',
handler:
options.handlers?.access
|| resolver.resolve('./runtime/server/api/access.post'),
})
DanielRivers marked this conversation as resolved.
Show resolved Hide resolved

// Composables
addImports({ name: 'useAuth', as: 'useAuth', from: resolver.resolve('./runtime/composables') })
addImports({ name: 'useKindeClient', as: 'useKindeClient', from: resolver.resolve('./runtime/composables') })
Expand All @@ -171,5 +179,27 @@ export default defineNuxtModule<ModuleOptions>({
name: 'RegisterLink',
filePath: resolver.resolve('./runtime/components/RegisterLink'),
})

addTypeTemplate({
filename: `types/nuxt-kinde.d.ts`,
getContents: () => {
return `
interface KindeRouteRules {
permissions: string[]
redirectUrl: string
}

declare module 'nitropack' {
interface NitroRouteRules {
kinde?: KindeRouteRules
}
interface NitroRouteConfig {
kinde?: KindeRouteRules
}
}
export {}
`
},
})
},
})
62 changes: 53 additions & 9 deletions src/runtime/middleware/auth-logged-in.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,58 @@
import { abortNavigation, createError, defineNuxtRouteMiddleware, useNuxtApp } from '#imports'
import type { NitroRouteRules } from 'nitropack'
import type { AccessResponse } from '../types'
import {
abortNavigation,
createError,
defineNuxtRouteMiddleware,
navigateTo,
useNuxtApp,
getRouteRules,
} from '#imports'

function rejectNavigation(statusCode: number, message: string) {
if (import.meta.server) {
return createError({
statusCode,
message,
})
}
return abortNavigation()
}

export default defineNuxtRouteMiddleware(async (to, from) => {
if (to.path === from.path && import.meta.client) {
return
}
const nuxt = useNuxtApp()
const kindeConfig: NitroRouteRules['kinde'] = (await getRouteRules(nuxt.ssrContext?.event.path ?? '')).kinde

function denyAccess() {
if (kindeConfig?.redirectUrl) {
return navigateTo(kindeConfig.redirectUrl)
}
return rejectNavigation(401, 'You must be logged in to access this page')
}

if (import.meta.client) {
const fetchResult = await $fetch<AccessResponse>('/api/access', { method: 'POST', body: JSON.stringify({
path: to.path,
}) })
if (!fetchResult.access && fetchResult.redirectUrl) {
window.location.href = fetchResult.redirectUrl
}
return
}

export default defineNuxtRouteMiddleware(() => {
// @ts-expect-error will be fixed in Nuxt v3.13
if (!useNuxtApp().$auth.loggedIn) {
if (import.meta.server) {
return createError({
statusCode: 401,
message: 'You must be logged in to access this page',
})
if (!nuxt.$auth.loggedIn) {
return denyAccess()
}

if (kindeConfig?.permissions) {
const usersPermissions = await nuxt.ssrContext!.event.context.kinde.getPermissions()

if (!kindeConfig.permissions.some(item => usersPermissions.permissions.includes(item))) {
return denyAccess()
}
return abortNavigation()
}
})
38 changes: 38 additions & 0 deletions src/runtime/server/api/access.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { defineEventHandler, readBody } from 'h3'
import type { NitroRouteRules } from 'nitropack'
import type { AccessResponse } from '../../types'
import { useRuntimeConfig } from '#imports'

export default defineEventHandler(async (event): Promise<AccessResponse> => {
const { kinde: kindeSettings, ...rest } = useRuntimeConfig()
const body = await readBody(event)

const routeRules: NitroRouteRules['kinde'] = rest.nitro?.routeRules?.[body.path]?.kinde
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we want to use a route rules matcher here as they are nested, so a parent matcher can set permissions on a child route, e.g.:

/**: {
  kinde: {
    permissions: ['example_permission'],
  }
}

(this would apply to all routes but wouldn't be detected by this code.)


if (!routeRules) {
return {
access: true,
}
}

const usersPermissions = await event.context.kinde.getPermissions()
const isAuthenticaded = await event.context.kinde.isAuthenticated()

if (!isAuthenticaded || (routeRules.permissions && !usersPermissions.permissions)) {
return {
access: false,
redirectUrl: routeRules.redirectUrl,
}
}

if (!routeRules.permissions?.some((item: string) => usersPermissions.permissions.includes(item))) {
return {
access: false,
redirectUrl: routeRules.redirectUrl,
}
}

return {
access: true,
}
})
5 changes: 5 additions & 0 deletions src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ export type AuthState =
{ loggedIn: true, user: UserType } |
{ loggedIn: false, user: null }

export type AccessResponse = {
access: boolean
redirectUrl?: string
}

type Slice<T extends Array<unknown>> = T extends [infer _A, ...infer B] ? B : never

export type KindeContext = {
Expand Down