import { defineStore } from 'pinia' import { ref, computed } from 'vue' import { jwtDecode } from 'jwt-decode' export interface JwtPayload { sub: string role: string rank: number scope_level: string region_code?: string scope_id?: number exp: number iat: number } export interface User { id: string email: string role: string rank: number scope_level: string region_code?: string scope_id?: number permissions: string[] } export const useAuthStore = defineStore('auth', () => { // State const token = ref(null) const user = ref(null) const isAuthenticated = computed(() => !!token.value && !isTokenExpired()) const isLoading = ref(false) const error = ref(null) // Initialize token from localStorage only on client side if (typeof window !== 'undefined') { token.value = localStorage.getItem('admin_token') } // Getters const getUserRole = computed(() => user.value?.role || '') const getUserRank = computed(() => user.value?.rank || 0) const getScopeLevel = computed(() => user.value?.scope_level || '') const getRegionCode = computed(() => user.value?.region_code || '') const getScopeId = computed(() => user.value?.scope_id || 0) const getPermissions = computed(() => user.value?.permissions || []) // Check if token is expired function isTokenExpired(): boolean { if (!token.value) return true try { const decoded = jwtDecode(token.value) return Date.now() >= decoded.exp * 1000 } catch { return true } } // Parse token and set user function parseToken(): void { if (!token.value) { user.value = null return } try { const decoded = jwtDecode(token.value) // Map JWT claims to user object user.value = { id: decoded.sub, email: decoded.sub, // Assuming sub is email role: decoded.role, rank: decoded.rank, scope_level: decoded.scope_level, region_code: decoded.region_code, scope_id: decoded.scope_id, permissions: generatePermissions(decoded.role, decoded.rank) } error.value = null } catch (err) { console.error('Failed to parse token:', err) error.value = 'Invalid token format' user.value = null // Clear invalid token from storage token.value = null if (typeof window !== 'undefined') { localStorage.removeItem('admin_token') } } } // Generate permissions based on role and rank function generatePermissions(role: string, rank: number): string[] { const permissions: string[] = [] // Base permissions based on role switch (role) { case 'superadmin': permissions.push('*') break case 'admin': permissions.push('view:dashboard', 'manage:users', 'manage:services', 'view:finance') if (rank >= 5) permissions.push('manage:settings') break case 'moderator': permissions.push('view:dashboard', 'moderate:services', 'view:users') break case 'salesperson': permissions.push('view:dashboard', 'view:sales', 'manage:leads') break } // Add geographical scope permissions permissions.push(`scope:${role}`) return permissions } // Check if user has permission function hasPermission(permission: string): boolean { if (!user.value) return false if (user.value.permissions.includes('*')) return true return user.value.permissions.includes(permission) } // Check if user has required role rank function hasRank(minRank: number): boolean { return user.value?.rank >= minRank } // Check if user can access scope function canAccessScope(requestedScopeId: number, requestedRegionCode?: string): boolean { if (!user.value) return false // Superadmin can access everything if (user.value.role === 'superadmin') return true // Check scope_id match if (user.value.scope_id && user.value.scope_id === requestedScopeId) return true // Check region_code match if (user.value.region_code && requestedRegionCode) { return user.value.region_code === requestedRegionCode } return false } // Login action - REAL API AUTHENTICATION ONLY async function login(email: string, password: string): Promise { isLoading.value = true error.value = null try { // Debug: Log what we're sending console.log('Auth store: Attempting login for', email) console.log('Auth store: Password length', password.length) // Prepare URL-encoded form data for OAuth2 password grant (as per FastAPI auth endpoint) // FastAPI's OAuth2PasswordRequestForm expects application/x-www-form-urlencoded // Use explicit string encoding to guarantee FastAPI accepts it (Nuxt's $fetch messes up URLSearchParams) const bodyString = `username=${encodeURIComponent(email)}&password=${encodeURIComponent(password)}`; console.log('Auth store: Body string created', bodyString) // Call real backend login endpoint using $fetch (Nuxt's fetch) // $fetch automatically throws on non-2xx responses, so we just need to catch const data = await $fetch('/api/v1/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: bodyString }) console.log('Auth login API response:', data) // Extract token const accessToken = data.access_token if (!accessToken) { throw new Error('No access token in response') } // Store token safely (SSR-safe) if (typeof window !== 'undefined') { localStorage.setItem('admin_token', accessToken) } token.value = accessToken parseToken() return true } catch (err) { console.error('Auth store: Login catch block error:', err) error.value = err instanceof Error ? err.message : 'Login failed' return false } finally { isLoading.value = false } } // Logout action function logout(): void { token.value = null user.value = null if (typeof window !== 'undefined') { localStorage.removeItem('admin_token') } } // Initialize store if (token.value) { parseToken() } return { // State token, user, isAuthenticated, isLoading, error, // Getters getUserRole, getUserRank, getScopeLevel, getRegionCode, getScopeId, getPermissions, // Actions login, logout, hasPermission, hasRank, canAccessScope, parseToken } })