-
Notifications
You must be signed in to change notification settings - Fork 6
feat(blabsy): dev tooling proposal for local Firebase emulator workflow #1036
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,113 @@ | ||
| /** | ||
| * Seed the local Firebase emulators with two dev users + a chat between | ||
| * them (so the messaging UI is reachable), and mint a sign-in token for | ||
| * the first user — no QR / eID-wallet flow needed. Local-dev only. | ||
| * | ||
| * Prereqs: emulators running (auth :9099, firestore :8080). | ||
| * Run from platforms/blabsy/api: | ||
| * | ||
| * FIRESTORE_EMULATOR_HOST=localhost:8080 \ | ||
| * FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 \ | ||
| * GOOGLE_APPLICATION_CREDENTIALS=<repo>/secrets/w3ds-staging-firebase-adminsdk.json \ | ||
| * pnpm exec ts-node src/scripts/seedEmulator.ts | ||
| * | ||
| * Prints a Firebase custom token. Sign in via: | ||
| * http://localhost:8079/dev-login?token=<token> | ||
| */ | ||
| import { initializeApp, cert, getApps } from "firebase-admin/app"; | ||
| import { getAuth, type Auth } from "firebase-admin/auth"; | ||
| import { getFirestore, Timestamp, type Firestore } from "firebase-admin/firestore"; | ||
| import * as fs from "fs"; | ||
|
|
||
| type SeedUser = { uid: string; username: string; displayName: string }; | ||
|
|
||
| const USERS: SeedUser[] = [ | ||
| { uid: "devuser01", username: "devuser", displayName: "Dev User" }, | ||
| { uid: "devuser02", username: "devuser2", displayName: "Dev User Two" } | ||
| ]; | ||
| const SIGN_IN_AS = "devuser01"; | ||
| const CHAT_ID = "devchat01"; | ||
| const CHAT_NAME = "New1"; | ||
|
|
||
| async function seedUser(auth: Auth, db: Firestore, { uid, username, displayName }: SeedUser): Promise<void> { | ||
| // Auth user (idempotent) | ||
| try { | ||
| await auth.createUser({ uid, email: `${username}@example.com`, displayName }); | ||
| console.log(`Created auth user ${uid}`); | ||
| } catch (e: any) { | ||
| if (e.code === "auth/uid-already-exists" || e.code === "auth/email-already-exists") { | ||
| console.log(`Auth user ${uid} already exists`); | ||
| } else throw e; | ||
| } | ||
|
|
||
| // Firestore user doc — auth-context signs out any uid without one, and the | ||
| // chat header reads the participants' docs. | ||
| await db.collection("users").doc(uid).set({ | ||
| id: uid, | ||
| bio: null, | ||
| name: displayName, | ||
| theme: null, | ||
| accent: null, | ||
| website: null, | ||
| location: null, | ||
| username, | ||
| photoURL: "/assets/twitter-avatar.jpg", | ||
| verified: false, | ||
| following: [], | ||
| followers: [], | ||
| createdAt: Timestamp.now(), | ||
| updatedAt: null, | ||
| totalTweets: 0, | ||
| totalPhotos: 0, | ||
| pinnedTweet: null, | ||
| coverPhotoURL: null | ||
| }); | ||
| console.log(`Wrote users/${uid}`); | ||
| } | ||
|
|
||
| async function main(): Promise<void> { | ||
| if (!process.env.FIRESTORE_EMULATOR_HOST || !process.env.FIREBASE_AUTH_EMULATOR_HOST) { | ||
| throw new Error( | ||
| "Refusing to run: FIRESTORE_EMULATOR_HOST and FIREBASE_AUTH_EMULATOR_HOST must be set " + | ||
| "so this only ever touches the emulators, never staging/prod." | ||
| ); | ||
| } | ||
|
|
||
| const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS; | ||
| if (!credentialsPath || !fs.existsSync(credentialsPath)) { | ||
| throw new Error("GOOGLE_APPLICATION_CREDENTIALS must point to the service-account JSON (used only to sign the custom token)."); | ||
| } | ||
| const serviceAccount = JSON.parse(fs.readFileSync(credentialsPath, "utf8")); | ||
|
|
||
| if (getApps().length === 0) { | ||
| initializeApp({ credential: cert(serviceAccount), projectId: serviceAccount.project_id }); | ||
| } | ||
|
|
||
| const auth = getAuth(); | ||
| const db = getFirestore(); | ||
|
|
||
| for (const u of USERS) await seedUser(auth, db, u); | ||
|
|
||
| // Chat between the two users so the messaging screen (and its input box) | ||
| // is reachable. Idempotent via the fixed id. Left without messages to match | ||
| // the "No messages yet" state in the report. | ||
| const now = Timestamp.now(); | ||
| await db.collection("chats").doc(CHAT_ID).set({ | ||
| id: CHAT_ID, | ||
| participants: USERS.map((u) => u.uid), | ||
| name: CHAT_NAME, | ||
| admins: [], | ||
| createdAt: now, | ||
| updatedAt: now | ||
| }); | ||
| console.log(`Wrote chats/${CHAT_ID} (${USERS.map((u) => u.uid).join(", ")})`); | ||
|
|
||
| const token = await auth.createCustomToken(SIGN_IN_AS); | ||
| console.log(`\nCustom token for ${SIGN_IN_AS} (sign in at http://localhost:8079/dev-login?token=<token>):\n`); | ||
| console.log(token); | ||
| } | ||
|
|
||
| main().then(() => process.exit(0)).catch((e) => { | ||
| console.error(e); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,54 @@ | ||||||||||
| 'use client'; | ||||||||||
|
|
||||||||||
| import { useEffect, useRef, useState } from 'react'; | ||||||||||
| import { useRouter } from 'next/router'; | ||||||||||
| import { useAuth } from '@lib/context/auth-context'; | ||||||||||
| import { isUsingEmulator } from '@lib/env'; | ||||||||||
|
|
||||||||||
| /** | ||||||||||
| * Local-dev only sign-in: bypasses the QR / eID-wallet flow by accepting a | ||||||||||
| * Firebase custom token in the URL. Mint one with the api seedEmulator script. | ||||||||||
| * | ||||||||||
| * /dev-login?token=<custom-token> | ||||||||||
| * | ||||||||||
| * Disabled unless running against the Firebase emulators. | ||||||||||
| */ | ||||||||||
| export default function DevLogin(): JSX.Element { | ||||||||||
| const { signInWithCustomToken, user } = useAuth(); | ||||||||||
| const router = useRouter(); | ||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||
| const attempted = useRef(false); | ||||||||||
|
|
||||||||||
| useEffect(() => { | ||||||||||
| // Attempt sign-in exactly once. signInWithCustomToken changes identity | ||||||||||
| // every render (and a failed attempt re-renders via setError), so the | ||||||||||
| // ref guard prevents an infinite retry loop while keeping the dep listed. | ||||||||||
| if (attempted.current) return; | ||||||||||
| attempted.current = true; | ||||||||||
|
|
||||||||||
| if (!isUsingEmulator) { | ||||||||||
| setError('dev-login is only available in emulator mode.'); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
| const token = new URLSearchParams(window.location.search).get('token'); | ||||||||||
| if (!token) { | ||||||||||
| setError('Missing ?token= parameter.'); | ||||||||||
| return; | ||||||||||
| } | ||||||||||
| void signInWithCustomToken(token); | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle errors from The promise returned by 🛡️ Proposed fix- void signInWithCustomToken(token);
+ signInWithCustomToken(token).catch((err) => {
+ setError(err.message || 'Failed to sign in with custom token.');
+ });📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||
| }, [signInWithCustomToken]); | ||||||||||
|
|
||||||||||
| useEffect(() => { | ||||||||||
| if (user) void router.push('/home'); | ||||||||||
| }, [user, router]); | ||||||||||
|
|
||||||||||
| return ( | ||||||||||
| <div className='flex h-screen items-center justify-center'> | ||||||||||
| {error ? ( | ||||||||||
| <p className='text-red-600'>{error}</p> | ||||||||||
| ) : ( | ||||||||||
| <p className='text-gray-600'>Signing in…</p> | ||||||||||
| )} | ||||||||||
| </div> | ||||||||||
| ); | ||||||||||
| } | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
router.queryinstead ofwindow.location.searchfor Next.js compatibility.Accessing query parameters via
window.location.searchcan cause SSR/hydration issues and is not idiomatic in Next.js. Userouter.query.tokeninstead, which is available during both server and client rendering.🔧 Proposed fix
Also update the dependency array:
📝 Committable suggestion
🤖 Prompt for AI Agents