Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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