Skip to content

Commit d910b2a

Browse files
authored
Merge pull request #2 from parsa-faraji/feat/auth-and-firestore-persistence
Bridge frontend to Firebase Auth + Firestore (foundation)
2 parents f276985 + 8904d30 commit d910b2a

21 files changed

Lines changed: 935 additions & 83 deletions

firestore.rules

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,17 @@ service cloud.firestore {
1616
allow update, delete: if isOwner(userId);
1717
}
1818

19-
// Study spots: any authenticated user can read; writes restricted
19+
// Study spots: any authenticated user can read; creator manages metadata,
20+
// any signed-in user may update only the rating aggregates (ratingSum/ratingCount)
2021
match /spots/{spotId} {
2122
allow read: if isSignedIn();
2223
allow create: if isSignedIn();
23-
allow update, delete: if isSignedIn()
24+
allow update: if isSignedIn() && (
25+
(resource.data.createdBy is string && resource.data.createdBy == request.auth.uid)
26+
|| request.resource.data.diff(resource.data).affectedKeys().hasOnly(['ratingSum', 'ratingCount'])
27+
);
28+
allow delete: if isSignedIn()
29+
&& resource.data.createdBy is string
2430
&& resource.data.createdBy == request.auth.uid;
2531
}
2632

@@ -33,11 +39,17 @@ service cloud.firestore {
3339
&& resource.data.userId == request.auth.uid;
3440
}
3541

36-
// Study groups: authenticated users can read; owner manages
42+
// Study groups: authenticated users can read; owner manages metadata,
43+
// any signed-in user may update only the membership lists (memberIds/members) to join/leave
3744
match /groups/{groupId} {
3845
allow read: if isSignedIn();
3946
allow create: if isSignedIn();
40-
allow update, delete: if isSignedIn()
47+
allow update: if isSignedIn() && (
48+
(resource.data.ownerId is string && resource.data.ownerId == request.auth.uid)
49+
|| request.resource.data.diff(resource.data).affectedKeys().hasOnly(['memberIds', 'members'])
50+
);
51+
allow delete: if isSignedIn()
52+
&& resource.data.ownerId is string
4153
&& resource.data.ownerId == request.auth.uid;
4254
}
4355

