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