From 262505c8982c83463298c99bacf6f464522b1335 Mon Sep 17 00:00:00 2001 From: mohamad Date: Thu, 8 May 2025 22:22:46 +0200 Subject: [PATCH] Refactor API integration across multiple components; introduce centralized API configuration, enhance error handling, and improve type safety in API calls for better maintainability and user experience. --- be/app/core/api_config.py | 102 ++++++++++ be/app/main.py | 11 +- fe/src/boot/axios.ts | 80 ++++---- fe/src/components/CreateListModal.vue | 10 +- fe/src/config/api-config.ts | 106 +++++++++++ fe/src/config/api.ts | 18 ++ fe/src/pages/AccountPage.vue | 260 +++++++++++++++++++++++++- fe/src/pages/GroupDetailPage.vue | 16 +- fe/src/pages/GroupsPage.vue | 14 +- fe/src/pages/ListDetailPage.vue | 61 +++--- fe/src/pages/ListsPage.vue | 101 ++++++++-- fe/src/stores/auth.ts | 18 +- 12 files changed, 666 insertions(+), 131 deletions(-) create mode 100644 be/app/core/api_config.py create mode 100644 fe/src/config/api-config.ts create mode 100644 fe/src/config/api.ts diff --git a/be/app/core/api_config.py b/be/app/core/api_config.py new file mode 100644 index 0000000..79af045 --- /dev/null +++ b/be/app/core/api_config.py @@ -0,0 +1,102 @@ +from typing import Dict, Any +from app.config import settings + +# API Version +API_VERSION = "v1" + +# API Prefix +API_PREFIX = f"/api/{API_VERSION}" + +# API Endpoints +class APIEndpoints: + # Auth + AUTH = { + "LOGIN": "/auth/login", + "SIGNUP": "/auth/signup", + "REFRESH_TOKEN": "/auth/refresh-token", + } + + # Users + USERS = { + "PROFILE": "/users/profile", + "UPDATE_PROFILE": "/users/profile", + } + + # Lists + LISTS = { + "BASE": "/lists", + "BY_ID": "/lists/{id}", + "ITEMS": "/lists/{list_id}/items", + "ITEM": "/lists/{list_id}/items/{item_id}", + } + + # Groups + GROUPS = { + "BASE": "/groups", + "BY_ID": "/groups/{id}", + "LISTS": "/groups/{group_id}/lists", + "MEMBERS": "/groups/{group_id}/members", + } + + # Invites + INVITES = { + "BASE": "/invites", + "BY_ID": "/invites/{id}", + "ACCEPT": "/invites/{id}/accept", + "DECLINE": "/invites/{id}/decline", + } + + # OCR + OCR = { + "PROCESS": "/ocr/process", + } + + # Financials + FINANCIALS = { + "EXPENSES": "/financials/expenses", + "EXPENSE": "/financials/expenses/{id}", + "SETTLEMENTS": "/financials/settlements", + "SETTLEMENT": "/financials/settlements/{id}", + } + + # Health + HEALTH = { + "CHECK": "/health", + } + +# API Metadata +API_METADATA = { + "title": settings.API_TITLE, + "description": settings.API_DESCRIPTION, + "version": settings.API_VERSION, + "openapi_url": settings.API_OPENAPI_URL, + "docs_url": settings.API_DOCS_URL, + "redoc_url": settings.API_REDOC_URL, +} + +# API Tags +API_TAGS = [ + {"name": "Authentication", "description": "Authentication and authorization endpoints"}, + {"name": "Users", "description": "User management endpoints"}, + {"name": "Lists", "description": "Shopping list management endpoints"}, + {"name": "Groups", "description": "Group management endpoints"}, + {"name": "Invites", "description": "Group invitation management endpoints"}, + {"name": "OCR", "description": "Optical Character Recognition endpoints"}, + {"name": "Financials", "description": "Financial management endpoints"}, + {"name": "Health", "description": "Health check endpoints"}, +] + +# Helper function to get full API URL +def get_api_url(endpoint: str, **kwargs) -> str: + """ + Get the full API URL for an endpoint. + + Args: + endpoint: The endpoint path + **kwargs: Path parameters to format the endpoint + + Returns: + str: The full API URL + """ + formatted_endpoint = endpoint.format(**kwargs) + return f"{API_PREFIX}{formatted_endpoint}" \ No newline at end of file diff --git a/be/app/main.py b/be/app/main.py index 0230e98..fca6ecb 100644 --- a/be/app/main.py +++ b/be/app/main.py @@ -4,8 +4,9 @@ import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from app.api.api_router import api_router # Import the main combined router +from app.api.api_router import api_router from app.config import settings +from app.core.api_config import API_METADATA, API_TAGS # Import database and models if needed for startup/shutdown events later # from . import database, models @@ -18,12 +19,8 @@ logger = logging.getLogger(__name__) # --- FastAPI App Instance --- app = FastAPI( - title=settings.API_TITLE, - description=settings.API_DESCRIPTION, - version=settings.API_VERSION, - openapi_url=settings.API_OPENAPI_URL, - docs_url=settings.API_DOCS_URL, - redoc_url=settings.API_REDOC_URL + **API_METADATA, + openapi_tags=API_TAGS ) # --- CORS Middleware --- diff --git a/fe/src/boot/axios.ts b/fe/src/boot/axios.ts index ed9fb0f..377a391 100644 --- a/fe/src/boot/axios.ts +++ b/fe/src/boot/axios.ts @@ -1,70 +1,72 @@ import { boot } from 'quasar/wrappers'; -import axios, { type AxiosInstance } from 'axios'; -import { useAuthStore } from 'stores/auth'; - -declare module '@vue/runtime-core' { - interface ComponentCustomProperties { - $axios: AxiosInstance; - } -} +import axios, { AxiosInstance } from 'axios'; +import { API_BASE_URL, API_VERSION, API_ENDPOINTS } from 'src/config/api-config'; +// Create axios instance const api = axios.create({ - baseURL: `${import.meta.env.VITE_API_URL || 'http://localhost:8000'}/api/v1`, + baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); -// Request interceptor for adding auth token +// Request interceptor api.interceptors.request.use( (config) => { - const authStore = useAuthStore(); - if (authStore.accessToken) { - config.headers.Authorization = `Bearer ${authStore.accessToken}`; + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => { - return Promise.reject(new Error(error.message)); - }, + return Promise.reject(error); + } ); -// Response interceptor for handling errors +// Response interceptor api.interceptors.response.use( (response) => response, async (error) => { - const authStore = useAuthStore(); + const originalRequest = error.config; + + // If error is 401 and we haven't tried to refresh token yet + if (error.response?.status === 401 && !originalRequest._retry) { + originalRequest._retry = true; - // If the error is 401 and we have a refresh token, try to refresh the access token - if (error.response?.status === 401 && authStore.refreshToken) { try { - await authStore.refreshAccessToken(); - // Retry the original request - const config = error.config; - config.headers.Authorization = `Bearer ${authStore.accessToken}`; - return api(config); - } catch (error) { - // If refresh fails, clear tokens and redirect to login - authStore.logout(); + const refreshToken = localStorage.getItem('refreshToken'); + if (!refreshToken) { + throw new Error('No refresh token available'); + } + + // Call refresh token endpoint + const response = await api.post('/api/v1/auth/refresh-token', { + refresh_token: refreshToken, + }); + + const { access_token } = response.data; + localStorage.setItem('token', access_token); + + // Retry the original request with new token + originalRequest.headers.Authorization = `Bearer ${access_token}`; + return api(originalRequest); + } catch (refreshError) { + // If refresh token fails, clear storage and redirect to login + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); window.location.href = '/login'; - return Promise.reject( - new Error(error instanceof Error ? error.message : 'Failed to refresh token'), - ); + return Promise.reject(refreshError); } } - // If it's a 401 without refresh token or refresh failed, clear tokens and redirect - if (error.response?.status === 401) { - authStore.logout(); - window.location.href = '/login'; - } - - return Promise.reject(new Error(error.response?.data?.detail || error.message)); - }, + return Promise.reject(error); + } ); export default boot(({ app }) => { - app.config.globalProperties.$axios = api; + app.config.globalProperties.$axios = axios; + app.config.globalProperties.$api = api; }); export { api }; diff --git a/fe/src/components/CreateListModal.vue b/fe/src/components/CreateListModal.vue index 03cf160..06914b4 100644 --- a/fe/src/components/CreateListModal.vue +++ b/fe/src/components/CreateListModal.vue @@ -39,7 +39,7 @@ diff --git a/fe/src/pages/GroupDetailPage.vue b/fe/src/pages/GroupDetailPage.vue index 805a024..4e9e509 100644 --- a/fe/src/pages/GroupDetailPage.vue +++ b/fe/src/pages/GroupDetailPage.vue @@ -34,7 +34,7 @@