diff --git a/src/components/dashboard/EventsSection/EventCheckIn.tsx b/src/components/dashboard/EventsSection/EventCheckIn.tsx index 0a5ae58..6b3eea4 100644 --- a/src/components/dashboard/EventsSection/EventCheckIn.tsx +++ b/src/components/dashboard/EventsSection/EventCheckIn.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Get } from "../../../scripts/pocketbase/Get"; import { Authentication } from "../../../scripts/pocketbase/Authentication"; import { Update } from "../../../scripts/pocketbase/Update"; @@ -23,6 +23,14 @@ const EventCheckIn = () => { const [isLoading, setIsLoading] = useState(false); const [foodInput, setFoodInput] = useState(""); + // SECURITY FIX: Purge event codes when component mounts + useEffect(() => { + const dataSync = DataSyncService.getInstance(); + dataSync.purgeEventCodes().catch(err => { + console.error("Error purging event codes:", err); + }); + }, []); + async function handleEventCheckIn(eventCode: string): Promise { try { const get = Get.getInstance(); @@ -48,18 +56,19 @@ const EventCheckIn = () => { `User ${currentUser.id} attempted to check in with code: ${eventCode}` ); - // Find the event with the given code using IndexedDB - // Force sync to ensure we have the latest data - await dataSync.syncCollection(Collections.EVENTS, `event_code = "${eventCode}"`); + // SECURITY FIX: Instead of syncing and querying IndexedDB with the event code, + // directly query PocketBase for the event with the given code + // This prevents the event code from being stored in IndexedDB + const pb = auth.getPocketBase(); + const records = await pb.collection(Collections.EVENTS).getList(1, 1, { + filter: `event_code = "${eventCode}"`, + }); - // Get the event from IndexedDB - const events = await dataSync.getData( - Collections.EVENTS, - false, // Don't force sync again - `event_code = "${eventCode}"` - ); - - const event = events.length > 0 ? events[0] : null; + // Convert the first result to our Event type + let event: Event | null = null; + if (records.items.length > 0) { + event = Get.convertUTCToLocal(records.items[0] as unknown as Event); + } if (!event) { await logger.send( @@ -186,8 +195,11 @@ const EventCheckIn = () => { // Update attendees array with the new entry await update.updateField("events", event.id, "attendees", updatedAttendees); - // Force sync the events collection to update IndexedDB - await dataSync.syncCollection(Collections.EVENTS); + // SECURITY FIX: Instead of syncing the entire events collection which would store event codes in IndexedDB, + // only sync the user's collection to update their points + if (event.points_to_reward > 0) { + await dataSync.syncCollection(Collections.USERS); + } // If food selection was made, log it if (foodSelection) { @@ -208,9 +220,6 @@ const EventCheckIn = () => { userPoints + event.points_to_reward ); - // Force sync the users collection to update IndexedDB - await dataSync.syncCollection(Collections.USERS); - // Log the points award await logger.send( "update", @@ -313,6 +322,37 @@ const EventCheckIn = () => { + + {/* Food Selection Modal */} + +
+

Food Selection

+

This event has food! Please let us know what you'd like to eat:

+
+
+ setFoodInput(e.target.value)} + required + /> +
+
+ + +
+
+
+
+ +
+
); }; diff --git a/src/components/dashboard/ProfileSection.astro b/src/components/dashboard/ProfileSection.astro index c900812..70cc7ff 100644 --- a/src/components/dashboard/ProfileSection.astro +++ b/src/components/dashboard/ProfileSection.astro @@ -22,10 +22,16 @@ import { Stats } from "./ProfileSection/Stats"; Recent Activity +
+ Real-time updates +
+

+ Track your recent interactions with the IEEE UCSD platform +

