// src/lib/stores/auth.svelte.ts import { browser } from '$app/environment'; import { goto } from '$app/navigation'; export type UserRole = 'unverified_email' | 'verified_email' | 'admin' | 'guest' | 'affiliate'; export interface DecodedToken { user_id: string; role: UserRole; exp: number; } export interface User { id: string; email: string; role: UserRole; firstName: string; lastName: string; phone?: string; dateOfBirth?: string; loyaltyStamps?: number; referralCode?: string; profilePicUrl?: string; } class AuthStore { private token = $state(null); private user = $state(null); private loading = $state(true); constructor() { if (browser) { this.initializeAuth(); } } get isAuthenticated() { return this.token !== null && this.user !== null; } get currentUser() { return this.user; } get currentToken() { return this.token; } get isLoading() { return this.loading; } get hasLoaded() { return !this.loading; } private initializeAuth() { const storedToken = localStorage.getItem('authToken'); if (storedToken) { const decoded = this.decodeToken(storedToken); if (decoded && !this.isTokenExpired(decoded)) { this.token = storedToken; // Set basic user info from token this.user = { id: decoded.user_id, role: decoded.role, email: '', firstName: '', lastName: '' }; this.fetchUserProfile(); } else { this.clearAuth(); } } this.loading = false; } private decodeToken(token: string): DecodedToken | null { try { const payload = token.split('.')[1]; const decoded = JSON.parse(atob(payload)); return decoded; } catch (e) { console.error('Failed to decode token:', e); return null; } } private isTokenExpired(decoded: DecodedToken): boolean { return decoded.exp * 1000 < Date.now(); } // Simple setters - UI handles the API calls setToken(token: string) { this.token = token; if (browser) { localStorage.setItem('authToken', token); } // Decode to get basic info const decoded = this.decodeToken(token); if (decoded) { this.user = { id: decoded.user_id, role: decoded.role, email: '', firstName: '', lastName: '' }; this.fetchUserProfile(); } } private async fetchUserProfile() { if (!this.token) return; try { const response = await fetch('/api/user/profile', { headers: { 'Authorization': `Bearer ${this.token}` } }); if (!response.ok) { throw new Error('Failed to fetch profile'); } const userData = await response.json(); this.user = userData; } catch (error) { console.error('Failed to fetch user profile:', error); this.clearAuth(); } } // inside AuthStore logout = () => { this.clearAuth(); goto('/'); }; private clearAuth() { this.token = null; this.user = null; if (browser) { localStorage.removeItem('authToken'); } } hasRole(requiredRole: UserRole | UserRole[]): boolean { if (!this.user) return false; const roles = Array.isArray(requiredRole) ? requiredRole : [requiredRole]; return roles.includes(this.user.role); } isAdmin(): boolean { return this.hasRole('admin'); } isVerified(): boolean { return this.hasRole(['verified_email', 'admin']); } // Refresh token before it expires async refreshTokenIfNeeded() { if (!this.token) return; const decoded = this.decodeToken(this.token); if (!decoded) { this.clearAuth(); return; } // Refresh if token expires in less than 2 weeks const threeDays = 2 * 7 * 24 * 60 * 60 * 1000; if (decoded.exp * 1000 - Date.now() < threeDays) { try { const response = await fetch('/api/refresh-token', { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` } }); if (response.ok) { const data = await response.json(); this.setToken(data.token); } else { this.clearAuth(); } } catch (error) { console.error('Token refresh failed:', error); } } } // Manual refresh method async refreshProfile() { await this.fetchUserProfile(); } } export const authStore = new AuthStore();