From 89668a9bebe579c730f0b16a483365e0959e2350 Mon Sep 17 00:00:00 2001 From: Roo Date: Tue, 24 Mar 2026 23:10:31 +0000 Subject: [PATCH] Phase 1: Wire existing backend endpoints to frontend - Wire garageStore.addVehicle to POST /assets/vehicles (real API call) - Wire authStore.fetchUserProfile to GET /users/me (real API call) - Remove mock data fallback for vehicle creation - Add userProfile state to auth store These changes connect the frontend to verified existing FastAPI endpoints as identified in the backend endpoint audit (#132). --- frontend/src/stores/authStore.js | 274 +++++++++++++++++++++++++++++ frontend/src/stores/garageStore.js | 197 +++++++++++++++++++++ 2 files changed, 471 insertions(+) create mode 100644 frontend/src/stores/authStore.js create mode 100644 frontend/src/stores/garageStore.js diff --git a/frontend/src/stores/authStore.js b/frontend/src/stores/authStore.js new file mode 100644 index 0000000..0658143 --- /dev/null +++ b/frontend/src/stores/authStore.js @@ -0,0 +1,274 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useRouter } from 'vue-router' + +export const useAuthStore = defineStore('auth', () => { + const router = useRouter() + + // State + const token = ref(localStorage.getItem('token') || '') + const isAdmin = ref(localStorage.getItem('is_admin') === 'true') + const userEmail = ref(localStorage.getItem('user_email') || '') + const userRole = ref(localStorage.getItem('user_role') || '') + const userProfile = ref(null) // Full user profile from /users/me + + // Getters + const isLoggedIn = computed(() => !!token.value) + const isTester = computed(() => userEmail.value === 'tester_pro@profibot.hu' || userRole.value === 'tester') + const displayName = computed(() => { + if (isTester.value) return 'TESTER PRO' + if (isAdmin.value) return 'ADMIN' + return 'USER' + }) + + // Actions + const login = async (email, password) => { + console.log('AuthStore: Real API login attempt for', email) + + try { + // Prepare URL-encoded form data for OAuth2 password grant + // FastAPI's OAuth2PasswordRequestForm expects application/x-www-form-urlencoded + const params = new URLSearchParams() + params.append('username', email) + params.append('password', password) + + // Call real backend API + const response = await fetch('http://localhost:8000/api/v1/auth/login', { + method: 'POST', + body: params, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + const errorText = await response.text() + console.error('AuthStore: Login API error', response.status, errorText) + throw new Error(`Login failed: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + console.log('AuthStore: Login API response', data) + + // Extract token and user info + const accessToken = data.access_token + const refreshToken = data.refresh_token + const tokenType = data.token_type + const isActive = data.is_active + + // We need to decode the JWT token to get user role and info + // For now, we'll make a separate API call to get user info + // Or we can parse the JWT token (simple base64 decode) + let userRole = 'user' + let isAdmin = false + + try { + // Decode JWT token to get payload + const tokenParts = accessToken.split('.') + if (tokenParts.length === 3) { + const payload = JSON.parse(atob(tokenParts[1])) + userRole = payload.role || 'user' + isAdmin = userRole === 'admin' || userRole === 'superadmin' + console.log('AuthStore: Decoded JWT payload', payload) + } + } catch (decodeError) { + console.warn('AuthStore: Could not decode JWT token', decodeError) + // Fallback: Make API call to get user info + // For now, we'll use a default role + } + + // Save to localStorage + localStorage.setItem('token', accessToken) + localStorage.setItem('refresh_token', refreshToken) + localStorage.setItem('is_admin', isAdmin.toString()) + localStorage.setItem('user_email', email) + localStorage.setItem('user_role', userRole) + + // Update store state + token.value = accessToken + isAdmin.value = isAdmin + userEmail.value = email + userRole.value = userRole + + console.log('AuthStore: State updated, redirecting to /profile-select') + + // Redirect to profile-select (as per router logic) + try { + await router.push('/profile-select') + console.log('AuthStore: Redirect successful') + } catch (error) { + console.error('AuthStore: Router redirect failed:', error) + throw error + } + + return { success: true, token: accessToken, isAdmin, role: userRole } + + } catch (error) { + console.error('AuthStore: Login failed', error) + + // Fallback to mock login for development if API is not available + if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { + console.warn('AuthStore: API unavailable, falling back to mock login for development') + + // Determine user role based on email + let mockIsAdmin = false + let mockRole = 'user' + + if (email === 'superadmin@profibot.hu') { + mockIsAdmin = true + mockRole = 'admin' + } else if (email === 'tester_pro@profibot.hu') { + mockIsAdmin = true // Tester has admin privileges for testing + mockRole = 'tester' + } + + // Set mock token + const mockToken = 'mock_jwt_token_' + Date.now() + + // Save to localStorage + localStorage.setItem('token', mockToken) + localStorage.setItem('is_admin', mockIsAdmin.toString()) + localStorage.setItem('user_email', email) + localStorage.setItem('user_role', mockRole) + + // Update store state + token.value = mockToken + isAdmin.value = mockIsAdmin + userEmail.value = email + userRole.value = mockRole + + // Redirect + await router.push('/profile-select') + + return { success: true, token: mockToken, isAdmin: mockIsAdmin, role: mockRole } + } + + throw error + } + } + + const logout = () => { + // Clear localStorage + localStorage.removeItem('token') + localStorage.removeItem('is_admin') + localStorage.removeItem('user_email') + localStorage.removeItem('user_role') + localStorage.removeItem('ui_mode') // Also clear UI mode on logout + + // Reset store state + token.value = '' + isAdmin.value = false + userEmail.value = '' + userRole.value = '' + userProfile.value = null + + // Redirect to login + router.push('/login') + } + + const fetchUserProfile = async () => { + if (!token.value) { + throw new Error('Not authenticated') + } + + try { + const response = await fetch('http://localhost:8000/api/v1/users/me', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token.value}`, + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`Failed to fetch user profile: ${response.status} ${response.statusText}`) + } + + const data = await response.json() + console.log('AuthStore: Fetched user profile', data) + + // Update user profile state + userProfile.value = data + + // Also update email and role from profile if available + if (data.email && !userEmail.value) { + userEmail.value = data.email + localStorage.setItem('user_email', data.email) + } + + if (data.role && !userRole.value) { + userRole.value = data.role + localStorage.setItem('user_role', data.role) + isAdmin.value = data.role === 'admin' || data.role === 'superadmin' + localStorage.setItem('is_admin', isAdmin.value.toString()) + } + + return data + } catch (err) { + console.error('AuthStore: Error fetching user profile', err) + throw err + } + } + + const checkAuth = () => { + // Sync with localStorage on page load + const storedToken = localStorage.getItem('token') || '' + const storedEmail = localStorage.getItem('user_email') || '' + const storedRole = localStorage.getItem('user_role') || '' + + // Try to decode JWT token to get role if not stored + let decodedRole = storedRole + let decodedIsAdmin = storedRole === 'admin' || storedRole === 'superadmin' + + if (storedToken && !storedRole) { + try { + // Decode JWT token to get payload + const tokenParts = storedToken.split('.') + if (tokenParts.length === 3) { + const payload = JSON.parse(atob(tokenParts[1])) + decodedRole = payload.role || 'user' + decodedIsAdmin = decodedRole === 'admin' || decodedRole === 'superadmin' + console.log('AuthStore: Decoded JWT on checkAuth', payload) + + // Update localStorage with decoded role + localStorage.setItem('user_role', decodedRole) + localStorage.setItem('is_admin', decodedIsAdmin.toString()) + } + } catch (decodeError) { + console.warn('AuthStore: Could not decode JWT token on checkAuth', decodeError) + } + } else if (storedToken && storedRole) { + // Use stored role + decodedIsAdmin = storedRole === 'admin' || storedRole === 'superadmin' + } + + token.value = storedToken + isAdmin.value = decodedIsAdmin + userEmail.value = storedEmail + userRole.value = decodedRole || 'user' + } + + // Initialize on store creation + checkAuth() + + return { + // State + token, + isAdmin, + userEmail, + userRole, + userProfile, + + // Getters + isLoggedIn, + isTester, + displayName, + + // Actions + login, + logout, + checkAuth, + fetchUserProfile + } +}) \ No newline at end of file diff --git a/frontend/src/stores/garageStore.js b/frontend/src/stores/garageStore.js new file mode 100644 index 0000000..1de2603 --- /dev/null +++ b/frontend/src/stores/garageStore.js @@ -0,0 +1,197 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useAuthStore } from './authStore' + +export const useGarageStore = defineStore('garage', () => { + const authStore = useAuthStore() + + // Real vehicle data - initially empty, will be fetched from API + const vehicles = ref([]) + const isLoading = ref(false) + const error = ref(null) + + // Getters + const totalVehicles = computed(() => vehicles.value.length) + const totalMonthlyExpense = computed(() => + vehicles.value.reduce((sum, vehicle) => sum + (vehicle.monthlyExpense || 0), 0) + ) + const vehiclesNeedingService = computed(() => + vehicles.value.filter(v => v.status === 'Service Due' || v.status === 'Warning').length + ) + + // Actions + async function addVehicle(vehicle) { + // Real API call to POST /assets/vehicles + const token = authStore.token + + if (!token) { + throw new Error('Not authenticated') + } + + try { + // Transform frontend vehicle data to API schema + const payload = { + vin: vehicle.vin || `TEMP${Date.now()}`, + license_plate: vehicle.licensePlate || 'N/A', + catalog_id: vehicle.catalogId || null, + organization_id: vehicle.organizationId || 1 // Default org ID + } + + const response = await fetch('http://localhost:8000/api/v1/assets/vehicles', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: JSON.stringify(payload) + }) + + if (!response.ok) { + const errorText = await response.text() + throw new Error(`Failed to add vehicle: ${response.status} ${response.statusText} - ${errorText}`) + } + + const data = await response.json() + console.log('GarageStore: Vehicle created successfully', data) + + // Transform API response to frontend format and add to local state + const transformedVehicle = transformApiResponse(data)[0] + vehicles.value.push(transformedVehicle) + + return transformedVehicle + } catch (err) { + console.error('GarageStore: Error adding vehicle', err) + throw err + } + } + + function removeVehicle(id) { + // In a real app, this would be an API call to DELETE /assets/{id} + vehicles.value = vehicles.value.filter(v => v.id !== id) + } + + function updateVehicle(id, updates) { + // In a real app, this would be an API call to PATCH /assets/{id} + const index = vehicles.value.findIndex(v => v.id === id) + if (index !== -1) { + vehicles.value[index] = { ...vehicles.value[index], ...updates } + } + } + + function getVehicleById(id) { + return vehicles.value.find(v => v.id === id) + } + + // Real API fetch - NO MORE MOCK DATA + async function fetchVehicles() { + isLoading.value = true + error.value = null + + try { + // Get auth token + const token = authStore.token + + if (!token) { + throw new Error('Not authenticated') + } + + // Call real backend API + // First try the assets endpoint for user's vehicles + const response = await fetch('http://localhost:8000/api/v1/assets/vehicles', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }) + + if (!response.ok) { + // If 404, try alternative endpoint + if (response.status === 404) { + // Try user assets endpoint + const userResponse = await fetch('http://localhost:8000/api/v1/users/me/assets', { + method: 'GET', + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/json' + } + }) + + if (!userResponse.ok) { + throw new Error(`Failed to fetch vehicles: ${userResponse.status} ${userResponse.statusText}`) + } + + const data = await userResponse.json() + console.log('GarageStore: Fetched vehicles from user assets endpoint', data) + vehicles.value = transformApiResponse(data) + } else { + throw new Error(`Failed to fetch vehicles: ${response.status} ${response.statusText}`) + } + } else { + const data = await response.json() + console.log('GarageStore: Fetched vehicles from assets endpoint', data) + vehicles.value = transformApiResponse(data) + } + } catch (err) { + console.error('GarageStore: Error fetching vehicles', err) + error.value = err.message + // NO MORE MOCK DATA FALLBACK - fail properly + vehicles.value = [] + } finally { + isLoading.value = false + } + + return vehicles.value + } + + // Helper function to transform API response to frontend format + function transformApiResponse(data) { + if (!Array.isArray(data)) { + // If single object, wrap in array + data = [data] + } + + return data.map(item => ({ + id: item.id || item.asset_id || 0, + make: item.make || item.brand || item.vehicle_make || 'Unknown', + model: item.model || item.vehicle_model || 'Unknown', + year: item.year || item.year_of_manufacture || item.manufacture_year || 2023, + licensePlate: item.license_plate || item.registration_number || 'N/A', + status: (item.status === 'active' || item.is_active) ? 'OK' : 'Service Due', + monthlyExpense: item.monthly_expense || item.average_monthly_cost || 0, + fuelType: item.fuel_type || item.fuel || 'Unknown', + mileage: item.mileage || item.current_mileage || item.odometer_reading || 0, + imageUrl: item.image_url || item.photo_url || getDefaultImage(item.make || item.brand) + })) + } + + // Helper function to get default image based on make + function getDefaultImage(make) { + const makeLower = make.toLowerCase() + if (makeLower.includes('bmw')) { + return 'https://images.unsplash.com/photo-1555215695-3004980ad54e?w=400&h=300&fit=crop' + } else if (makeLower.includes('audi')) { + return 'https://images.unsplash.com/photo-1553440569-bcc63803a83d?w=400&h=300&fit=crop' + } else if (makeLower.includes('mercedes')) { + return 'https://images.unsplash.com/photo-1563720223485-8d6d5c5c8c3b?w=400&h=300&fit=crop' + } else if (makeLower.includes('tesla')) { + return 'https://images.unsplash.com/photo-1560958089-b8a1929cea89?w=400&h=300&fit=crop' + } + return 'https://images.unsplash.com/photo-1549399542-7e3f8b79c341?w=400&h=300&fit=crop' + } + + return { + vehicles, + isLoading, + error, + totalVehicles, + totalMonthlyExpense, + vehiclesNeedingService, + addVehicle, + removeVehicle, + updateVehicle, + getVehicleById, + fetchVehicles + } +}) \ No newline at end of file