Skip to content

Commit 1aa5728

Browse files
committed
wip(nuxt): auth plugin
1 parent a8c043f commit 1aa5728

File tree

7 files changed

+154
-34
lines changed

7 files changed

+154
-34
lines changed

packages/nuxt/playground/app.vue

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<script lang="ts" setup>
2+
import { computed } from 'vue'
3+
import { useRouter } from 'vue-router'
4+
5+
const router = useRouter()
6+
const routes = router
7+
.getRoutes()
8+
// remove dynamic routes
9+
.filter(route => route.path !== '/' && !route.path.includes(':') && !route.children.length)
10+
.map((route) => {
11+
return {
12+
to: route.name ? { name: route.name } : route.path,
13+
label: route.path,
14+
}
15+
}).sort((a, b) => a.label.localeCompare(b.label))
16+
</script>
17+
18+
<template>
19+
<div>
20+
<nav class="nav-links">
21+
<ul>
22+
<li v-for="route in routes">
23+
<NuxtLink :to="route.to">
24+
{{ route.label }}
25+
</NuxtLink>
26+
</li>
27+
</ul>
28+
</nav>
29+
30+
<hr>
31+
32+
<NuxtPage />
33+
</div>
34+
</template>
35+
36+
<style scoped>
37+
.nav-links {
38+
margin: 0;
39+
padding: 0;
40+
}
41+
42+
.nav-links > ul {
43+
display: flex;
44+
list-style: none;
45+
flex-direction: row;
46+
align-items: center;
47+
padding: 0;
48+
margin: 0;
49+
}
50+
51+
.nav-links > ul > li {
52+
margin: 0 0.5rem;
53+
}
54+
</style>
Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,8 @@
11
<script setup lang="ts">
2-
const router = useRouter()
3-
4-
const routeRecords = router
5-
.getRoutes()
6-
// remove dynamic routes
7-
.filter(route => route.path !== '/' && !route.path.includes(':'))
8-
.map((route) => {
9-
return {
10-
to: route.name ? { name: route.name } : route.path,
11-
label: route.path,
12-
}
13-
}).sort((a, b) => a.label.localeCompare(b.label))
142
</script>
153

164
<template>
175
<div>
18-
<ul>
19-
<li v-for="route in routeRecords" :key="route.label">
20-
<NuxtLink :to="route.to">
21-
{{ route.label }}
22-
</NuxtLink>
23-
</li>
24-
</ul>
6+
<h1>Select one of the links above</h1>
257
</div>
268
</template>

packages/nuxt/src/auth/session.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { readBody, setCookie, assertMethod, defineEventHandler, H3Response, setHeader } from 'h3'
2+
import { useRuntimeConfig } from '#app'
3+
4+
export default defineEventHandler(async (event) => {
5+
assertMethod(event, 'POST')
6+
const { token } = await readBody(event)
7+
8+
// console.log('💚 updating token', token)
9+
10+
if (token) {
11+
setCookie(event, AUTH_COOKIE_NAME, token, {
12+
maxAge: AUTH_COOKIE_MAX_AGE,
13+
secure: true,
14+
httpOnly: true,
15+
path: '/',
16+
sameSite: 'lax'
17+
})
18+
// empty content status
19+
} else {
20+
// delete the cookie
21+
setCookie(event, AUTH_COOKIE_NAME, '', {
22+
maxAge: -1,
23+
path: '/'
24+
})
25+
}
26+
27+
// empty response
28+
event.node.res.statusCode = 204
29+
return ''
30+
})
31+
32+
export const AUTH_COOKIE_MAX_AGE = 60 * 60 * 24 * 5 * 1_000
33+
export const AUTH_COOKIE_NAME = '_vuefire_auth'

packages/nuxt/src/module.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { normalize } from 'node:path'
33
import {
44
addPlugin,
55
addPluginTemplate,
6+
addServerHandler,
67
createResolver,
78
defineNuxtModule,
89
} from '@nuxt/kit'
@@ -84,6 +85,8 @@ const VueFire: NuxtModule<VueFireNuxtModuleOptions> =
8485
}
8586

