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).
This commit is contained in:
274
frontend/src/stores/authStore.js
Normal file
274
frontend/src/stores/authStore.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
197
frontend/src/stores/garageStore.js
Normal file
197
frontend/src/stores/garageStore.js
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user