Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ secrets/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
firebase-debug.log
firestore-debug.log

# Misc
.DS_Store
Expand Down
113 changes: 113 additions & 0 deletions platforms/blabsy/api/src/scripts/seedEmulator.ts
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);
});
54 changes: 54 additions & 0 deletions platforms/blabsy/client/src/pages/dev-login.tsx
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');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use router.query instead of window.location.search for Next.js compatibility.

Accessing query parameters via window.location.search can cause SSR/hydration issues and is not idiomatic in Next.js. Use router.query.token instead, which is available during both server and client rendering.

🔧 Proposed fix
-        const token = new URLSearchParams(window.location.search).get('token');
+        const token = router.query.token as string | undefined;
         if (!token) {
             setError('Missing ?token= parameter.');
             return;

Also update the dependency array:

-    }, [signInWithCustomToken]);
+    }, [signInWithCustomToken, router.query.token]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const token = new URLSearchParams(window.location.search).get('token');
const token = router.query.token as string | undefined;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platforms/blabsy/client/src/pages/dev-login.tsx` at line 26, Replace the
direct window.location.search usage with Next.js router query access: import and
use useRouter(), then read the token from router.query.token instead of new
URLSearchParams(window.location.search).get('token'); update any
useEffect/useMemo that depends on the token (e.g., the effect where token is
read) to include router.query.token (or router) in its dependency array so it
reacts when the query becomes available; ensure references to token elsewhere
use the router-derived value and remove the window.location import/usage.

if (!token) {
setError('Missing ?token= parameter.');
return;
}
void signInWithCustomToken(token);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle errors from signInWithCustomToken.

The promise returned by signInWithCustomToken is discarded with void, so any errors thrown or rejected will not update the error state. Users will be stuck seeing "Signing in…" indefinitely if authentication fails.

🛡️ Proposed fix
-        void signInWithCustomToken(token);
+        signInWithCustomToken(token).catch((err) => {
+            setError(err.message || 'Failed to sign in with custom token.');
+        });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void signInWithCustomToken(token);
signInWithCustomToken(token).catch((err) => {
setError(err.message || 'Failed to sign in with custom token.');
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@platforms/blabsy/client/src/pages/dev-login.tsx` at line 31, The call to
signInWithCustomToken currently discards the returned promise (void
signInWithCustomToken(token)), so authentication errors never update UI; change
this to properly await or attach a catch to signInWithCustomToken(token) and on
error call the component's error state updater (e.g., setError) and clear any
"signing in" flag (e.g., setIsSigningIn(false)); ensure successful resolution
continues the normal flow and failures set a descriptive error message so the UI
doesn't stay stuck on "Signing in…".

}, [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>
);
}
Loading