-
+
diff --git a/src/components/dashboard/ProfileSection/ShowProfileLogs.tsx b/src/components/dashboard/ProfileSection/ShowProfileLogs.tsx index e7a87f4..27ba406 100644 --- a/src/components/dashboard/ProfileSection/ShowProfileLogs.tsx +++ b/src/components/dashboard/ProfileSection/ShowProfileLogs.tsx @@ -4,6 +4,7 @@ import { DataSyncService } from "../../../scripts/database/DataSyncService"; import { Collections } from "../../../schemas/pocketbase/schema"; import debounce from 'lodash/debounce'; import type { Log } from "../../../schemas/pocketbase"; +import { SendLog } from "../../../scripts/pocketbase/SendLog"; const LOGS_PER_PAGE = 5; @@ -17,6 +18,20 @@ export default function ShowProfileLogs() { const [searchQuery, setSearchQuery] = useState(''); const [allLogs, setAllLogs] = useState([]); const [isFetchingAll, setIsFetchingAll] = useState(false); + const [autoRefresh, setAutoRefresh] = useState(true); + + // Auto-refresh logs every 30 seconds if enabled + useEffect(() => { + if (!autoRefresh) return; + + const interval = setInterval(() => { + if (!isFetchingAll) { + fetchLogs(true); + } + }, 30000); // 30 seconds + + return () => clearInterval(interval); + }, [autoRefresh, isFetchingAll]); const fetchLogs = async (skipCache = false) => { setLoading(true); @@ -34,6 +49,7 @@ export default function ShowProfileLogs() { try { setIsFetchingAll(true); + console.log("Fetching logs for user:", userId); // Use DataSyncService to fetch logs const dataSync = DataSyncService.getInstance(); @@ -41,21 +57,49 @@ export default function ShowProfileLogs() { // First sync logs for this user await dataSync.syncCollection( Collections.LOGS, - `user_id = "${userId}"`, - "-created" + `user = "${userId}"`, + "-created", + { expand: "user" } ); // Then get all logs from IndexedDB const fetchedLogs = await dataSync.getData( Collections.LOGS, false, // Don't force sync again - `user_id = "${userId}"`, + `user = "${userId}"`, "-created" ); - setAllLogs(fetchedLogs); - setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE)); - setTotalLogs(fetchedLogs.length); + console.log("Fetched logs:", fetchedLogs.length); + + if (fetchedLogs.length === 0) { + // If no logs found, try to fetch directly from PocketBase + console.log("No logs found in IndexedDB, trying direct fetch from PocketBase"); + try { + const sendLog = SendLog.getInstance(); + const directLogs = await sendLog.getUserLogs(userId); + console.log("Direct fetch logs:", directLogs.length); + + if (directLogs.length > 0) { + setAllLogs(directLogs); + setTotalPages(Math.ceil(directLogs.length / LOGS_PER_PAGE)); + setTotalLogs(directLogs.length); + } else { + setAllLogs(fetchedLogs); + setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE)); + setTotalLogs(fetchedLogs.length); + } + } catch (directError) { + console.error("Failed to fetch logs directly:", directError); + setAllLogs(fetchedLogs); + setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE)); + setTotalLogs(fetchedLogs.length); + } + } else { + setAllLogs(fetchedLogs); + setTotalPages(Math.ceil(fetchedLogs.length / LOGS_PER_PAGE)); + setTotalLogs(fetchedLogs.length); + } } catch (error) { console.error("Failed to fetch logs:", error); setError("Error loading activity"); @@ -67,6 +111,8 @@ export default function ShowProfileLogs() { // Memoized search function const filteredLogs = useMemo(() => { + if (!allLogs.length) return []; + if (!searchQuery.trim()) { // When not searching, return only the current page of logs const startIndex = (currentPage - 1) * LOGS_PER_PAGE; @@ -78,6 +124,8 @@ export default function ShowProfileLogs() { return allLogs.filter(log => { return ( log.message?.toLowerCase().includes(query) || + log.type?.toLowerCase().includes(query) || + log.part?.toLowerCase().includes(query) || (log.created && new Date(log.created).toLocaleString().toLowerCase().includes(query)) ); }); @@ -86,6 +134,7 @@ export default function ShowProfileLogs() { // Update displayed logs whenever filtered results change useEffect(() => { setLogs(filteredLogs); + console.log("Filtered logs updated:", filteredLogs.length, "logs"); }, [filteredLogs]); // Debounced search handler @@ -121,9 +170,128 @@ export default function ShowProfileLogs() { }; useEffect(() => { - fetchLogs(); + const loadLogsWithRetry = async () => { + try { + await fetchLogs(); + + // Wait a moment for state to update + setTimeout(async () => { + // Check if logs were loaded + if (allLogs.length === 0) { + console.log("No logs found after initial fetch, trying direct fetch"); + await directFetchLogs(); + } + }, 1000); + } catch (error) { + console.error("Failed to load logs with retry:", error); + } + }; + + loadLogsWithRetry(); + checkLogsExist(); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Check if there are any logs in the database at all + const checkLogsExist = async () => { + try { + const auth = Authentication.getInstance(); + const pb = auth.getPocketBase(); + + // Check if the logs collection exists and has any records + const result = await pb.collection(Collections.LOGS).getList(1, 1); + console.log("Logs collection check:", { + totalItems: result.totalItems, + page: result.page, + perPage: result.perPage, + totalPages: result.totalPages + }); + } catch (error) { + console.error("Failed to check logs collection:", error); + } + }; + + // Calculate log statistics + const logStats = useMemo(() => { + if (!allLogs.length) return null; + + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const lastWeek = new Date(today); + lastWeek.setDate(lastWeek.getDate() - 7); + + const todayLogs = allLogs.filter(log => new Date(log.created) >= today); + const weekLogs = allLogs.filter(log => new Date(log.created) >= lastWeek); + + // Count by type + const typeCount: Record = {}; + allLogs.forEach(log => { + const type = log.type || 'unknown'; + typeCount[type] = (typeCount[type] || 0) + 1; + }); + + return { + total: allLogs.length, + today: todayLogs.length, + week: weekLogs.length, + types: typeCount + }; + }, [allLogs]); + + // Direct fetch from PocketBase as a fallback + const directFetchLogs = async () => { + try { + setLoading(true); + setError(null); + + const auth = Authentication.getInstance(); + const pb = auth.getPocketBase(); + const userId = auth.getPocketBase().authStore.model?.id; + + if (!userId) { + setError("Not authenticated"); + setLoading(false); + return; + } + + console.log("Direct fetching logs for user:", userId); + + // Fetch logs directly from PocketBase + const result = await pb.collection(Collections.LOGS).getList(1, 100, { + filter: `user = "${userId}"`, + sort: "-created", + expand: "user" + }); + + console.log("Direct fetch result:", { + totalItems: result.totalItems, + items: result.items.length + }); + + if (result.items.length > 0) { + setAllLogs(result.items); + setTotalPages(Math.ceil(result.items.length / LOGS_PER_PAGE)); + setTotalLogs(result.items.length); + } + } catch (error) { + console.error("Failed to direct fetch logs:", error); + setError("Error loading activity"); + } finally { + setLoading(false); + } + }; + + // Add a button to try direct fetch + const renderDirectFetchButton = () => ( + + ); + if (loading && !allLogs.length) { return (

@@ -146,19 +314,83 @@ export default function ShowProfileLogs() { ); } - if (logs.length === 0 && !searchQuery && !loading) { + // Debug logs + console.log("Render state:", { + logsLength: logs.length, + allLogsLength: allLogs.length, + searchQuery, + loading, + currentPage + }); + + if (allLogs.length === 0 && !searchQuery && !loading) { return ( -

- - - - No recent activity to display. -

+
+

+ + + + No recent activity to display. +

+
+ + +
+
); } return (
+ {/* Activity Summary */} + {logStats && !searchQuery && ( +
+
+
Today
+
{logStats.today}
+
Activities recorded today
+
+
+
This Week
+
{logStats.week}
+
Activities in the last 7 days
+
+
+
Total
+
{logStats.total}
+
All-time activities
+
+
+ )} + {/* Search and Refresh Controls */}
@@ -169,16 +401,35 @@ export default function ShowProfileLogs() { className="input input-bordered w-full" />
- +
+ +
    +
  • + +
  • +
  • +
+
{isFetchingAll && ( @@ -199,24 +450,27 @@ export default function ShowProfileLogs() { {logs.map((log) => (
-
- - - +
+ {getLogTypeIcon(log.type)}

{log.message}

-

- {new Date(log.created).toLocaleString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - })} -

+
+ {log.part && ( + {log.part} + )} +

+ {new Date(log.created).toLocaleString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + })} +

+
))} @@ -257,3 +511,69 @@ export default function ShowProfileLogs() {
); } + +function getLogTypeColor(type: string): string { + switch (type?.toLowerCase()) { + case 'error': + return 'bg-error/10 text-error'; + case 'update': + return 'bg-info/10 text-info'; + case 'delete': + return 'bg-warning/10 text-warning'; + case 'create': + return 'bg-success/10 text-success'; + case 'login': + case 'logout': + return 'bg-primary/10 text-primary'; + default: + return 'bg-base-300/50 text-base-content'; + } +} + +function getLogTypeIcon(type: string) { + switch (type?.toLowerCase()) { + case 'error': + return ( + + + + ); + case 'update': + return ( + + + + + ); + case 'delete': + return ( + + + + ); + case 'create': + return ( + + + + ); + case 'login': + return ( + + + + ); + case 'logout': + return ( + + + + ); + default: + return ( + + + + ); + } +}