import { v4 as uuidv4 } from 'uuid'; import { User, Medication, UserSettings, TakenDoses, CustomReminder, CouchDBDocument, UserRole, } from '../types'; import { AccountStatus } from './auth/auth.constants'; // This is a mock CouchDB service that uses localStorage for persistence. // It mimics the async nature of a real database API and includes robust error handling and conflict resolution. const latency = () => new Promise(res => setTimeout(res, Math.random() * 200 + 50)); export class CouchDBError extends Error { status: number; constructor(message: string, status: number) { super(message); this.name = 'CouchDBError'; this.status = status; } } class CouchDBService { private async getDb(dbName: string): Promise { await latency(); const db = localStorage.getItem(dbName); return db ? JSON.parse(db) : []; } private async saveDb(dbName: string, data: T[]): Promise { await latency(); localStorage.setItem(dbName, JSON.stringify(data)); } private async getDoc( dbName: string, id: string ): Promise { const allDocs = await this.getDb(dbName); return allDocs.find(doc => doc._id === id) || null; } private async query( dbName: string, predicate: (doc: T) => boolean ): Promise { const allDocs = await this.getDb(dbName); return allDocs.filter(predicate); } private async putDoc( dbName: string, doc: Omit & { _rev?: string } ): Promise { const allDocs = await this.getDb(dbName); const docIndex = allDocs.findIndex(d => d._id === doc._id); if (docIndex > -1) { // Update const existingDoc = allDocs[docIndex]; if (existingDoc._rev !== doc._rev) { throw new CouchDBError('Document update conflict', 409); } const newRev = parseInt(existingDoc._rev.split('-')[0], 10) + 1; const updatedDoc = { ...doc, _rev: `${newRev}-${Math.random().toString(36).substr(2, 9)}`, } as T; allDocs[docIndex] = updatedDoc; await this.saveDb(dbName, allDocs); return updatedDoc; } else { // Create const newDoc = { ...doc, _rev: `1-${Math.random().toString(36).substr(2, 9)}`, } as T; allDocs.push(newDoc); await this.saveDb(dbName, allDocs); return newDoc; } } private async deleteDoc( dbName: string, doc: T ): Promise { let docs = await this.getDb(dbName); const docIndex = docs.findIndex(d => d._id === doc._id); if (docIndex > -1) { if (docs[docIndex]._rev !== doc._rev) { throw new CouchDBError('Document update conflict', 409); } docs = docs.filter(m => m._id !== doc._id); await this.saveDb(dbName, docs); } else { throw new CouchDBError('Document not found', 404); } } // Generic update function with conflict resolution private async updateDocWithConflictResolution( dbName: string, doc: T, mergeFn?: (latest: T, incoming: T) => T ): Promise { try { return await this.putDoc(dbName, doc); } catch (error) { if (error instanceof CouchDBError && error.status === 409) { console.warn( `Conflict detected for doc ${doc._id}. Attempting to resolve.` ); const latestDoc = await this.getDoc(dbName, doc._id); if (latestDoc) { // Default merge: incoming changes overwrite latest const defaultMerge = { ...latestDoc, ...doc, _rev: latestDoc._rev }; const mergedDoc = mergeFn ? mergeFn(latestDoc, doc) : defaultMerge; // Retry the update with the latest revision and merged data return this.putDoc(dbName, mergedDoc); } } // Re-throw if it's not a resolvable conflict or fetching the latest doc fails throw error; } } // --- User Management --- async findUserByUsername(username: string): Promise { const users = await this.query( 'users', u => u.username.toLowerCase() === username.toLowerCase() ); return users[0] || null; } async findUserByEmail(email: string): Promise { const users = await this.query( 'users', u => u.email?.toLowerCase() === email.toLowerCase() ); return users[0] || null; } async createUser(username: string): Promise { if (await this.findUserByUsername(username)) { throw new CouchDBError('User already exists', 409); } const newUser: Omit = { _id: uuidv4(), username }; return this.putDoc('users', newUser); } async createUserWithPassword( email: string, password: string, username?: string ): Promise { // Check if user already exists by email const existingUser = await this.findUserByEmail(email); if (existingUser) { throw new CouchDBError('User already exists', 409); } const newUser: Omit = { _id: uuidv4(), username: username || email.split('@')[0], // Default username from email email, password, // In production, this should be hashed with bcrypt emailVerified: false, // Require email verification for password accounts status: AccountStatus.PENDING, role: UserRole.USER, // Default role is USER createdAt: new Date(), lastLoginAt: new Date(), }; return this.putDoc('users', newUser); } async createUserFromOAuth(userData: { email: string; username: string; avatar?: string; }): Promise { // Check if user already exists by email const existingUser = await this.findUserByEmail(userData.email); if (existingUser) { throw new CouchDBError('User already exists', 409); } const newUser: Omit = { _id: uuidv4(), username: userData.username, email: userData.email, avatar: userData.avatar, emailVerified: true, // OAuth users have verified emails status: AccountStatus.ACTIVE, role: UserRole.USER, // Default role is USER createdAt: new Date(), lastLoginAt: new Date(), }; return this.putDoc('users', newUser); } async updateUser(user: User): Promise { return this.updateDocWithConflictResolution('users', user); } // --- Admin User Management --- async getAllUsers(): Promise { return this.getDb('users'); } async getUserById(userId: string): Promise { return this.getDoc('users', userId); } async suspendUser(userId: string): Promise { const user = await this.getUserById(userId); if (!user) { throw new CouchDBError('User not found', 404); } const updatedUser = { ...user, status: AccountStatus.SUSPENDED }; return this.updateUser(updatedUser); } async activateUser(userId: string): Promise { const user = await this.getUserById(userId); if (!user) { throw new CouchDBError('User not found', 404); } const updatedUser = { ...user, status: AccountStatus.ACTIVE }; return this.updateUser(updatedUser); } async deleteUser(userId: string): Promise { const user = await this.getUserById(userId); if (!user) { throw new CouchDBError('User not found', 404); } // Delete user data await this.deleteDoc('users', user); // Delete user's associated data const userMeds = this.getUserDbName('meds', userId); const userSettings = this.getUserDbName('settings', userId); const userTaken = this.getUserDbName('taken', userId); const userReminders = this.getUserDbName('reminders', userId); localStorage.removeItem(userMeds); localStorage.removeItem(userSettings); localStorage.removeItem(userTaken); localStorage.removeItem(userReminders); } async changeUserPassword(userId: string, newPassword: string): Promise { const user = await this.getUserById(userId); if (!user) { throw new CouchDBError('User not found', 404); } // In production, hash the password with bcrypt const updatedUser = { ...user, password: newPassword }; return this.updateUser(updatedUser); } // --- User Data Management --- private getUserDbName = ( type: 'meds' | 'settings' | 'taken' | 'reminders', userId: string ) => `${type}_${userId}`; async getMedications(userId: string): Promise { return this.getDb(this.getUserDbName('meds', userId)); } async addMedication( userId: string, med: Omit ): Promise { const newMed = { ...med, _id: uuidv4() }; return this.putDoc(this.getUserDbName('meds', userId), newMed); } async updateMedication(userId: string, med: Medication): Promise { return this.updateDocWithConflictResolution( this.getUserDbName('meds', userId), med ); } async deleteMedication(userId: string, med: Medication): Promise { return this.deleteDoc(this.getUserDbName('meds', userId), med); } async getCustomReminders(userId: string): Promise { return this.getDb(this.getUserDbName('reminders', userId)); } async addCustomReminder( userId: string, reminder: Omit ): Promise { const newReminder = { ...reminder, _id: uuidv4() }; return this.putDoc( this.getUserDbName('reminders', userId), newReminder ); } async updateCustomReminder( userId: string, reminder: CustomReminder ): Promise { return this.updateDocWithConflictResolution( this.getUserDbName('reminders', userId), reminder ); } async deleteCustomReminder( userId: string, reminder: CustomReminder ): Promise { return this.deleteDoc( this.getUserDbName('reminders', userId), reminder ); } async getSettings(userId: string): Promise { const dbName = this.getUserDbName('settings', userId); let settings = await this.getDoc(dbName, userId); if (!settings) { settings = await this.putDoc(dbName, { _id: userId, notificationsEnabled: true, hasCompletedOnboarding: false, }); } return settings; } async updateSettings( userId: string, settings: UserSettings ): Promise { return this.updateDocWithConflictResolution( this.getUserDbName('settings', userId), settings ); } async getTakenDoses(userId: string): Promise { const dbName = this.getUserDbName('taken', userId); let takenDoses = await this.getDoc(dbName, userId); if (!takenDoses) { takenDoses = await this.putDoc(dbName, { _id: userId, doses: {}, }); } return takenDoses; } async updateTakenDoses( userId: string, takenDoses: TakenDoses ): Promise { // Custom merge logic for taken doses to avoid overwriting recent updates const mergeFn = (latest: TakenDoses, incoming: TakenDoses): TakenDoses => { return { ...latest, // Use latest doc as the base ...incoming, // Apply incoming changes doses: { ...latest.doses, ...incoming.doses }, // Specifically merge the doses object _rev: latest._rev, // IMPORTANT: Use the latest revision for the update attempt }; }; return this.updateDocWithConflictResolution( this.getUserDbName('taken', userId), takenDoses, mergeFn ); } async deleteAllUserData(userId: string): Promise { await latency(); localStorage.removeItem(this.getUserDbName('meds', userId)); localStorage.removeItem(this.getUserDbName('settings', userId)); localStorage.removeItem(this.getUserDbName('taken', userId)); localStorage.removeItem(this.getUserDbName('reminders', userId)); } } export { CouchDBService }; export const dbService = new CouchDBService();