From 2224d18ce878e98db68f0ee4e5c3951cc65fe77e Mon Sep 17 00:00:00 2001 From: chark1es Date: Sun, 2 Mar 2025 00:49:33 -0800 Subject: [PATCH] fix syncing --- src/components/SyncStatus.tsx | 228 ++++++++++++++++++++++++ src/scripts/database/AuthSyncService.ts | 3 + src/scripts/database/DataSyncService.ts | 72 +++++++- 3 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/components/SyncStatus.tsx diff --git a/src/components/SyncStatus.tsx b/src/components/SyncStatus.tsx new file mode 100644 index 0000000..44c64f8 --- /dev/null +++ b/src/components/SyncStatus.tsx @@ -0,0 +1,228 @@ +import React, { useEffect, useState } from 'react'; +import { DataSyncService, SyncStatus } from '../scripts/database/DataSyncService'; +import { Collections } from '../schemas/pocketbase/schema'; + +interface SyncStatusProps { + collection?: string; // Optional specific collection to show status for + showLabel?: boolean; // Whether to show the collection name + onSyncClick?: () => void; // Optional callback when sync button is clicked +} + +export const SyncStatusIndicator: React.FC = ({ + collection, + showLabel = false, + onSyncClick, +}) => { + const [status, setStatus] = useState(SyncStatus.SYNCED); + const [isLoading, setIsLoading] = useState(false); + const [autoSyncEnabled, setAutoSyncEnabled] = useState(true); + const [showMenu, setShowMenu] = useState(false); + + useEffect(() => { + const dataSyncService = DataSyncService.getInstance(); + + // Initialize auto-sync state + setAutoSyncEnabled(dataSyncService.isAutoSyncEnabled()); + + // If a specific collection is provided, get its status + if (collection) { + setStatus(dataSyncService.getSyncStatus(collection)); + + // Listen for status changes + const handleStatusChange = (col: string, newStatus: SyncStatus) => { + if (col === collection) { + setStatus(newStatus); + } + }; + + dataSyncService.onSyncStatusChange(handleStatusChange); + + return () => { + dataSyncService.offSyncStatusChange(handleStatusChange); + }; + } else { + // If no collection is specified, show an aggregate status + const checkAllStatuses = () => { + const allStatuses = Object.values(Collections).map(col => + dataSyncService.getSyncStatus(col) + ); + + if (allStatuses.includes(SyncStatus.ERROR)) { + setStatus(SyncStatus.ERROR); + } else if (allStatuses.includes(SyncStatus.OUT_OF_SYNC)) { + setStatus(SyncStatus.OUT_OF_SYNC); + } else if (allStatuses.includes(SyncStatus.CHECKING)) { + setStatus(SyncStatus.CHECKING); + } else if (allStatuses.includes(SyncStatus.OFFLINE)) { + setStatus(SyncStatus.OFFLINE); + } else { + setStatus(SyncStatus.SYNCED); + } + }; + + // Check initial status + checkAllStatuses(); + + // Listen for any status changes + const handleStatusChange = () => { + checkAllStatuses(); + }; + + Object.values(Collections).forEach(col => { + dataSyncService.onSyncStatusChange(handleStatusChange); + }); + + return () => { + Object.values(Collections).forEach(col => { + dataSyncService.offSyncStatusChange(handleStatusChange); + }); + }; + } + }, [collection]); + + const handleSync = async () => { + if (isLoading) return; + + setIsLoading(true); + const dataSyncService = DataSyncService.getInstance(); + + try { + if (collection) { + // Sync specific collection + await dataSyncService.syncCollection(collection); + } else { + // Check all collections and sync those that are out of sync + const outOfSyncCollections = await dataSyncService.checkAllCollectionsVersion(); + + for (const [col, isOutOfSync] of Object.entries(outOfSyncCollections)) { + if (isOutOfSync) { + await dataSyncService.syncCollection(col); + } + } + } + + if (onSyncClick) { + onSyncClick(); + } + } catch (error) { + console.error('Error syncing data:', error); + } finally { + setIsLoading(false); + } + }; + + const toggleAutoSync = () => { + const dataSyncService = DataSyncService.getInstance(); + const newState = !autoSyncEnabled; + dataSyncService.setAutoSync(newState); + setAutoSyncEnabled(newState); + }; + + const toggleMenu = (e: React.MouseEvent) => { + e.stopPropagation(); + setShowMenu(!showMenu); + }; + + // Close menu when clicking outside + useEffect(() => { + const handleClickOutside = () => { + if (showMenu) setShowMenu(false); + }; + + document.addEventListener('click', handleClickOutside); + return () => { + document.removeEventListener('click', handleClickOutside); + }; + }, [showMenu]); + + // Get icon and color based on status + const getStatusInfo = () => { + switch (status) { + case SyncStatus.SYNCED: + return { icon: '✓', color: 'text-green-500', text: 'Synced' }; + case SyncStatus.OUT_OF_SYNC: + return { icon: '↻', color: 'text-yellow-500', text: 'Out of sync' }; + case SyncStatus.CHECKING: + return { icon: '⟳', color: 'text-blue-500', text: 'Checking' }; + case SyncStatus.ERROR: + return { icon: '✗', color: 'text-red-500', text: 'Error' }; + case SyncStatus.OFFLINE: + return { icon: '⚡', color: 'text-gray-500', text: 'Offline' }; + default: + return { icon: '?', color: 'text-gray-500', text: 'Unknown' }; + } + }; + + const { icon, color, text } = getStatusInfo(); + + return ( +
+
+ {showLabel && collection && ( + {collection}: + )} +
+ + {icon} + + {text} +
+ +
+ + {showMenu && ( +
+
+ Auto-sync + +
+
+ +
+ )} +
+ ); +}; + +// Component to show status for all collections +export const AllCollectionsSyncStatus: React.FC = () => { + return ( +
+