scripts/seed-firestore.cjs

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/* eslint-disable */
2+
// Idempotent Firestore seed script.
3+
// Run from repo root: node scripts/seed-firestore.js
4+
// Requires Backend/serviceAccountKey.json (already gitignored).
5+
6+
const path = require('path');
7+
const fs = require('fs');
8+
const Module = require('module');
9+
10+
// firebase-admin is installed in Backend/node_modules, not root
11+
Module.globalPaths.push(path.join(__dirname, '..', 'Backend', 'node_modules'));
12+
const admin = require(path.join(__dirname, '..', 'Backend', 'node_modules', 'firebase-admin'));
13+
14+
const serviceAccountPath = path.join(__dirname, '..', 'Backend', 'serviceAccountKey.json');
15+
if (!fs.existsSync(serviceAccountPath)) {
16+
console.error(
17+
`Missing ${serviceAccountPath}.\n` +
18+
'Download a service-account key from Firebase Console > Project settings > Service accounts.\n' +
19+
'See Backend/SETUP_FIREBASE.md.',
20+
);
21+
process.exit(1);
22+
}
23+
24+
const serviceAccount = require(serviceAccountPath);
25+
admin.initializeApp({ credential: admin.credential.cert(serviceAccount) });
26+
const db = admin.firestore();
27+
28+
// Mock data sources (kept as the seed of truth)
29+
const studySpots = [
30+
{
31+
id: '1',
32+
name: 'Doe Library',
33+
location: 'On campus',
34+
description: 'Convenient, beautiful library',
35+
distance: 0.3,
36+
hours: '9AM - 9PM',
37+
noiseLevel: 'Silent',
38+
outlets: true,
39+
lighting: 'Bright',
40+
crowded: 'Low',
41+
roomType: 'Library',
42+
open: true,
43+
rating: 4.7,
44+
image: '/cat.webp',
45+
},
46+
{
47+
id: '2',
48+
name: 'MLK Student Union',
49+
location: 'On campus',
50+
description: 'Collaborative study environment',
51+
distance: 0.5,
52+
hours: '10AM - 11PM',
53+
noiseLevel: 'Medium',
54+
outlets: true,
55+
lighting: 'Medium',
56+
crowded: 'High',
57+
roomType: 'Student Center',
58+
open: true,
59+
rating: 4.1,
60+
image: '/anothercat.jpg',
61+
},
62+
{
63+
id: '3',
64+
name: 'Cafe Strada',
65+
location: 'Off campus',
66+
description: 'Great outdoor seating',
67+
distance: 0.4,
68+
hours: '8AM - 6PM',
69+
noiseLevel: 'Loud',
70+
outlets: false,
71+
lighting: 'Dim',
72+
crowded: 'Medium',
73+
roomType: 'Cafe',
74+
open: true,
75+
rating: 3.2,
76+
image: '/yetanothercat.jpg',
77+
},
78+
];
79+
80+
const studyGroups = [
81+
{
82+
id: '1',
83+
course: 'CS 61A',
84+
name: 'cs warriors',
85+
pace: 'Fast',
86+
noiseLevel: 'Medium',
87+
groupSize: 4,
88+
availability: 'Evenings',
89+
vibe: 'Focused',
90+
method: 'Practice problems',
91+
description: 'Looking for 2 more members!',
92+
creator: 'Alex',
93+
meetingTime: 'Wed, April 10, 6:00PM',
94+
meetingPlace: 'Doe Library Room 123',
95+
members: ['Alex', 'Taylor', 'Jordan'],
96+
image: '/cat.webp',
97+
},
98+
{
99+
id: '2',
100+
course: 'MATH 54',
101+
name: 'we love arun sharma',
102+
pace: 'Medium',
103+
noiseLevel: 'Quiet',
104+
groupSize: 3,
105+
availability: 'Afternoons',
106+
vibe: 'Chill',
107+
method: 'Concept discussion',
108+
description: 'Hoping to study collaboratively and meet new people!',
109+
creator: 'Jamie',
110+
meetingTime: 'Thu, April 11, 3:00PM',
111+
meetingPlace: 'Evans Hall Room 210',
112+
members: ['Jamie', 'Sam'],
113+
image: '/anothercat.jpg',
114+
},
115+
];
116+
117+
async function seedSpots() {
118+
let created = 0;
119+
let skipped = 0;
120+
for (const { id, ...data } of studySpots) {
121+
const ref = db.collection('spots').doc(id);
122+
const snap = await ref.get();
123+
if (snap.exists) {
124+
skipped++;
125+
continue;
126+
}
127+
await ref.set({
128+
...data,
129+
ratingSum: 0,
130+
ratingCount: 0,
131+
createdAt: admin.firestore.FieldValue.serverTimestamp(),
132+
});
133+
created++;
134+
}
135+
console.log(`spots: ${created} created, ${skipped} already existed`);
136+
}
137+
138+
async function seedGroups() {
139+
let created = 0;
140+
let skipped = 0;
141+
for (const { id, ...data } of studyGroups) {
142+
const ref = db.collection('groups').doc(id);
143+
const snap = await ref.get();
144+
if (snap.exists) {
145+
skipped++;
146+
continue;
147+
}
148+
await ref.set({
149+
...data,
150+
ownerId: '',
151+
memberIds: [],
152+
createdAt: admin.firestore.FieldValue.serverTimestamp(),
153+
});
154+
created++;
155+
}
156+
console.log(`groups: ${created} created, ${skipped} already existed`);
157+
}
158+
159+
(async () => {
160+
try {
161+
await seedSpots();
162+
await seedGroups();
163+
console.log('Seed complete.');
164+
process.exit(0);
165+
} catch (e) {
166+
console.error('Seed failed:', e);
167+
process.exit(1);
168+
}
169+
})();

src/App.jsx

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { Routes, Route } from "react-router-dom";
1+
import { Navigate, Routes, Route } from "react-router-dom";
22

33
import AppLayout from "./layouts/AppLayout";
44
import AuthLayout from "./layouts/AuthLayout";
5+
import RequireAuth from "./components/RequireAuth";
6+
import { useAuth } from "./context/AuthContext";
57

68
import Login from "./pages/auth/Login";
79
import Signup from "./pages/auth/Signup";
@@ -13,29 +15,38 @@ import StudyGroupInfo from "./pages/study-groups/StudyGroupInfo";
1315
import StudyGroupCreate from "./pages/study-groups/StudyGroupCreate";
1416
import Insights from "./pages/Insights";
1517

18+
function PublicOnly({ children }) {
19+
const { user, loading } = useAuth();
20+
if (loading) return null;
21+
if (user) return <Navigate to="/study-spots" replace />;
22+
return children;
23+
}
24+
1625
function App() {
1726
return (
1827
<Routes>
1928

20-
{/* Auth pages (NO nav) */}
29+
{/* Auth pages (NO nav) — redirect signed-in users to app */}
2130
<Route element={<AuthLayout />}>
22-
<Route path="/" element={<Login />} />
23-
<Route path="/signup" element={<Signup />} />
31+
<Route path="/" element={<PublicOnly><Login /></PublicOnly>} />
32+
<Route path="/signup" element={<PublicOnly><Signup /></PublicOnly>} />
2433
</Route>
2534

26-
{/* App pages (WITH nav) */}
27-
<Route element={<AppLayout />}>
28-
<Route path="/study-spots/" element={<StudySpotDiscovery />} />
29-
<Route path="/study-spots/:id" element={<StudySpotInfo />} />
30-
<Route path="/study-spots/log/:id" element={<StudySessionLog />} />
31-
<Route path="/study-groups" element={<StudyGroupDiscovery />} />
32-
<Route path="/study-groups/:id" element={<StudyGroupInfo />} />
33-
<Route path="/study-groups/create" element={<StudyGroupCreate />} />
34-
<Route path="/insights" element={<Insights />} />
35+
{/* App pages (WITH nav) — protected */}
36+
<Route element={<RequireAuth />}>
37+
<Route element={<AppLayout />}>
38+
<Route path="/study-spots/" element={<StudySpotDiscovery />} />
39+
<Route path="/study-spots/:id" element={<StudySpotInfo />} />
40+
<Route path="/study-spots/log/:id" element={<StudySessionLog />} />
41+
<Route path="/study-groups" element={<StudyGroupDiscovery />} />
42+
<Route path="/study-groups/:id" element={<StudyGroupInfo />} />
43+
<Route path="/study-groups/create" element={<StudyGroupCreate />} />
44+
<Route path="/insights" element={<Insights />} />
45+
</Route>
3546
</Route>
3647

3748
</Routes>
3849
);
3950
}
4051

41-
export default App;
52+
export default App;

src/components/RequireAuth.jsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Navigate, Outlet, useLocation } from "react-router-dom";
2+
import { useAuth } from "../context/AuthContext";
3+
4+
export default function RequireAuth() {
5+
const { user, loading } = useAuth();
6+
const location = useLocation();
7+
8+
if (loading) {
9+
return (
10+
<div className="min-h-screen flex items-center justify-center bg-white">
11+
<p className="text-black" style={{ fontFamily: "'Jost', sans-serif" }}>Loading...</p>
12+
</div>
13+
);
14+
}
15+
16+
if (!user) {
17+
return <Navigate to="/" state={{ from: location.pathname }} replace />;
18+
}
19+
20+
return <Outlet />;
21+
}

src/components/cards/StudySpotCard.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,17 @@ const crowdBadgeColor = (level) =>
3434
level === "Low" || level === "Small" ? "green" : level === "Medium" ? "orange" : "red";
3535
const ratingBadgeColor = (rating) => (rating >= 4 ? "green" : rating >= 3.0 ? "orange" : "red");
3636

37+
function displayRating(spot) {
38+
if (spot.ratingCount && spot.ratingCount > 0) {
39+
return (spot.ratingSum / spot.ratingCount).toFixed(1);
40+
}
41+
if (spot.rating !== undefined) return Number(spot.rating).toFixed(1);
42+
return null;
43+
}
44+
3745
export default function StudySpotCard({ spot }) {
3846
const navigate = useNavigate();
47+
const rating = displayRating(spot);
3948

4049
return (
4150
<div className="flex justify-center w-full">
@@ -73,10 +82,10 @@ export default function StudySpotCard({ spot }) {
7382
<Badge label={spot.open ? "Open" : "Closed"} color={spot.open ? "green" : "red"} />
7483
</div>
7584
)}
76-
{spot.rating !== undefined && (
85+
{rating !== null && (
7786
<div className="flex flex-col items-center">
7887
<span className="text-gray-500 text-[0.65rem]" style={{ fontFamily: "'Jost', sans-serif" }}>Rating</span>
79-
<Badge label={`${spot.rating}`} color={ratingBadgeColor(spot.rating)} />
88+
<Badge label={rating} color={ratingBadgeColor(Number(rating))} />
8089
</div>
8190
)}
8291
</div>

src/components/cards/StudySpotCardL.jsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,18 @@ const crowdBadgeColor = (level) => (level === "Low" ? "green" : level === "Mediu
3131
const openBadgeColor = (isOpen) => (isOpen ? "green" : "red");
3232
const ratingBadgeColor = (rating) => (rating >= 4.5 ? "green" : rating >= 4.0 ? "orange" : "red");
3333

34+
function displayRating(data) {
35+
if (data.ratingCount && data.ratingCount > 0) {
36+
return (data.ratingSum / data.ratingCount).toFixed(1);
37+
}
38+
if (data.rating !== undefined) return Number(data.rating).toFixed(1);
39+
return null;
40+
}
41+
3442
export default function StudySpotCardL({ data, buttonText = "Join", onJoin, onConfirmJoin }) {
3543
const navigate = useNavigate();
3644
const [modalOpen, setModalOpen] = useState(false);
45+
const rating = displayRating(data);
3746

3847
return (
3948
<>
@@ -75,10 +84,10 @@ export default function StudySpotCardL({ data, buttonText = "Join", onJoin, onCo
7584
<Badge label={data.open ? "Open" : "Closed"} color={openBadgeColor(data.open)} />
7685
</div>
7786
)}
78-
{data.rating !== undefined && (
87+
{rating !== null && (
7988
<div className="flex flex-col items-center">
8089
<span className="text-gray-500 text-[0.65rem]" style={{ fontFamily: "'Jost', sans-serif" }}>Rating</span>
81-
<Badge label={`${data.rating}`} color={ratingBadgeColor(data.rating)} />
90+
<Badge label={rating} color={ratingBadgeColor(Number(rating))} />
8291
</div>
8392
)}
8493
</div>

0 commit comments

Comments
 (0)