ieeeucsd-org/src/scripts/database/AuthSyncService.ts
2025-03-01 17:32:13 -08:00

324 lines
8.7 KiB
TypeScript

import { Authentication } from "../pocketbase/Authentication";
import { DataSyncService } from "./DataSyncService";
import { DexieService } from "./DexieService";
import { Collections } from "../../schemas/pocketbase/schema";
import { SendLog } from "../pocketbase/SendLog";
// Check if we're in a browser environment
const isBrowser =
typeof window !== "undefined" && typeof window.indexedDB !== "undefined";
/**
* Service to handle data synchronization during authentication flows
*/
export class AuthSyncService {
private static instance: AuthSyncService;
private auth: Authentication;
private dataSync: DataSyncService;
private dexieService: DexieService;
private logger: SendLog;
private isSyncing: boolean = false;
private syncErrors: Record<string, Error> = {};
private syncQueue: string[] = [];
private syncPromise: Promise<void> | null = null;
// Collections to sync on login
private readonly collectionsToSync = [
Collections.USERS,
Collections.EVENTS,
Collections.EVENT_REQUESTS,
Collections.LOGS,
Collections.OFFICERS,
Collections.REIMBURSEMENTS,
Collections.RECEIPTS,
Collections.SPONSORS,
];
private constructor() {
this.auth = Authentication.getInstance();
this.dataSync = DataSyncService.getInstance();
this.dexieService = DexieService.getInstance();
this.logger = SendLog.getInstance();
// Listen for auth state changes only in browser
if (isBrowser) {
this.auth.onAuthStateChange(this.handleAuthStateChange.bind(this));
}
}
/**
* Get the singleton instance of AuthSyncService
*/
public static getInstance(): AuthSyncService {
if (!AuthSyncService.instance) {
AuthSyncService.instance = new AuthSyncService();
}
return AuthSyncService.instance;
}
/**
* Handle authentication state changes
*/
private async handleAuthStateChange(isAuthenticated: boolean): Promise<void> {
if (!isBrowser) return;
if (isAuthenticated) {
// User just logged in
await this.handleLogin();
} else {
// User just logged out
await this.handleLogout();
}
}
/**
* Handle login by syncing user data
*/
public async handleLogin(): Promise<boolean> {
if (!isBrowser) return true;
if (this.isSyncing) {
console.log("Sync already in progress, queueing login sync");
if (this.syncPromise) {
this.syncPromise = this.syncPromise.then(() => this.performLoginSync());
} else {
this.syncPromise = this.performLoginSync();
}
return true;
}
this.syncPromise = this.performLoginSync();
return this.syncPromise.then(
() => Object.keys(this.syncErrors).length === 0,
);
}
/**
* Perform the actual login sync
*/
private async performLoginSync(): Promise<void> {
if (!isBrowser) return;
if (!this.auth.isAuthenticated()) {
console.log("Not authenticated, skipping login sync");
return;
}
this.isSyncing = true;
this.syncErrors = {};
try {
console.log("Starting login sync process...");
// Display sync notification if in browser environment
this.showSyncNotification("Syncing your data...");
// Sync user-specific data first
const userId = this.auth.getUserId();
if (userId) {
// First sync the current user's data
await this.dataSync.syncCollection(
Collections.USERS,
`id = "${userId}"`,
);
// Log the sync operation
console.log("User data synchronized on login");
}
// Sync all collections in parallel with conflict resolution
await Promise.all(
this.collectionsToSync.map(async (collection) => {
try {
await this.dataSync.syncCollection(collection);
console.log(`Successfully synced ${collection}`);
} catch (error) {
console.error(`Error syncing ${collection}:`, error);
this.syncErrors[collection] = error as Error;
}
}),
);
// Verify sync was successful
const syncVerification = await this.verifySyncSuccess();
if (syncVerification.success) {
console.log("Login sync completed successfully");
this.showSyncNotification("Data sync complete!", "success");
} else {
console.warn(
"Login sync completed with issues:",
syncVerification.errors,
);
this.showSyncNotification("Some data could not be synced", "warning");
}
} catch (error) {
console.error("Error during login sync:", error);
this.showSyncNotification("Failed to sync data", "error");
} finally {
this.isSyncing = false;
// Process any queued sync operations
if (this.syncQueue.length > 0) {
const nextSync = this.syncQueue.shift();
if (nextSync === "login") {
this.handleLogin();
} else if (nextSync === "logout") {
this.handleLogout();
}
}
}
}
/**
* Handle logout by clearing user data
*/
public async handleLogout(): Promise<boolean> {
if (!isBrowser) return true;
if (this.isSyncing) {
console.log("Sync already in progress, queueing logout cleanup");
this.syncQueue.push("logout");
return true;
}
this.isSyncing = true;
try {
console.log("Starting logout cleanup process...");
// Ensure any pending changes are synced before logout
await this.syncPendingChanges();
// Clear all data from IndexedDB
await this.dexieService.clearAllData();
console.log("Logout cleanup completed successfully");
return true;
} catch (error) {
console.error("Error during logout cleanup:", error);
return false;
} finally {
this.isSyncing = false;
// Process any queued sync operations
if (this.syncQueue.length > 0) {
const nextSync = this.syncQueue.shift();
if (nextSync === "login") {
this.handleLogin();
} else if (nextSync === "logout") {
this.handleLogout();
}
}
}
}
/**
* Sync any pending changes before logout
*/
private async syncPendingChanges(): Promise<void> {
if (!isBrowser) return;
// This would be implemented if we had offline capabilities
// For now, we just log that we would sync pending changes
console.log("Checking for pending changes to sync before logout...");
// In a real implementation, this would sync any offline changes
}
/**
* Verify that sync was successful by checking data in IndexedDB
*/
private async verifySyncSuccess(): Promise<{
success: boolean;
errors: Record<string, string>;
}> {
if (!isBrowser) return { success: true, errors: {} };
const errors: Record<string, string> = {};
// Check each collection that had errors
for (const [collection, error] of Object.entries(this.syncErrors)) {
errors[collection] = error.message;
}
// Check if user data was synced properly
const userId = this.auth.getUserId();
if (userId) {
try {
const user = await this.dataSync.getItem(
Collections.USERS,
userId,
false,
);
if (!user) {
errors["user_verification"] =
"User data not found in IndexedDB after sync";
}
} catch (error) {
errors["user_verification"] =
`Error verifying user data: ${(error as Error).message}`;
}
}
return {
success: Object.keys(errors).length === 0,
errors,
};
}
/**
* Show a notification to the user about sync status
*/
private showSyncNotification(
message: string,
type: "info" | "success" | "warning" | "error" = "info",
): void {
// Only run in browser environment
if (!isBrowser) return;
// Check if toast function exists (from react-hot-toast or similar)
if (typeof window.toast === "function") {
window.toast(message, { type });
} else {
// Fallback to console
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
/**
* Force a sync of all collections
*/
public async forceSyncAll(): Promise<boolean> {
if (this.isSyncing) {
console.log("Sync already in progress, queueing full sync");
this.syncQueue.push("login"); // Reuse login sync logic
return true;
}
return this.handleLogin();
}
/**
* Check if a sync is currently in progress
*/
public isSyncInProgress(): boolean {
return this.isSyncing;
}
/**
* Get any errors from the last sync operation
*/
public getSyncErrors(): Record<string, Error> {
return { ...this.syncErrors };
}
}
// Add toast type to window for TypeScript
declare global {
interface Window {
toast?: (
message: string,
options?: { type: "info" | "success" | "warning" | "error" },
) => void;
}
}