Data Sync Status

+
+ {Object.values(Collections).map(collection => ( + + ))} +
+
+ ); +}; + +export default SyncStatusIndicator; \ No newline at end of file diff --git a/src/scripts/database/AuthSyncService.ts b/src/scripts/database/AuthSyncService.ts index 4af0bed..20c5862 100644 --- a/src/scripts/database/AuthSyncService.ts +++ b/src/scripts/database/AuthSyncService.ts @@ -139,6 +139,9 @@ export class AuthSyncService { }), ); + // SECURITY FIX: Purge any event codes that might have been synced + await this.dataSync.purgeEventCodes(); + // Verify sync was successful const syncVerification = await this.verifySyncSuccess(); diff --git a/src/scripts/database/DataSyncService.ts b/src/scripts/database/DataSyncService.ts index f9024db..e465316 100644 --- a/src/scripts/database/DataSyncService.ts +++ b/src/scripts/database/DataSyncService.ts @@ -180,6 +180,12 @@ export class DataSyncService { items.map(async (item) => { const existingItem = existingItemsMap.get(item.id); + // SECURITY FIX: Remove event_code from events before storing in IndexedDB + if (collection === Collections.EVENTS && 'event_code' in item) { + const { event_code, ...rest } = item as any; + item = rest as T; + } + if (existingItem) { // Check for conflicts (local changes vs server changes) const resolvedItem = await this.resolveConflict( @@ -217,6 +223,12 @@ export class DataSyncService { localItem: T, serverItem: T, ): Promise { + // SECURITY FIX: Remove event_code from events before resolving conflicts + if (collection === Collections.EVENTS && 'event_code' in serverItem) { + const { event_code, ...rest } = serverItem as any; + serverItem = rest as T; + } + // Check if there are pending offline changes for this item const pendingChanges = await this.getPendingChangesForRecord( collection, @@ -457,6 +469,17 @@ export class DataSyncService { }); } + // SECURITY FIX: Remove event_code from events before returning them + if (collection === Collections.EVENTS) { + data = data.map((item: any) => { + if ('event_code' in item) { + const { event_code, ...rest } = item; + return rest; + } + return item; + }); + } + return data as T[]; } @@ -484,8 +507,15 @@ export class DataSyncService { try { const pbItem = await this.get.getOne(collection, id); if (pbItem) { - await table.put(pbItem); - item = pbItem; + // SECURITY FIX: Remove event_code from events before storing in IndexedDB + if (collection === Collections.EVENTS && 'event_code' in pbItem) { + const { event_code, ...rest } = pbItem as any; + await table.put(rest as T); + item = rest as T; + } else { + await table.put(pbItem); + item = pbItem; + } } } catch (error) { console.error(`Error fetching ${collection} item ${id}:`, error); @@ -588,4 +618,42 @@ export class DataSyncService { return null; } } + + /** + * Purge event_code fields from events in IndexedDB for security + * This should be called on login to ensure no event codes are stored + */ + public async purgeEventCodes(): Promise { + if (!isBrowser) return; + + try { + const db = this.dexieService.getDB(); + const table = this.getTableForCollection(Collections.EVENTS); + + if (!table) { + console.error('Events table not found'); + return; + } + + // Get all events + const events = await table.toArray(); + + // Remove event_code from each event + const updatedEvents = events.map(event => { + if ('event_code' in event) { + const { event_code, ...rest } = event; + return rest; + } + return event; + }); + + // Clear the table and add the updated events + await table.clear(); + await table.bulkAdd(updatedEvents); + + console.log('Successfully purged event codes from IndexedDB'); + } catch (error) { + console.error('Error purging event codes:', error); + } + } }