Skip to content

Commit 8b9e213

Browse files
committed
feat(firestore): added user profile sync and scan_logs analytics
- syncProfile writes email/displayName/photoURL to users/{uid} once per session - scan_logs collection logs every scan with user email for admin visibility - updated firestore.rules to allow profile writes and write-only scan_logs
1 parent ce9f341 commit 8b9e213

File tree

3 files changed

+58
-2
lines changed

3 files changed

+58
-2
lines changed

firestore.rules

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
rules_version = '2';
22
service cloud.firestore {
33
match /databases/{database}/documents {
4+
// users can read/write their own profile doc
5+
match /users/{userId} {
6+
allow read, write: if request.auth != null && request.auth.uid == userId;
7+
}
8+
49
// users can only read/write their own scan history
510
match /users/{userId}/scans/{scanId} {
611
allow read, write: if request.auth != null && request.auth.uid == userId;
712
}
13+
14+
// any authenticated user can create a scan log entry (write-only, no reads from client)
15+
match /scan_logs/{logId} {
16+
allow create: if request.auth != null;
17+
}
818
}
919
}

src/lib/stores/auth.svelte.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
updateProfile,
1414
type User
1515
} from 'firebase/auth';
16-
import { auth } from '$lib/firebase';
16+
import { auth, db } from '$lib/firebase';
17+
import { doc, setDoc, serverTimestamp } from 'firebase/firestore';
1718

1819
class AuthStore {
1920
user = $state<User | null>(null);
2021
loading = $state(true);
2122
error = $state<string | null>(null);
23+
private profileSynced = false;
2224

2325
get isAuthenticated(): boolean {
2426
return this.user !== null;
@@ -49,6 +51,7 @@ class AuthStore {
4951
onAuthStateChanged(auth, (user) => {
5052
this.user = user;
5153
this.loading = false;
54+
if (user) this.syncProfile(user);
5255
});
5356
// handle redirect result from signInWithRedirect fallback
5457
getRedirectResult(auth).catch(() => {});
@@ -131,6 +134,26 @@ class AuthStore {
131134
this.error = null;
132135
}
133136

137+
/** write user profile to users/{uid} once per session (not every page load) */
138+
private async syncProfile(user: User) {
139+
if (this.profileSynced) return;
140+
this.profileSynced = true;
141+
try {
142+
await setDoc(
143+
doc(db, 'users', user.uid),
144+
{
145+
email: user.email,
146+
displayName: user.displayName ?? null,
147+
photoURL: user.photoURL ?? null,
148+
lastLogin: serverTimestamp()
149+
},
150+
{ merge: true }
151+
);
152+
} catch {
153+
// non-critical, don't break the app
154+
}
155+
}
156+
134157
private getErrorMessage(err: unknown): string {
135158
const code = (err as { code?: string })?.code ?? '';
136159
switch (code) {

src/lib/stores/scores.svelte.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
doc,
1111
query,
1212
orderBy,
13-
limit
13+
limit,
14+
serverTimestamp
1415
} from 'firebase/firestore';
1516
import { db } from '$lib/firebase';
1617
import { authStore } from './auth.svelte';
@@ -133,6 +134,9 @@ class ScoresStore {
133134
const docRef = await addDoc(scansRef, sanitized);
134135
console.warn('[scores] saved scan to history:', docRef.id);
135136

137+
// write to top-level scan_logs for admin visibility
138+
this.writeScanLog(sanitized, uid);
139+
136140
// prune old scans beyond the cap
137141
const allScansQuery = query(scansRef, orderBy('timestamp', 'desc'));
138142
const allSnap = await getDocs(allScansQuery);
@@ -150,6 +154,25 @@ class ScoresStore {
150154
}
151155
}
152156

157+
/** log scan to top-level scan_logs collection for admin browsing */
158+
private async writeScanLog(entry: Omit<ScanHistoryEntry, 'id'>, uid: string) {
159+
try {
160+
const user = authStore.user;
161+
await addDoc(collection(db, 'scan_logs'), {
162+
uid,
163+
email: user?.email ?? null,
164+
displayName: user?.displayName ?? null,
165+
fileName: entry.fileName ?? null,
166+
mode: entry.mode,
167+
averageScore: entry.averageScore,
168+
passingCount: entry.passingCount,
169+
createdAt: serverTimestamp()
170+
});
171+
} catch {
172+
// non-critical, don't break the scan flow
173+
}
174+
}
175+
153176
async clearHistory() {
154177
if (!browser || !authStore.isAuthenticated || !authStore.user) return;
155178

0 commit comments

Comments
 (0)