Skip to content

Commit 6b34f36

Browse files
committed
feat(emulator): work without a service account
1 parent 40b92e4 commit 6b34f36

File tree

4 files changed

+79
-47
lines changed

4 files changed

+79
-47
lines changed

packages/nuxt/src/module.ts

Lines changed: 38 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@ import {
1010
} from '@nuxt/kit'
1111
// cannot import from firebase/app because the build fails, maybe a nuxt bug?
1212
import type { FirebaseApp, FirebaseOptions } from '@firebase/app-types'
13-
import type { AppOptions, App as FirebaseAdminApp } from 'firebase-admin/app'
13+
import type { App as FirebaseAdminApp } from 'firebase-admin/app'
1414
import { markRaw } from 'vue'
1515
import { consola } from 'consola'
1616
import {
1717
VueFireNuxtModuleOptions,
1818
VueFireNuxtModuleOptionsResolved,
1919
} from './module/options'
20-
import { FirebaseEmulatorsToEnable, detectEmulators } from './module/emulators'
20+
import {
21+
FirebaseEmulatorsToEnable,
22+
detectEmulators,
23+
willUseEmulators,
24+
} from './module/emulators'
2125

2226
const logger = consola.withTag('nuxt-vuefire module')
2327

@@ -47,6 +51,13 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
4751
const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
4852
const templatesDir = fileURLToPath(new URL('../templates', import.meta.url))
4953

54+
// we need this to avoid some warnings about missing credentials and ssr
55+
const hasEmulatorsEnabled = await willUseEmulators(
56+
options,
57+
resolve(nuxt.options.rootDir, 'firebase.json'),
58+
logger
59+
)
60+
5061
// to handle TimeStamp and GeoPoints objects
5162
addPlugin(resolve(runtimeDir, 'payload-plugin'))
5263

@@ -55,6 +66,8 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
5566
nuxt.options.appConfig.firebaseConfig = markRaw(options.config)
5667
nuxt.options.appConfig.vuefireOptions = markRaw(options)
5768

69+
nuxt.options.runtimeConfig.vuefire = { options }
70+
5871
nuxt.options.build.transpile.push(runtimeDir)
5972
nuxt.options.build.transpile.push(templatesDir)
6073

@@ -83,24 +96,28 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
8396
// NOTE: the order of the plugins is reversed, so we end by adding the app plugin which is used by all other
8497
// plugins
8598

