import { ILifecycleService } from './api'; import { CreateItemDTO, DailyDigestConfig, DailyDigestEmail, Group, LifecycleItem, LifecycleType, RemindersSummary, UpdateItemDTO, User, } from './types'; const STORAGE_KEYS = { TOKEN: 'plt_auth_token_v1', CURRENT_USER_ID: 'plt_current_user_id_v1', USER_TOKENS: 'plt_user_tokens_v1', EXPLICIT_LOGOUT: 'plt_logged_out_v1', }; const DEFAULT_SEED_PASSWORD = 'password123'; export class BackendLifecycleService implements ILifecycleService { private baseUrl: string; private token: string | null = null; private currentUserId: string | null = null; private userTokens: Record = {}; private explicitlyLoggedOut: boolean = false; constructor(baseUrl?: string) { if (baseUrl !== undefined) { this.baseUrl = baseUrl.replace(/\/$/, ''); } else { const envUrl = typeof import.meta !== 'undefined' && import.meta.env ? import.meta.env.VITE_API_URL : ''; let resolvedBase = envUrl || ''; if (!resolvedBase && typeof window !== 'undefined' && window.location) { const host = window.location.hostname; if (host.includes('cloudfront.net')) { resolvedBase = 'https://7ksomsylvke2uawuhhccbfr6hy0agzyb.lambda-url.ap-south-1.on.aws'; } } this.baseUrl = resolvedBase.replace(/\/$/, ''); } this.loadState(); } private isStorageAvailable(): boolean { return typeof window !== 'undefined' && typeof window.localStorage !== 'undefined'; } private loadState(): void { if (!this.isStorageAvailable()) return; try { this.token = localStorage.getItem(STORAGE_KEYS.TOKEN); this.currentUserId = localStorage.getItem(STORAGE_KEYS.CURRENT_USER_ID); this.explicitlyLoggedOut = localStorage.getItem(STORAGE_KEYS.EXPLICIT_LOGOUT) === 'true'; const storedUserTokens = localStorage.getItem(STORAGE_KEYS.USER_TOKENS); if (storedUserTokens) { this.userTokens = JSON.parse(storedUserTokens); } } catch { // Storage access or parse error fallback } } private saveState(): void { if (!this.isStorageAvailable()) return; try { if (this.token) { localStorage.setItem(STORAGE_KEYS.TOKEN, this.token); } else { localStorage.removeItem(STORAGE_KEYS.TOKEN); } if (this.currentUserId) { localStorage.setItem(STORAGE_KEYS.CURRENT_USER_ID, this.currentUserId); } else { localStorage.removeItem(STORAGE_KEYS.CURRENT_USER_ID); } if (this.explicitlyLoggedOut) { localStorage.setItem(STORAGE_KEYS.EXPLICIT_LOGOUT, 'true'); } else { localStorage.removeItem(STORAGE_KEYS.EXPLICIT_LOGOUT); } localStorage.setItem(STORAGE_KEYS.USER_TOKENS, JSON.stringify(this.userTokens)); } catch { // Ignore quota errors } } private setSession(user: User, token: string): void { this.token = token; this.currentUserId = user.id; this.userTokens[user.id] = token; this.explicitlyLoggedOut = false; this.saveState(); } private clearSession(): void { this.token = null; this.currentUserId = null; this.explicitlyLoggedOut = true; this.saveState(); } private async request(endpoint: string, options: RequestInit = {}): Promise { const url = `${this.baseUrl}${endpoint}`; const headers = new Headers(options.headers || {}); if (!headers.has('Content-Type') && options.body && typeof options.body === 'string') { headers.set('Content-Type', 'application/json'); } if (this.token && !headers.has('Authorization')) { headers.set('Authorization', `Bearer ${this.token}`); } const response = await fetch(url, { ...options, headers, }); if (!response.ok) { let errorMessage = `HTTP error ${response.status}`; try { const errorJson = await response.json(); if (errorJson?.error) { errorMessage = errorJson.error; } else if (errorJson?.detail) { errorMessage = typeof errorJson.detail === 'string' ? errorJson.detail : JSON.stringify(errorJson.detail); } } catch { // Response might not be JSON } throw new Error(errorMessage); } if (response.status === 204) { return undefined as unknown as T; } const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { return (await response.json()) as T; } return undefined as unknown as T; } // ------------------ AUTH & USER MANAGEMENT ------------------ public async getCurrentUser(): Promise { if (this.token) { try { const user = await this.request('/api/v1/auth/me', { method: 'GET' }); this.currentUserId = user.id; this.saveState(); return user; } catch { // Token expired or invalid this.token = null; this.currentUserId = null; this.saveState(); } } // If user explicitly logged out, do not auto-login if (this.explicitlyLoggedOut) { return null; } // Default seamless initial user experience: login default seed user Alex Taylor try { const user = await this.login('alex@example.com', DEFAULT_SEED_PASSWORD); return user; } catch { return null; } } public async login(email: string, password?: string): Promise { const trimmedEmail = email.trim().toLowerCase(); const payload = { email: trimmedEmail, password: password || DEFAULT_SEED_PASSWORD, }; const res = await this.request<{ user: User; token: string }>('/api/v1/auth/login', { method: 'POST', body: JSON.stringify(payload), }); this.setSession(res.user, res.token); return res.user; } public async signup(email: string, password?: string, name?: string): Promise { const trimmedEmail = email.trim().toLowerCase(); const payload = { email: trimmedEmail, password: password || DEFAULT_SEED_PASSWORD, name: name?.trim() || trimmedEmail.split('@')[0], }; const res = await this.request<{ user: User; token: string }>('/api/v1/auth/signup', { method: 'POST', body: JSON.stringify(payload), }); this.setSession(res.user, res.token); return res.user; } public async logout(): Promise { try { await this.request<{ message: string }>('/api/v1/auth/logout', { method: 'POST' }); } catch { // Ignore network errors during logout } this.clearSession(); } public async switchUser(userId: string): Promise { // 1. Check if we already have a cached token for this user const cachedToken = this.userTokens[userId]; if (cachedToken) { this.token = cachedToken; try { const user = await this.request('/api/v1/auth/me', { method: 'GET' }); this.setSession(user, cachedToken); return user; } catch { // Cached token invalid/expired, proceed to re-login } } // 2. Fetch available users to find target user email const users = await this.listUsers(); const targetUser = users.find((u) => u.id === userId); if (!targetUser) { throw new Error('User not found'); } // 3. Log in with seed password const user = await this.login(targetUser.email, DEFAULT_SEED_PASSWORD); return user; } public async listUsers(): Promise { return this.request('/api/v1/users', { method: 'GET' }); } public async deleteAccount(userId: string): Promise { await this.request(`/api/v1/users/${encodeURIComponent(userId)}`, { method: 'DELETE', }); delete this.userTokens[userId]; if (this.currentUserId === userId) { this.clearSession(); } else { this.saveState(); } } // ------------------ LIFECYCLE ITEMS MANAGEMENT ------------------ public async getItems(options?: { groupId?: string | null; type?: LifecycleType; searchQuery?: string; }): Promise { const params = new URLSearchParams(); if (options?.groupId !== undefined) { params.set('groupId', options.groupId === null ? 'null' : options.groupId); } if (options?.type) { params.set('type', options.type); } if (options?.searchQuery) { params.set('searchQuery', options.searchQuery); } const query = params.toString(); const endpoint = `/api/v1/items${query ? `?${query}` : ''}`; return this.request(endpoint, { method: 'GET' }); } public async getItem(id: string): Promise { return this.request(`/api/v1/items/${encodeURIComponent(id)}`, { method: 'GET', }); } public async createItem(data: CreateItemDTO): Promise { return this.request('/api/v1/items', { method: 'POST', body: JSON.stringify(data), }); } public async updateItem(id: string, data: UpdateItemDTO): Promise { return this.request(`/api/v1/items/${encodeURIComponent(id)}`, { method: 'PATCH', body: JSON.stringify(data), }); } public async toggleItemCompletion(id: string): Promise { return this.request(`/api/v1/items/${encodeURIComponent(id)}/complete`, { method: 'POST', }); } public async deleteItem(id: string): Promise { await this.request(`/api/v1/items/${encodeURIComponent(id)}`, { method: 'DELETE', }); } // ------------------ GROUPS & SHARING ------------------ public async getGroups(): Promise { return this.request('/api/v1/groups', { method: 'GET' }); } public async getGroup(id: string): Promise { return this.request(`/api/v1/groups/${encodeURIComponent(id)}`, { method: 'GET', }); } public async createGroup(name: string): Promise { return this.request('/api/v1/groups', { method: 'POST', body: JSON.stringify({ name }), }); } public async inviteMember(groupId: string, memberEmail: string): Promise { return this.request(`/api/v1/groups/${encodeURIComponent(groupId)}/members`, { method: 'POST', body: JSON.stringify({ email: memberEmail }), }); } public async removeMember(groupId: string, memberEmail: string): Promise { return this.request( `/api/v1/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(memberEmail)}`, { method: 'DELETE', } ); } public async leaveGroup(groupId: string): Promise { await this.request<{ message: string }>( `/api/v1/groups/${encodeURIComponent(groupId)}/leave`, { method: 'POST', } ); } public async deleteGroup(groupId: string): Promise { await this.request(`/api/v1/groups/${encodeURIComponent(groupId)}`, { method: 'DELETE', }); } // ------------------ REMINDERS & EMAIL DIGEST ------------------ public async getRemindersSummary(): Promise { return this.request('/api/v1/reminders/summary', { method: 'GET', }); } public async getDigestConfig(): Promise { return this.request('/api/v1/reminders/digest/config', { method: 'GET', }); } public async updateDigestConfig( config: Partial ): Promise { return this.request('/api/v1/reminders/digest/config', { method: 'PATCH', body: JSON.stringify(config), }); } public async generateAndSendDailyDigest( recipientEmail?: string ): Promise { return this.request('/api/v1/reminders/digest/generate', { method: 'POST', body: JSON.stringify(recipientEmail ? { recipientEmail } : {}), }); } public async getDigestHistory(): Promise { return this.request('/api/v1/reminders/digest/history', { method: 'GET', }); } // ------------------ DEVELOPMENT & SEEDING ------------------ public async resetToDefaultSeed(): Promise { await this.request<{ message: string }>('/api/v1/dev/seed', { method: 'POST', }); } }