Skip to content

Commit 4e55007

Browse files
Bekibooclaude
andauthored
feat(blabsy): dev tooling proposal for local Firebase emulator workflow (#1036)
* feat(blabsy): dev tooling proposal for local Firebase emulator workflow 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 <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 5f08b54 commit 4e55007

3 files changed

Lines changed: 169 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ secrets/
3939
npm-debug.log*
4040
yarn-debug.log*
4141
yarn-error.log*
42+
firebase-debug.log
43+
firestore-debug.log
4244

4345
# Misc
4446
.DS_Store
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Seed the local Firebase emulators with two dev users + a chat between
3+
* them (so the messaging UI is reachable), and mint a sign-in token for
4+
* the first user — no QR / eID-wallet flow needed. Local-dev only.
5+
*
6+
* Prereqs: emulators running (auth :9099, firestore :8080).
7+
* Run from platforms/blabsy/api:
8+
*
9+
* FIRESTORE_EMULATOR_HOST=localhost:8080 \
10+
* FIREBASE_AUTH_EMULATOR_HOST=localhost:9099 \
11+
* GOOGLE_APPLICATION_CREDENTIALS=<repo>/secrets/w3ds-staging-firebase-adminsdk.json \
12+
* pnpm exec ts-node src/scripts/seedEmulator.ts
13+
*
14+
* Prints a Firebase custom token. Sign in via:
15+
* http://localhost:8079/dev-login?token=<token>
16+
*/
17+
import { initializeApp, cert, getApps } from "firebase-admin/app";
18+
import { getAuth, type Auth } from "firebase-admin/auth";
19+
import { getFirestore, Timestamp, type Firestore } from "firebase-admin/firestore";
20+
import * as fs from "fs";
21+
22+
type SeedUser = { uid: string; username: string; displayName: string };
23+
24+
const USERS: SeedUser[] = [
25+
{ uid: "devuser01", username: "devuser", displayName: "Dev User" },
26+
{ uid: "devuser02", username: "devuser2", displayName: "Dev User Two" }
27+
];
28+
const SIGN_IN_AS = "devuser01";
29+
const CHAT_ID = "devchat01";
30+
const CHAT_NAME = "New1";
31+
32+
async function seedUser(auth: Auth, db: Firestore, { uid, username, displayName }: SeedUser): Promise<void> {
33+
// Auth user (idempotent)
34+
try {
35+
await auth.createUser({ uid, email: `${username}@example.com`, displayName });
36+
console.log(`Created auth user ${uid}`);
37+
} catch (e: any) {
38+
if (e.code === "auth/uid-already-exists" || e.code === "auth/email-already-exists") {
39+
console.log(`Auth user ${uid} already exists`);
40+
} else throw e;
41+
}
42+
43+
// Firestore user doc — auth-context signs out any uid without one, and the
44+
// chat header reads the participants' docs.
45+
await db.collection("users").doc(uid).set({
46+
id: uid,
47+
bio: null,
48+
name: displayName,
49+
theme: null,
50+
accent: null,
51+
website: null,
52+
location: null,
53+
username,
54+
photoURL: "/assets/twitter-avatar.jpg",
55+
verified: false,
56+
following: [],
57+
followers: [],
58+
createdAt: Timestamp.now(),
59+
updatedAt: null,
60+
totalTweets: 0,
61+
totalPhotos: 0,
62+
pinnedTweet: null,
63+
coverPhotoURL: null
64+
});
65+
console.log(`Wrote users/${uid}`);
66+
}
67+
68+
async function main(): Promise<void> {
69+
if (!process.env.FIRESTORE_EMULATOR_HOST || !process.env.FIREBASE_AUTH_EMULATOR_HOST) {
70+
throw new Error(
71+
"Refusing to run: FIRESTORE_EMULATOR_HOST and FIREBASE_AUTH_EMULATOR_HOST must be set " +
72+
"so this only ever touches the emulators, never staging/prod."
73+
);
74+
}
75+
76+
const credentialsPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
77+
if (!credentialsPath || !fs.existsSync(credentialsPath)) {
78+
throw new Error("GOOGLE_APPLICATION_CREDENTIALS must point to the service-account JSON (used only to sign the custom token).");
79+
}
80+
const serviceAccount = JSON.parse(fs.readFileSync(credentialsPath, "utf8"));
81+
82+
if (getApps().length === 0) {
83+
initializeApp({ credential: cert(serviceAccount), projectId: serviceAccount.project_id });
84+
}
85+
86+
const auth = getAuth();
87+
const db = getFirestore();
88+
89+
for (const u of USERS) await seedUser(auth, db, u);
90+
91+
// Chat between the two users so the messaging screen (and its input box)
92+
// is reachable. Idempotent via the fixed id. Left without messages to match
93+
// the "No messages yet" state in the report.
94+
const now = Timestamp.now();
95+
await db.collection("chats").doc(CHAT_ID).set({
96+
id: CHAT_ID,
97+
participants: USERS.map((u) => u.uid),
98+
name: CHAT_NAME,
99+
admins: [],
100+
createdAt: now,
101+
updatedAt: now
102+
});
103+
console.log(`Wrote chats/${CHAT_ID} (${USERS.map((u) => u.uid).join(", ")})`);
104+
105+
const token = await auth.createCustomToken(SIGN_IN_AS);
106+
console.log(`\nCustom token for ${SIGN_IN_AS} (sign in at http://localhost:8079/dev-login?token=<token>):\n`);
107+
console.log(token);
108+
}
109+
110+
main().then(() => process.exit(0)).catch((e) => {
111+
console.error(e);
112+
process.exit(1);
113+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use client';
2+
3+
import { useEffect, useRef, useState } from 'react';
4+
import { useRouter } from 'next/router';
5+
import { useAuth } from '@lib/context/auth-context';
6+
import { isUsingEmulator } from '@lib/env';
7+
8+
/**
9+
* Local-dev only sign-in: bypasses the QR / eID-wallet flow by accepting a
10+
* Firebase custom token in the URL. Mint one with the api seedEmulator script.
11+
*
12+
* /dev-login?token=<custom-token>
13+
*
14+
* Disabled unless running against the Firebase emulators.
15+
*/
16+
export default function DevLogin(): JSX.Element {
17+
const { signInWithCustomToken, user } = useAuth();
18+
const router = useRouter();
19+
const [error, setError] = useState<string | null>(null);
20+
const attempted = useRef(false);
21+
22+
useEffect(() => {
23+
// Attempt sign-in exactly once. signInWithCustomToken changes identity
24+
// every render (and a failed attempt re-renders via setError), so the
25+
// ref guard prevents an infinite retry loop while keeping the dep listed.
26+
if (attempted.current) return;
27+
attempted.current = true;
28+
29+
if (!isUsingEmulator) {
30+
setError('dev-login is only available in emulator mode.');
31+
return;
32+
}
33+
const token = new URLSearchParams(window.location.search).get('token');
34+
if (!token) {
35+
setError('Missing ?token= parameter.');
36+
return;
37+
}
38+
void signInWithCustomToken(token);
39+
}, [signInWithCustomToken]);
40+
41+
useEffect(() => {
42+
if (user) void router.push('/home');
43+
}, [user, router]);
44+
45+
return (
46+
<div className='flex h-screen items-center justify-center'>
47+
{error ? (
48+
<p className='text-red-600'>{error}</p>
49+
) : (
50+
<p className='text-gray-600'>Signing in…</p>
51+
)}
52+
</div>
53+
);
54+
}

0 commit comments

Comments
 (0)