86-
if (options.auth) {
87-
if (nuxt.options.ssr && !hasServiceAccount) {
88-
logger.warn(
89-
'You activated both SSR and auth but you are not providing a service account for the admin SDK. See https://vuefire.vuejs.org/nuxt/getting-started.html#configuring-the-admin-sdk.'
90-
)
91-
}
92-
93-
if (options.appCheck) {
94-
addPlugin(resolve(runtimeDir, 'app-check/plugin.client'))
95-
// TODO: ensure this is the only necessary check. Maybe we need to check if server
99+
if (options.appCheck) {
100+
addPlugin(resolve(runtimeDir, 'app-check/plugin.client'))
101+
// TODO: ensure this is the only necessary check. Maybe we need to check if server
102+
if (!hasEmulatorsEnabled) {
96103
if (hasServiceAccount) {
104+
// this is needed by the api endpoint to properly work if no service account is provided, otherwise, the projectId is within the service account
97105
addPlugin(resolve(runtimeDir, 'app-check/plugin.server'))
98106
} else if (nuxt.options.ssr) {
99107
logger.warn(
100108
'You activated both SSR and app-check but you are not providing a service account for the admin SDK. See https://vuefire.vuejs.org/nuxt/getting-started.html#configuring-the-admin-sdk.'
101109
)
102110
}
103111
}
112+
}
113+
114+
if (options.auth) {
115+
// TODO: should be fine if emulators are activated
116+
if (nuxt.options.ssr && !hasServiceAccount && !hasEmulatorsEnabled) {
117+
logger.warn(
118+
'You activated both SSR and auth but you are not providing a service account for the admin SDK. See https://vuefire.vuejs.org/nuxt/getting-started.html#configuring-the-admin-sdk.'
119+
)
120+
}
104121

105122
// this adds the VueFire plugin and handle SSR state serialization and hydration
106123
addPluginTemplate({
@@ -112,7 +129,11 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
112129
},
113130
})
114131

115-
if (options.auth && nuxt.options.ssr && hasServiceAccount) {
132+
if (
133+
options.auth &&
134+
nuxt.options.ssr &&
135+
(hasServiceAccount || hasEmulatorsEnabled)
136+
) {
116137
// Add the session handler than mints a cookie for the user
117138
addServerHandler({
118139
route: '/api/__session',
@@ -143,22 +164,9 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
143164
}
144165

145166
// Emulators must be enabled after the app is initialized but before some APIs like auth.signinWithCustomToken() are called
146-
const isEmulatorEnabled =
147-
typeof options.emulators === 'object'
148-
? options.emulators.enabled
149-
: !!options.emulators
150167

151-
if (
152-
// Disable emulators on production unless the user explicitly enables them
153-
(process.env.NODE_ENV !== 'production' ||
154-
process.env.VUEFIRE_EMULATORS) &&
155-
isEmulatorEnabled
156-
) {
157-
const emulators = await detectEmulators(
158-
options,
159-
resolve(nuxt.options.rootDir, 'firebase.json'),
160-
logger
161-
)
168+
if (hasEmulatorsEnabled) {
169+
const emulators = detectEmulators(options, hasEmulatorsEnabled, logger)
162170

163171
// expose the detected emulators to the plugins
164172
nuxt.options.runtimeConfig.public.vuefire ??= {}
@@ -189,7 +197,7 @@ export default defineNuxtModule<VueFireNuxtModuleOptions>({
189197
)
190198
}
191199

192-
if (hasServiceAccount) {
200+
if (hasServiceAccount || hasEmulatorsEnabled) {
193201
if (options.auth) {
194202
// decodes user token from cookie if any
195203
addPlugin(resolve(runtimeDir, 'auth/plugin-user-token.server'))
@@ -286,7 +294,7 @@ interface VueFireRuntimeConfig {
286294
* Options passed to the Nuxt VueFire module
287295
* @internal
288296
*/
289-
options?: VueFireNuxtModuleOptionsResolved
297+
options?: VueFireNuxtModuleOptions
290298
}
291299
}
292300

packages/nuxt/src/module/emulators.ts

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,26 @@ import stripJsonComments from 'strip-json-comments'
33
import type { ConsolaInstance } from 'consola'
44
import type { VueFireNuxtModuleOptions } from './options'
55

6-
/**
7-
* Detects the emulators to enable based on the `firebase.json` file. Returns an object of all the emulators that should
8-
* be enabled based on the `firebase.json` file and other options and environment variables.
9-
*
10-
* @param options - The module options
11-
* @param firebaseJsonPath - resolved path to the `firebase.json` file
12-
* @param logger - The logger instance
13-
*/
14-
export async function detectEmulators(
15-
{ emulators: _emulatorsOptions, auth }: VueFireNuxtModuleOptions,
6+
export async function willUseEmulators(
7+
{ emulators }: VueFireNuxtModuleOptions,
168
firebaseJsonPath: string,
179
logger: ConsolaInstance
18-
) {
10+
): Promise<FirebaseEmulatorsJSON | null> {
11+
const isEmulatorEnabled =
12+
(typeof emulators === 'object' ? emulators.enabled : !!emulators) &&
13+
// Disable emulators on production unless the user explicitly enables them
14+
(process.env.NODE_ENV !== 'production' || process.env.VUEFIRE_EMULATORS)
15+
16+
// Avoid even checking the firebase.json
17+
if (!isEmulatorEnabled) {
18+
return null
19+
}
20+
1921
const fileStats = await stat(firebaseJsonPath)
2022
if (!fileStats.isFile()) {
21-
return
23+
return null
2224
}
23-
let firebaseJson: FirebaseEmulatorsJSON
25+
let firebaseJson: FirebaseEmulatorsJSON | null = null
2426
try {
2527
firebaseJson = JSON.parse(
2628
stripJsonComments(await readFile(firebaseJsonPath, 'utf8'), {
@@ -30,9 +32,24 @@ export async function detectEmulators(
3032
} catch (err) {
3133
logger.error('Error parsing the `firebase.json` file', err)
3234
logger.error('Cannot enable Emulators')
33-
return
3435
}
3536

37+
return firebaseJson
38+
}
39+
40+
/**
41+
* Detects the emulators to enable based on the `firebase.json` file. Returns an object of all the emulators that should
42+
* be enabled based on the `firebase.json` file and other options and environment variables.
43+
*
44+
* @param options - The module options
45+
* @param firebaseJsonPath - resolved path to the `firebase.json` file
46+
* @param logger - The logger instance
47+
*/
48+
export function detectEmulators(
49+
{ emulators: _emulatorsOptions, auth }: VueFireNuxtModuleOptions,
50+
firebaseJson: FirebaseEmulatorsJSON,
51+
logger: ConsolaInstance
52+
) {
3653
// normalize the emulators option
3754
const emulatorsOptions =
3855
typeof _emulatorsOptions === 'object'

packages/nuxt/src/runtime/auth/api.session-verification.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@ import {
99
} from 'h3'
1010
import { getAdminApp } from 'vuefire/server'
1111
import { logger } from '../logging'
12+
import { useRuntimeConfig } from '#imports'
1213

1314
/**
1415
* Setups an API endpoint to be used by the client to mint a cookie based auth session.
1516
*/
1617
export default defineEventHandler(async (event) => {
1718
assertMethod(event, 'POST')
1819
const { token } = await readBody<{ token?: string }>(event)
20+
const { vuefire } = useRuntimeConfig()
1921

20-
logger.debug('Getting the admin app')
21-
const adminApp = getAdminApp(undefined, 'session-verification')
22+
const adminApp = getAdminApp(
23+
{
24+
projectId: vuefire?.options?.config?.projectId,
25+
...vuefire?.options?.admin?.options,
26+
},
27+
'session-verification'
28+
)
2229
const adminAuth = getAdminAuth(adminApp)
2330

2431
logger.debug(token ? 'Verifying the token' : 'Deleting the session cookie')

src/auth/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { FirebaseApp } from 'firebase/app'
2-
import { getAuth, initializeAuth, type User } from 'firebase/auth'
2+
import { getAuth, type User } from 'firebase/auth'
33
import { App, ref } from 'vue-demi'
44
import { useFirebaseApp } from '../app'
55
import { getGlobalScope } from '../globals'

0 commit comments

Comments
 (0)