Skip to content

Bridge frontend to Firebase Auth + Firestore (foundation)#2

Merged
parsa-faraji merged 1 commit into
mainfrom
feat/auth-and-firestore-persistence
Apr 18, 2026
Merged

Bridge frontend to Firebase Auth + Firestore (foundation)#2
parsa-faraji merged 1 commit into
mainfrom
feat/auth-and-firestore-persistence

Conversation

@parsa-faraji

@parsa-faraji parsa-faraji commented Apr 18, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replaces mock auth + in-memory React Context with real Firebase Auth and Firestore persistence
  • Every existing button now does work that survives a page reload — joins, creates, ratings, leaves all persist
  • 3 dead modal buttons (Message Group / Add to Calendar / Leave Group) wired with real handlers
  • Forgot Password works
  • Sign-out and route protection added
  • Architecture: direct Firestore from the React client (per chosen plan); Express backend untouched

What's new

  • src/services/* — services layer (firebase init, auth, users, spots, groups, ratings, calendar, authErrors)
  • src/context/AuthContext.jsx + src/components/RequireAuth.jsx — auth provider + route guard
  • src/context/AppContext.jsx rewritten — same public hook API, internals now sourced from Firestore via onSnapshot
  • src/pages/auth/Login.jsx, Signup.jsx — real Firebase Auth, error UX, busy state, Forgot Password
  • src/components/modals/JoinModal.jsx — Message Group (mailto resolved from member uids), Add to Calendar (.ics download), Leave Group/Spot (auto-detected); group-only buttons hidden for spots
  • src/components/cards/StudySpotCard.jsx / StudySpotCardL.jsx — display computed avg rating from ratingSum / ratingCount
  • src/pages/Insights.jsx — Sign Out button at bottom + signed-in email display
  • firestore.rules — narrow field-level updates: any signed-in user may update only ratingSum/ratingCount on spots and only memberIds/members on groups (so rating aggregation and joins/leaves work); creator/owner still controls full metadata
  • scripts/seed-firestore.cjs — idempotent Node script that seeds 3 spots + 2 groups via firebase-admin

Required deploy step

The CI service account lacks serviceusage permission, so I couldn't deploy the rules from here. Please run once after merge:

firebase login
firebase deploy --only firestore:rules --project huddle-5ae58

Without this, joins/leaves/rating submissions will be rejected by the live rules.

Test plan

  • npm run typecheck clean
  • npm run build clean (87 modules, 620 kB — Firebase SDK adds ~350 kB; acceptable)
  • All 9 routes return 200 in dev
  • All new modules transform via Vite without errors
  • Seed script ran successfully, idempotent on re-run
  • (manual after merge + rules deploy) Sign up new account → redirected to /study-spots
  • Direct-nav to /study-spots while signed out → redirected to /
  • Join a group → reload → still joined (visible on Insights)
  • Create a group → reload → still in Discovery
  • Submit a rating → spot card shows updated avg → reload → persists
  • Click "Message Group" → mailto: opens with member emails
  • Click "Add to Calendar" → .ics file downloads + opens in Calendar app
  • Click "Leave Group" → modal closes, group removed from joined list
  • Click "Forgot Password" → email prompt → success message

Out of scope (deferred to next round)

  • Profile / Preferences page UI
  • Advanced filtering (noise / outlets / open late / crowdedness)
  • Reviews list under each spot
  • Map view (Google Maps preference noted)
  • Recommendations algorithm
  • Image uploads (Firebase Storage)

Note

High Risk
High risk because it replaces core client state/auth flows with live Firebase Auth + Firestore reads/writes and updates security rules, which can break sign-in, data access, or allow unintended writes if misconfigured.

Overview
Migrates the app to real Firebase-backed persistence. Adds a new src/services/* layer (firebase, auth, users, spots, groups, ratings) and rewrites AppContext to subscribe to Firestore (onSnapshot) for spots/groups/user doc/sessions, persisting joins/leaves, group creation, and rating submissions (including a transaction that updates spot ratingSum/ratingCount and writes a ratings doc).

Introduces auth and protected routing. Adds AuthContext, RequireAuth, and route updates so app pages require a signed-in user, while auth pages redirect signed-in users; Login/Signup now use Firebase Auth with improved error/busy UX and password reset, and Insights adds a sign-out control.

Updates UX and rules to match new data model. Study spot cards display computed average ratings from aggregates, JoinModal wires message/calendar/leave actions (mailto via member UIDs, .ics download, leave group/spot), firestore.rules restricts updates to membership lists and rating aggregates for non-owners, and a new idempotent scripts/seed-firestore.cjs seeds initial spots/groups data.

Reviewed by Cursor Bugbot for commit 8904d30. Bugbot is set up for automated code reviews on this repo. Configure here.

Replaces mock auth and in-memory React state with real Firebase Auth
and Firestore persistence. Every existing button now does work that
survives a page reload.

New services layer (src/services/*):
- firebase.ts: app/auth/db init from VITE_FIREBASE_* env vars
- auth.ts: signIn/signUp/signOut/resetPassword wrappers; signUp also
  creates the users/{uid} doc with empty joinedGroupIds/joinedSpotIds
- users.ts: subscribeUserDoc, joinSpot/leaveSpot, addJoinedGroup/
  removeJoinedGroup, getUserEmails (resolves uids to emails for the
  Message Group mailto: link)
- spots.ts, groups.ts: subscribe* (onSnapshot), get*, createGroup,
  joinGroup, leaveGroup
- ratings.ts: submitRating runs a Firestore transaction that writes
  the rating doc and atomically bumps spot.ratingSum / ratingCount
- calendar.ts: hand-rolled .ics generator + downloadICS for the
  Add to Calendar button (no extra deps)
- authErrors.ts: maps Firebase error codes to friendly messages

Auth wiring:
- AuthContext.jsx wraps onAuthStateChanged
- RequireAuth.jsx route guard redirects unauthenticated users to /
- App.jsx wraps the AppLayout block in RequireAuth; auth pages now
  redirect signed-in users to /study-spots
- main.jsx adds AuthProvider above AppProvider

AppContext rewrite (public API preserved so pages don't change):
- spots/groups/sessions/joinedGroups/joinedSpots now sourced from
  Firestore via onSnapshot subscriptions tied to the current user
- joinGroup/joinSpot/addGroup/addSession delegate to the services
- leaveGroup/leaveSpot added (used by JoinModal)

UI wiring:
- Login: real signIn, error UX, busy state, Forgot Password calls
  resetPassword (prompts for email if blank)
- Signup: real signUp, error UX, busy state
- JoinModal: 3 dead buttons wired:
  * Message Group -> mailto: with member emails resolved from uids
  * Add to Calendar -> downloads huddle-{name}.ics (only when
    meetingTime exists)
  * Leave Group/Spot -> calls leaveGroup or leaveSpot (auto-detects
    via memberIds/meetingTime); group-only buttons hidden for spots
- StudySpotCard / StudySpotCardL: display computed avg rating from
  ratingSum / ratingCount, falling back to seed rating
- Insights: Sign Out button at the bottom + signed-in email display

Firestore rules update:
- spots: any signed-in user may update only ratingSum + ratingCount
  (lets the rating aggregation transaction run); creator still owns
  full updates and deletes
- groups: any signed-in user may update only memberIds + members
  (lets joins and leaves work); owner still controls metadata

Seed script:
- scripts/seed-firestore.cjs (Node, idempotent) seeds the original
  3 spots and 2 groups via firebase-admin (using
  Backend/serviceAccountKey.json). Re-runs are no-ops.

Verified: npm run typecheck and npm run build pass; all 9 routes
return 200 in dev; Vite transforms every new module without errors.

Required follow-up by user:
- Deploy the updated firestore.rules:
    firebase login
    firebase deploy --only firestore:rules --project huddle-5ae58
  (CI service account here lacks serviceusage permission to deploy.)
@vercel

vercel Bot commented Apr 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
huddle Building Building Preview, Comment Apr 18, 2026 1:52am
huddle-ucb-learning Building Building Preview, Comment Apr 18, 2026 1:52am

@parsa-faraji parsa-faraji merged commit d910b2a into main Apr 18, 2026
4 of 6 checks passed
@parsa-faraji parsa-faraji deleted the feat/auth-and-firestore-persistence branch April 18, 2026 01:53

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 6 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8904d30. Configure here.

return newGroup;
const addGroup = async (data) => {
if (!user) throw new Error("Not signed in");
return svcCreateGroup(data, user.uid, user.displayName || "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

addGroup now async but caller doesn't await

High Severity

addGroup was changed from a synchronous function to async, so it now returns a Promise. The existing caller in StudyGroupCreate.jsx does const newGroup = addGroup(formData) without await, then passes the resulting Promise object to setCreatedGroup(newGroup). The CreateModal component then tries to render group.name, group.course, etc. on a Promise, producing undefined values. The create-group flow's confirmation modal is fully broken.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8904d30. Configure here.

const [userDoc, setUserDoc] = useState(null);

useEffect(() => subscribeSpots(setSpots), []);
useEffect(() => subscribeGroups(setGroups), []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Firestore string IDs break group lookup with parseInt

High Severity

groups are now sourced from Firestore via subscribeGroups, where document IDs are strings (e.g., "1"). StudyGroupInfo.jsx looks up a group with groups.find((g) => g.id === parseInt(id)), which uses strict equality to compare a string g.id against a number from parseInt. This always returns undefined, so every group info page renders "Group not found."

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8904d30. Configure here.

Comment thread firestore.rules
allow update: if isSignedIn() && (
(resource.data.createdBy is string && resource.data.createdBy == request.auth.uid)
|| request.resource.data.diff(resource.data).affectedKeys().hasOnly(['ratingSum', 'ratingCount'])
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rules allow arbitrary rating aggregate values from any user

Medium Severity

The spots update rule uses affectedKeys().hasOnly(['ratingSum', 'ratingCount']) to gate non-owner writes. This validates which fields change but not what values they're set to. Any signed-in user can set ratingSum and ratingCount to arbitrary numbers (e.g., setting ratingSum to 500 and ratingCount to 1), allowing them to manipulate any spot's displayed average rating.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8904d30. Configure here.

Comment thread firestore.rules
allow update: if isSignedIn() && (
(resource.data.ownerId is string && resource.data.ownerId == request.auth.uid)
|| request.resource.data.diff(resource.data).affectedKeys().hasOnly(['memberIds', 'members'])
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rules allow any user to alter any group's membership

Medium Severity

The groups update rule allows any signed-in user to write arbitrary values to memberIds and members on any group. There's no validation that the user is only adding or removing themselves. A malicious user could remove all members from any group or inject arbitrary user IDs into the member list.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8904d30. Configure here.

}
if (spot.rating !== undefined) return Number(spot.rating).toFixed(1);
return null;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Identical displayRating duplicated across two card components

Low Severity

The displayRating function is identically defined in both StudySpotCard.jsx and StudySpotCardL.jsx. This duplicated logic means any future change to rating display (e.g., handling edge cases or format changes) needs to be applied in two places, risking divergence.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8904d30. Configure here.

const [userDoc, setUserDoc] = useState(null);

useEffect(() => subscribeSpots(setSpots), []);
useEffect(() => subscribeGroups(setGroups), []);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spots/groups subscriptions fire before auth, permanently fail

High Severity

subscribeSpots and subscribeGroups fire unconditionally on mount with [] deps, regardless of auth state. When the app loads on the login page (no signed-in user), the Firestore onSnapshot listeners receive a PERMISSION_DENIED error because the rules require isSignedIn(). Since no error handler is provided, Firebase detaches the listeners silently. After the user logs in and navigates to the app, the effects never re-run (empty deps), so spots and groups remain empty arrays permanently.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8904d30. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant