From 9a27097d4fcad743ab49bbeca07a02692c0fba79 Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Fri, 12 Jun 2026 14:31:07 +0300 Subject: [PATCH 1/2] feat(blabsy): dev tooling proposal for local Firebase emulator workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Staging (w3ds-staging) is on the Spark plan and both its Firestore read quota and Storage bucket quota are currently exhausted, which blocks local dev — image uploads in particular. The client already supports emulator mode (NEXT_PUBLIC_USE_EMULATOR), but two gaps made it unusable in practice: - No way to sign in without the QR / eID-wallet flow: add a dev-only /dev-login page that accepts a Firebase custom token, hard-guarded behind isUsingEmulator so it is inert outside emulator mode. - Empty emulator DB signs out any uid without a Firestore user doc: add an api seedEmulator script that creates an auth user + users doc and mints a sign-in token. It refuses to run unless the EMULATOR_HOST env vars are set, so it can never touch staging or prod. Also ignore the emulator debug logs. Note: the Firestore emulator uses port 8080 (hardcoded in app.ts), so run the client on another port in emulator mode, e.g. `pnpm exec next dev -p 8079`. Co-Authored-By: Claude Fable 5 --- .gitignore | 2 + .../blabsy/api/src/scripts/seedEmulator.ts | 87 +++++++++++++++++++ .../blabsy/client/src/pages/dev-login.tsx | 47 ++++++++++ 3 files changed, 136 insertions(+) create mode 100644 platforms/blabsy/api/src/scripts/seedEmulator.ts create mode 100644 platforms/blabsy/client/src/pages/dev-login.tsx diff --git a/.gitignore b/.gitignore index e6c7a614a..49c0bc5b2 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,8 @@ secrets/ npm-debug.log* yarn-debug.log* yarn-error.log* +firebase-debug.log +firestore-debug.log # Misc .DS_Store diff --git a/platforms/blabsy/api/src/scripts/seedEmulator.ts b/platforms/blabsy/api/src/scripts/seedEmulator.ts new file mode 100644 index 000000000..11bc81ac3 --- /dev/null +++ b/platforms/blabsy/api/src/scripts/seedEmulator.ts @@ -0,0 +1,87 @@ +/** + * Seed the local Firebase emulators with a dev user so you can sign in + * without the QR / eID-wallet flow. 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=/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= + */ +import { initializeApp, cert, getApps } from "firebase-admin/app"; +import { getAuth } from "firebase-admin/auth"; +import { getFirestore, Timestamp } from "firebase-admin/firestore"; +import * as fs from "fs"; + +const UID = "devuser01"; +const USERNAME = "devuser"; + +async function main(): Promise { + 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(); + + // Auth user (idempotent) + try { + await auth.createUser({ uid: UID, email: `${USERNAME}@example.com`, displayName: "Dev User" }); + 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. + const now = Timestamp.now(); + await db.collection("users").doc(UID).set({ + id: UID, + bio: null, + name: "Dev User", + theme: null, + accent: null, + website: null, + location: null, + username: USERNAME, + photoURL: "/assets/twitter-avatar.jpg", + verified: false, + following: [], + followers: [], + createdAt: now, + updatedAt: null, + totalTweets: 0, + totalPhotos: 0, + pinnedTweet: null, + coverPhotoURL: null + }); + console.log(`Wrote users/${UID}`); + + const token = await auth.createCustomToken(UID); + console.log("\nCustom token (sign in at http://localhost:8079/dev-login?token=):\n"); + console.log(token); +} + +main().then(() => process.exit(0)).catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/platforms/blabsy/client/src/pages/dev-login.tsx b/platforms/blabsy/client/src/pages/dev-login.tsx new file mode 100644 index 000000000..4f23c8e29 --- /dev/null +++ b/platforms/blabsy/client/src/pages/dev-login.tsx @@ -0,0 +1,47 @@ +'use client'; + +import { useEffect, 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= + * + * Disabled unless running against the Firebase emulators. + */ +export default function DevLogin(): JSX.Element { + const { signInWithCustomToken, user } = useAuth(); + const router = useRouter(); + const [error, setError] = useState(null); + + useEffect(() => { + 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); + }, [signInWithCustomToken]); + + useEffect(() => { + if (user) void router.push('/home'); + }, [user, router]); + + return ( +
+ {error ? ( +

{error}

+ ) : ( +

Signing in…

+ )} +
+ ); +} From cea8196c0ce9c95b971e649c0afa5227af8fe60d Mon Sep 17 00:00:00 2001 From: Bekiboo Date: Mon, 15 Jun 2026 10:21:05 +0300 Subject: [PATCH 2/2] feat(blabsy): seed a second user + chat, and make dev-login sign in once Extends the emulator dev tooling so messaging flows are testable: - seedEmulator: create two dev users and a chat between them (so the messages screen is reachable), via a reusable seedUser helper. The chat is left empty to mirror a fresh "No messages yet" conversation. - dev-login: sign in exactly once. signInWithCustomToken changes identity every render and a failed attempt re-renders via setError, which caused an infinite retry loop on an invalid token; a ref guard fixes it while keeping the dependency listed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../blabsy/api/src/scripts/seedEmulator.ts | 100 +++++++++++------- .../blabsy/client/src/pages/dev-login.tsx | 9 +- 2 files changed, 71 insertions(+), 38 deletions(-) diff --git a/platforms/blabsy/api/src/scripts/seedEmulator.ts b/platforms/blabsy/api/src/scripts/seedEmulator.ts index 11bc81ac3..8d4d75a0c 100644 --- a/platforms/blabsy/api/src/scripts/seedEmulator.ts +++ b/platforms/blabsy/api/src/scripts/seedEmulator.ts @@ -1,6 +1,7 @@ /** - * Seed the local Firebase emulators with a dev user so you can sign in - * without the QR / eID-wallet flow. Local-dev only. + * 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: @@ -14,12 +15,55 @@ * http://localhost:8079/dev-login?token= */ import { initializeApp, cert, getApps } from "firebase-admin/app"; -import { getAuth } from "firebase-admin/auth"; -import { getFirestore, Timestamp } from "firebase-admin/firestore"; +import { getAuth, type Auth } from "firebase-admin/auth"; +import { getFirestore, Timestamp, type Firestore } from "firebase-admin/firestore"; import * as fs from "fs"; -const UID = "devuser01"; -const USERNAME = "devuser"; +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 { + // 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 { if (!process.env.FIRESTORE_EMULATOR_HOST || !process.env.FIREBASE_AUTH_EMULATOR_HOST) { @@ -42,42 +86,24 @@ async function main(): Promise { const auth = getAuth(); const db = getFirestore(); - // Auth user (idempotent) - try { - await auth.createUser({ uid: UID, email: `${USERNAME}@example.com`, displayName: "Dev User" }); - 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; - } + for (const u of USERS) await seedUser(auth, db, u); - // Firestore user doc — auth-context signs out any uid without one. + // 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("users").doc(UID).set({ - id: UID, - bio: null, - name: "Dev User", - theme: null, - accent: null, - website: null, - location: null, - username: USERNAME, - photoURL: "/assets/twitter-avatar.jpg", - verified: false, - following: [], - followers: [], + await db.collection("chats").doc(CHAT_ID).set({ + id: CHAT_ID, + participants: USERS.map((u) => u.uid), + name: CHAT_NAME, + admins: [], createdAt: now, - updatedAt: null, - totalTweets: 0, - totalPhotos: 0, - pinnedTweet: null, - coverPhotoURL: null + updatedAt: now }); - console.log(`Wrote users/${UID}`); + console.log(`Wrote chats/${CHAT_ID} (${USERS.map((u) => u.uid).join(", ")})`); - const token = await auth.createCustomToken(UID); - console.log("\nCustom token (sign in at http://localhost:8079/dev-login?token=):\n"); + 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=):\n`); console.log(token); } diff --git a/platforms/blabsy/client/src/pages/dev-login.tsx b/platforms/blabsy/client/src/pages/dev-login.tsx index 4f23c8e29..1a9fb2475 100644 --- a/platforms/blabsy/client/src/pages/dev-login.tsx +++ b/platforms/blabsy/client/src/pages/dev-login.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useRouter } from 'next/router'; import { useAuth } from '@lib/context/auth-context'; import { isUsingEmulator } from '@lib/env'; @@ -17,8 +17,15 @@ export default function DevLogin(): JSX.Element { const { signInWithCustomToken, user } = useAuth(); const router = useRouter(); const [error, setError] = useState(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;