8687
const { resolve } = createResolver(import.meta.url)
88+
// TODO: refactor folder structure to be domain based (e.g. auth, firestore, etc.)
89+
const authDir = fileURLToPath(new URL('./auth', import.meta.url))
8790
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
8891
const templatesDir = fileURLToPath(
8992
new URL('../templates', import.meta.url)
@@ -95,6 +98,8 @@ const VueFire: NuxtModule<VueFireNuxtModuleOptions> =
9598

9699
// nuxt.options.build.transpile.push(templatesDir)
97100
nuxt.options.build.transpile.push(runtimeDir)
101+
nuxt.options.build.transpile.push(authDir)
102+
98103
// FIXME: this is a workaround because of the resolve issue with firebase
99104
// without this, we use different firebase packages within vuefire and nuxt-vuefire
100105
nuxt.options.build.transpile.push('vuefire')
@@ -112,10 +117,18 @@ const VueFire: NuxtModule<VueFireNuxtModuleOptions> =
112117
}
113118
}
114119

120+
if (nuxt.options.ssr) {
121+
addServerHandler({
122+
route: '/api/_vuefire/auth',
123+
handler: resolve(authDir, 'session'),
124+
})
125+
}
126+
115127
nuxt.hook('modules:done', () => {
116-
addPlugin(resolve(runtimeDir, 'plugins/admin.server'))
128+
// must be added after the admin module to use the admin app
129+
addPlugin(resolve(runtimeDir, 'plugins/auth'))
117130

118-
addPlugin(resolve(runtimeDir, 'plugins/auth.client'))
131+
addPlugin(resolve(runtimeDir, 'plugins/admin.server'))
119132

120133
// plugin are added in reverse order
121134
addPluginTemplate({

packages/nuxt/src/runtime/plugins/auth.client.ts

Lines changed: 0 additions & 13 deletions
This file was deleted.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { FirebaseApp } from '@firebase/app-types'
2+
import type { App as AdminApp } from 'firebase-admin/app'
3+
import { getAuth as getAdminAuth } from 'firebase-admin/auth'
4+
import { getAuth, onIdTokenChanged, signInWithCredential, AuthCredential } from 'firebase/auth'
5+
import { getCurrentUser, VueFireAuth } from 'vuefire'
6+
import { getCookie } from 'h3'
7+
import { AUTH_COOKIE_NAME } from '../../auth/session'
8+
import { defineNuxtPlugin, useRequestEvent } from '#app'
9+
10+
/**
11+
* Setups VueFireAuth and automatically mints a cookie based auth session. On the server, it reads the cookie to
12+
* generate the proper auth state.
13+
*/
14+
export default defineNuxtPlugin(async (nuxtApp) => {
15+
const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp
16+
17+
if (process.server) {
18+
const event = useRequestEvent()
19+
const token = getCookie(event, AUTH_COOKIE_NAME)
20+
21+
if (token) {
22+
const firebaseApp = nuxtApp.$firebaseApp as FirebaseApp
23+
const adminApp = nuxtApp.$adminApp as AdminApp
24+
const auth = getAdminAuth(adminApp)
25+
26+
const decodedToken = await auth.verifyIdToken(token)
27+
const user = await auth.getUser(decodedToken.uid)
28+
29+
// signInWithCredential(getAuth(firebaseApp)))
30+
31+
console.log('🔥 setting user', user)
32+
33+
// provide user
34+
}
35+
} else {
36+
VueFireAuth()(firebaseApp, nuxtApp.vueApp)
37+
const auth = getAuth(firebaseApp)
38+
// send a post request to the server when auth state changes to mint a cookie
39+
onIdTokenChanged(auth, async (user) => {
40+
const jwtToken = await user?.getIdToken()
41+
// console.log('📚 updating server cookie with', jwtToken)
42+
// TODO: error handling: should we call showError() in dev only?
43+
await $fetch('/api/_vuefire/auth', {
44+
method: 'POST',
45+
// if the token is undefined, the server will delete the cookie
46+
body: { token: jwtToken },
47+
})
48+
})
49+
}
50+
})

src/auth/user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ type _UserState =
9494

9595
const initialUserMap = new WeakMap<FirebaseApp, _UserState>()
9696

97+
// TODO: add firebase app name?
9798
// @internal
9899
function _getCurrentUserState() {
99100
const firebaseApp = useFirebaseApp()

0 commit comments

Comments
 (0)