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..8d4d75a0c --- /dev/null +++ b/platforms/blabsy/api/src/scripts/seedEmulator.ts @@ -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=/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, 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 { + // 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) { + 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=):\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..1a9fb2475 --- /dev/null +++ b/platforms/blabsy/client/src/pages/dev-login.tsx @@ -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= + * + * 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); + 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); + }, [signInWithCustomToken]); + + useEffect(() => { + if (user) void router.push('/home'); + }, [user, router]); + + return ( +
+ {error ? ( +

{error}

+ ) : ( +

Signing in…

+ )} +
+ ); +}