Skip to content

Commit

Permalink
feat!: support hybrid rendering (#104)
Browse files Browse the repository at this point in the history
* feat: support hybrid rendering

* chore: fixes
  • Loading branch information
atinux committed Jun 18, 2024
1 parent 1b06908 commit f968c4d
Show file tree
Hide file tree
Showing 17 changed files with 2,990 additions and 1,606 deletions.
50 changes: 49 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ Minimalist Authentication module for Nuxt exposing Vue composables and server ut

## Features

- Support for Hybrid Rendering (SSR / CSR / SWR / Prerendering)
- Secured & sealed cookies sessions
- [OAuth Providers](#supported-oauth-providers)

## Requirements

This module only works with SSR (server-side rendering) enabled as it uses server API routes. You cannot use this module with `nuxt generate`.
This module only works with the Nuxt server running as it uses server API routes. This means that you cannot use this module with `nuxt generate`.

## Quick Setup

Expand Down Expand Up @@ -222,6 +223,53 @@ const { data } = await useFetch('/api/protected-endpoint')

> There's [an open issue](https://github.com/nuxt/nuxt/issues/24813) to include credentials in `$fetch` in Nuxt.
## Hybrid Rendering

When using [Nuxt `routeRules`](https://nuxt.com/docs/guide/concepts/rendering#hybrid-rendering) to prerender or cache your pages, Nuxt Auth Utils will not fetch the user session during prerendering but instead fetch it on the client-side (after hydration).

This is because the user session is stored in a secure cookie and cannot be accessed during prerendering.

**This means that you should not rely on the user session during prerendering.**

### `<AuthState>` component

You can use the `<AuthState>` component to safely display auth-related data in your components without worrying about the rendering mode.

One common use case if the Login button in the header:

```vue
<template>
<header>
<AuthState v-slot="{ loggedIn, clear }">
<button v-if="loggedIn" @click="clear">Logout</button>
<NuxtLink v-else to="/login">Login</NuxtLink>
</AuthState>
</header>
</template>
```

If the page is cached or prerendered, nothing will be rendered until the user session is fetched on the client-side.

You can use the `placeholder` slot to show a placeholder on server-side and while the user session is being fetched on client-side for the prerendered pages:

```vue
<template>
<header>
<AuthState>
<template #default="{ loggedIn, clear }">
<button v-if="loggedIn" @click="clear">Logout</button>
<NuxtLink v-else to="/login">Login</NuxtLink>
</template>
<template #placeholder>
<button disabled>Loading...</button>
</template>
</AuthState>
</header>
</template>
```

If you are caching your routes with `routeRules`, please make sure to use [`nitro-nightly`](https://nitro.unjs.io/guide/nightly) or Nitro >= `2.10.0` to support the client-side fetching of the user session.

## Configuration

We leverage `runtimeConfig.session` to give the defaults option to [h3 `useSession`](https://h3.unjs.io/examples/handle-session).
Expand Down
26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"test:watch": "vitest watch"
},
"dependencies": {
"@nuxt/kit": "^3.11.2",
"@nuxt/kit": "^3.12.2",
"defu": "^6.1.4",
"hookable": "^5.5.3",
"ofetch": "^1.3.4",
Expand All @@ -40,20 +40,20 @@
"uncrypto": "^0.1.3"
},
"devDependencies": {
"@iconify-json/simple-icons": "^1.1.99",
"@iconify-json/simple-icons": "^1.1.106",
"@nuxt/devtools": "latest",
"@nuxt/eslint-config": "^0.3.8",
"@nuxt/module-builder": "^0.6.0",
"@nuxt/schema": "^3.11.2",
"@nuxt/test-utils": "^3.12.1",
"@nuxt/ui": "^2.15.2",
"@nuxt/ui-pro": "^1.1.0",
"@types/node": "^20.12.7",
"@nuxt/eslint-config": "^0.3.13",
"@nuxt/module-builder": "^0.7.1",
"@nuxt/schema": "^3.12.2",
"@nuxt/test-utils": "^3.13.1",
"@nuxt/ui": "^2.17.0",
"@nuxt/ui-pro": "^1.3.0",
"@types/node": "^20.14.2",
"changelogen": "^0.5.5",
"eslint": "^9.0.0",
"nuxt": "^3.11.2",
"eslint": "^9.5.0",
"nuxt": "^3.12.2",
"typescript": "^5.4.5",
"vitest": "^1.5.0",
"vue-tsc": "^2.0.13"
"vitest": "^1.6.0",
"vue-tsc": "^2.0.21"
}
}
98 changes: 84 additions & 14 deletions playground/app.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
<script setup lang="ts">
const { loggedIn, user, session, clear } = useUserSession()
const { loggedIn, user, session, clear, fetch } = useUserSession()
const loginModal = ref(false)
const logging = ref(false)
const password = ref('')
const toast = useToast()
async function login() {
if (logging.value || !password.value) return
logging.value = true
await $fetch('/api/login', {
method: 'POST',
body: {
password: password.value,
},
})
.then(() => {
fetch()
loginModal.value = false
})
.catch((err) => {
console.log(err)
toast.add({
color: 'red',
title: err.data?.message || err.message,
})
})
logging.value = false
}
const providers = computed(() => [
{
Expand Down Expand Up @@ -87,29 +114,72 @@ const providers = computed(() => [
Nuxt Auth Utils
</template>
<template #right>
<UDropdown :items="[providers]">
<AuthState>
<UButton
v-if="!loggedIn"
size="xs"
color="gray"
@click="loginModal = true"
>
Login
</UButton>
<UDropdown :items="[providers]">
<UButton
icon="i-heroicons-chevron-down"
trailing
color="gray"
size="xs"
>
Login with
</UButton>
</UDropdown>
<UButton
icon="i-heroicons-chevron-down"
trailing
v-if="loggedIn"
color="gray"
size="xs"
@click="clear"
>
Login with
Logout
</UButton>
</UDropdown>
<UButton
v-if="loggedIn"
color="gray"
size="xs"
@click="clear"
>
Logout
</UButton>
<template #placeholder>
<UButton
size="xs"
color="gray"
disabled
>
Loading...
</UButton>
</template>
</AuthState>
</template>
</UHeader>
<UMain>
<UContainer>
<NuxtPage />
</UContainer>
</UMain>
<UDashboardModal
v-model="loginModal"
title="Login with password"
description="Use the password: 123456"
>
<form @submit.prevent="login">
<UFormGroup label="Password">
<UInput
v-model="password"
name="password"
type="password"
/>
</UFormGroup>
<UButton
type="submit"
:disabled="!password"
color="black"
class="mt-2"
>
Login
</UButton>
</form>
</UDashboardModal>
<UNotifications />
</template>
1 change: 1 addition & 0 deletions playground/auth.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
declare module '#auth-utils' {
interface User {
password?: string
spotify?: string
github?: string
google?: string
Expand Down
8 changes: 8 additions & 0 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export default defineNuxtConfig({
compatibilityDate: '2024-06-17',
extends: ['@nuxt/ui-pro'],
modules: [
'nuxt-auth-utils',
Expand All @@ -12,4 +13,11 @@ export default defineNuxtConfig({
imports: {
autoImport: true,
},
routeRules: {
'/': {
// prerender: true,
// swr: 5,
// ssr: false,
},
},
})
2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"generate": "nuxi generate"
},
"dependencies": {
"nuxt": "^3.11.2",
"nuxt": "^3.12.2",
"nuxt-auth-utils": "latest"
}
}
13 changes: 8 additions & 5 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<script setup lang="ts">
const { session } = useUserSession()
</script>

<template>
<UPageBody>
<pre>{{ session }}</pre>
<AuthState>
<template #default="{ session }">
<pre>{{ session }}</pre>
</template>
<template #placeholder>
...
</template>
</AuthState>
</UPageBody>
</template>
18 changes: 18 additions & 0 deletions playground/server/api/login.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export default eventHandler(async (event) => {
const { password } = await readBody(event)

if (password !== '123456') {
throw createError({
statusCode: 401,
message: 'Wrong password',
})
}
await setUserSession(event, {
user: {
password: 'admin',
},
loggedInAt: Date.now(),
})

return {}
})
Loading

0 comments on commit f968c4d

Please sign in to comment.