fix syncing
This commit is contained in:
parent
8cb711d361
commit
2224d18ce8
3 changed files with 301 additions and 2 deletions
228
src/components/SyncStatus.tsx
Normal file
228
src/components/SyncStatus.tsx
Normal file
|
@ -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<SyncStatusProps> = ({
|
||||||
|
collection,
|
||||||
|
showLabel = false,
|
||||||
|
onSyncClick,
|
||||||
|
}) => {
|
||||||
|
const [status, setStatus] = useState<SyncStatus>(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 (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{showLabel && collection && (
|
||||||
|
<span className="text-sm font-medium">{collection}:</span>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`flex items-center cursor-pointer ${isLoading ? 'opacity-50' : ''}`}
|
||||||
|
onClick={handleSync}
|
||||||
|
title={`${text}${collection ? ` - ${collection}` : ''} (Click to sync)`}
|
||||||
|
>
|
||||||
|
<span className={`${color} text-lg ${status === SyncStatus.CHECKING ? 'animate-spin' : ''}`}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
<span className="ml-1 text-sm">{text}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={toggleMenu}
|
||||||
|
className="ml-1 text-gray-500 hover:text-gray-700 focus:outline-none"
|
||||||
|
title="Sync options"
|
||||||
|
>
|
||||||
|
⚙️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showMenu && (
|
||||||
|
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg z-10 p-2">
|
||||||
|
<div className="flex items-center justify-between p-2 hover:bg-gray-100 rounded">
|
||||||
|
<span className="text-sm">Auto-sync</span>
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={autoSyncEnabled}
|
||||||
|
onChange={toggleAutoSync}
|
||||||
|
className="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div className={`w-9 h-5 rounded-full peer ${autoSyncEnabled ? 'bg-blue-600' : 'bg-gray-200'} peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all`}></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="border-t my-1"></div>
|
||||||
|
<button
|
||||||
|
onClick={handleSync}
|
||||||
|
className="w-full text-left p-2 text-sm hover:bg-gray-100 rounded"
|
||||||
|
>
|
||||||
|
Sync now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Component to show status for all collections
|
||||||
|
export const AllCollectionsSyncStatus: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Data Sync Status</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
|
{Object.values(Collections).map(collection => (
|
||||||
|
<SyncStatusIndicator
|
||||||
|
key={collection}
|
||||||
|
collection={collection}
|
||||||
|
showLabel={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SyncStatusIndicator;
|
|
@ -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
|
// Verify sync was successful
|
||||||
const syncVerification = await this.verifySyncSuccess();
|
const syncVerification = await this.verifySyncSuccess();
|
||||||
|
|
||||||
|
|
|
@ -180,6 +180,12 @@ export class DataSyncService {
|
||||||
items.map(async (item) => {
|
items.map(async (item) => {
|
||||||
const existingItem = existingItemsMap.get(item.id);
|
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) {
|
if (existingItem) {
|
||||||
// Check for conflicts (local changes vs server changes)
|
// Check for conflicts (local changes vs server changes)
|
||||||
const resolvedItem = await this.resolveConflict(
|
const resolvedItem = await this.resolveConflict(
|
||||||
|
@ -217,6 +223,12 @@ export class DataSyncService {
|
||||||
localItem: T,
|
localItem: T,
|
||||||
serverItem: T,
|
serverItem: T,
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
// 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
|
// Check if there are pending offline changes for this item
|
||||||
const pendingChanges = await this.getPendingChangesForRecord(
|
const pendingChanges = await this.getPendingChangesForRecord(
|
||||||
collection,
|
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[];
|
return data as T[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -484,8 +507,15 @@ export class DataSyncService {
|
||||||
try {
|
try {
|
||||||
const pbItem = await this.get.getOne<T>(collection, id);
|
const pbItem = await this.get.getOne<T>(collection, id);
|
||||||
if (pbItem) {
|
if (pbItem) {
|
||||||
await table.put(pbItem);
|
// SECURITY FIX: Remove event_code from events before storing in IndexedDB
|
||||||
item = pbItem;
|
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) {
|
} catch (error) {
|
||||||
console.error(`Error fetching ${collection} item ${id}:`, error);
|
console.error(`Error fetching ${collection} item ${id}:`, error);
|
||||||
|
@ -588,4 +618,42 @@ export class DataSyncService {
|
||||||
return null;
|
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<void> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue