Bridge frontend to Firebase Auth + Firestore (foundation)#2
Conversation
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.)
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 6 potential issues.
❌ 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 || ""); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 8904d30. Configure here.
| const [userDoc, setUserDoc] = useState(null); | ||
|
|
||
| useEffect(() => subscribeSpots(setSpots), []); | ||
| useEffect(() => subscribeGroups(setGroups), []); |
There was a problem hiding this comment.
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."
Reviewed by Cursor Bugbot for commit 8904d30. Configure here.
| 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']) | ||
| ); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 8904d30. Configure here.
| 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']) | ||
| ); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 8904d30. Configure here.
| } | ||
| if (spot.rating !== undefined) return Number(spot.rating).toFixed(1); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
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)
Reviewed by Cursor Bugbot for commit 8904d30. Configure here.
| const [userDoc, setUserDoc] = useState(null); | ||
|
|
||
| useEffect(() => subscribeSpots(setSpots), []); | ||
| useEffect(() => subscribeGroups(setGroups), []); |
There was a problem hiding this comment.
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.
Reviewed by Cursor Bugbot for commit 8904d30. Configure here.


Summary
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 guardsrc/context/AppContext.jsxrewritten — same public hook API, internals now sourced from Firestore viaonSnapshotsrc/pages/auth/Login.jsx,Signup.jsx— real Firebase Auth, error UX, busy state, Forgot Passwordsrc/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 spotssrc/components/cards/StudySpotCard.jsx/StudySpotCardL.jsx— display computed avg rating fromratingSum / ratingCountsrc/pages/Insights.jsx— Sign Out button at bottom + signed-in email displayfirestore.rules— narrow field-level updates: any signed-in user may update onlyratingSum/ratingCounton spots and onlymemberIds/memberson groups (so rating aggregation and joins/leaves work); creator/owner still controls full metadatascripts/seed-firestore.cjs— idempotent Node script that seeds 3 spots + 2 groups viafirebase-adminRequired deploy step
The CI service account lacks
serviceusagepermission, so I couldn't deploy the rules from here. Please run once after merge:Without this, joins/leaves/rating submissions will be rejected by the live rules.
Test plan
npm run typecheckcleannpm run buildclean (87 modules, 620 kB — Firebase SDK adds ~350 kB; acceptable)Out of scope (deferred to next round)
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 rewritesAppContextto subscribe to Firestore (onSnapshot) for spots/groups/user doc/sessions, persisting joins/leaves, group creation, and rating submissions (including a transaction that updates spotratingSum/ratingCountand writes aratingsdoc).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, andInsightsadds a sign-out control.Updates UX and rules to match new data model. Study spot cards display computed average ratings from aggregates,
JoinModalwires message/calendar/leave actions (mailto via member UIDs,.icsdownload, leave group/spot),firestore.rulesrestricts updates to membership lists and rating aggregates for non-owners, and a new idempotentscripts/seed-firestore.cjsseeds initialspots/groupsdata.Reviewed by Cursor Bugbot for commit 8904d30. Bugbot is set up for automated code reviews on this repo. Configure here.