From 71e11eaf30418c9810cc694be2b48b6a103217fa Mon Sep 17 00:00:00 2001 From: yuany3721 Date: Sun, 12 Apr 2026 13:31:27 +0800 Subject: [PATCH] feat: Add OIDC authentication with Authentik and refactor project structure Backend: - Add auth.py for JWT token verification - Update main.py to protect all routes with auth middleware - Remove dashboard router (frontend handles aggregation) - Add Docker support with Dockerfile and docker-compose.yml Frontend: - Add OIDC authentication using oidc-client-ts with PKCE flow - Create router.js with auth guards for automatic login/logout - Add api.js for unified Axios instance with auth headers - Add composables: useAuth.js, useVehicleData.js for caching - Add views/Main.vue as main application page - Simplify App.vue to router-view container - Add deploy-web.sh deployment script Documentation: - Update AGENTS.md with new architecture and auth flow --- .gitignore | 2 + AGENTS.md | 189 +++++-- api/Dockerfile | 14 + api/auth.py | 184 +++++++ api/database.py | 12 +- api/docker-compose.yml | 11 + api/main.py | 22 +- api/models.py | 23 +- api/requirements.txt | 29 +- api/routers/__init__.py | 7 +- api/routers/costs.py | 96 ++-- api/routers/dashboard.py | 128 ----- api/routers/fuel_records.py | 227 ++++----- api/routers/vehicles.py | 67 +-- api/schemas.py | 55 +- deploy-web.sh | 86 ++++ web/package-lock.json | 474 +++++++++++++++++- web/package.json | 6 +- web/src/App.vue | 432 +--------------- web/src/api.js | 16 + web/src/components/cards/StatsCards.vue | 32 +- web/src/components/charts/DashboardCharts.vue | 303 ++++------- .../components/dialogs/cost/AddCostDialog.vue | 4 +- .../dialogs/cost/EditCostDialog.vue | 8 +- .../components/dialogs/fuel/AddFuelDialog.vue | 12 +- .../dialogs/fuel/EditFuelDialog.vue | 106 ++-- .../dialogs/vehicle/AddVehicleDialog.vue | 4 +- .../dialogs/vehicle/EditVehicleDialog.vue | 4 +- .../components/panels/CostRecordsPanel.vue | 338 +++++-------- .../components/panels/FuelRecordsPanel.vue | 102 ++-- web/src/composables/useAuth.js | 35 ++ web/src/composables/useVehicleData.js | 147 ++++++ web/src/constants/index.js | 15 + web/src/main.js | 2 + web/src/router.js | 65 +++ web/src/utils/calculations.js | 357 +++++++++++++ web/src/views/Main.vue | 405 +++++++++++++++ 37 files changed, 2517 insertions(+), 1502 deletions(-) create mode 100644 api/Dockerfile create mode 100644 api/auth.py create mode 100644 api/docker-compose.yml delete mode 100644 api/routers/dashboard.py create mode 100755 deploy-web.sh create mode 100644 web/src/api.js create mode 100644 web/src/composables/useAuth.js create mode 100644 web/src/composables/useVehicleData.js create mode 100644 web/src/constants/index.js create mode 100644 web/src/router.js create mode 100644 web/src/utils/calculations.js create mode 100644 web/src/views/Main.vue diff --git a/.gitignore b/.gitignore index cf8c060..877f4e8 100644 --- a/.gitignore +++ b/.gitignore @@ -300,3 +300,5 @@ dist .yarn/install-state.gz .pnp.* + +.ruff_cache/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 74d3f76..652ad92 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,70 +1,144 @@ # CarCost - Agent Instructions ## Project Overview -Full-stack vehicle expense tracking application. +Full-stack vehicle expense tracking application with OIDC authentication. - **Frontend**: Vue 3 + Vite + Element Plus + ECharts (`web/`) - **Backend**: FastAPI + SQLAlchemy + PostgreSQL (`api/`) +- **Authentication**: Authentik OIDC with PKCE flow ## Architecture ### Monorepo Structure ``` -web/src/ -├── App.vue # Main app component -├── main.js # Entry with Element Plus + ECharts setup -└── components/ # Reusable components - ├── panels/ # List panels with charts + table - │ ├── FuelRecordsPanel.vue # Desktop & mobile shared - │ └── CostRecordsPanel.vue # Desktop & mobile shared - ├── charts/ # Chart components - │ └── DashboardCharts.vue - ├── cards/ # Card components - │ └── StatsCards.vue - └── dialogs/ # Dialog components - ├── vehicle/ - ├── fuel/ - └── cost/ +carcost/ +├── deploy-web.sh # Frontend deployment script +├── web/ +│ ├── src/ +│ │ ├── App.vue # Root container (router-view only) +│ │ ├── main.js # Entry with Element Plus + ECharts +│ │ ├── router.js # Vue Router with OIDC auth guard +│ │ ├── api.js # Unified Axios instance with auth +│ │ ├── composables/ # Vue composables +│ │ │ ├── useVehicleData.js +│ │ │ └── useAuth.js # OIDC configuration (userManager only) +│ │ ├── views/ # Page views +│ │ │ └── Main.vue # Main application page +│ │ └── components/ # Reusable components +│ └── dist/ # Build output +│ +├── api/ +│ ├── main.py # FastAPI entry with auth middleware +│ ├── auth.py # JWT verification module +│ ├── config.py # Configuration +│ ├── database.py # Database connection +│ ├── models.py # SQLAlchemy models +│ ├── schemas.py # Pydantic schemas +│ └── routers/ +│ ├── vehicles.py # Vehicle CRUD +│ ├── fuel_records.py # Fuel record CRUD +│ └── costs.py # Cost record CRUD ``` -**Responsive Design Pattern**: -- `App.vue` uses CSS media queries to switch between desktop/mobile layouts -- `*Panel.vue` components handle both desktop and mobile via `showCharts`/`showList` props -- Desktop: Dual-column layout with StatsCards + DashboardCharts + Panels side-by-side -- Mobile: Single-column with view switching (dashboard/fuel/cost tabs) +### Authentication Flow + +**Frontend (Vue Router Guard)**: +1. `router.beforeEach` checks authentication status +2. Unauthenticated → redirect to Authentik OIDC login +3. Callback `/auth/callback` → process code, exchange for token +4. Token stored in localStorage, set to Axios headers +5. All API requests include `Authorization: Bearer ` + +**Backend (FastAPI JWT)**: +1. `auth.py` validates JWT token from Authentik +2. All routes protected by `get_current_user` dependency +3. Returns 401 for invalid/missing tokens ### API Routing Convention All API routes include `/carcost` prefix: -- `/carcost/vehicles/*` - Vehicle CRUD -- `/carcost/fuel_records/*` - Fuel records -- `/carcost/costs/*` - Cost records -- `/carcost/dashboard/*` - Dashboard data +- `GET/POST /carcost/vehicles/*` - Vehicle CRUD +- `GET/POST /carcost/fuel-records/*` - Fuel records +- `GET/POST /carcost/costs/*` - Cost records -Frontend uses: `API_BASE = 'https://api.yuany3721.site/carcost'` +Frontend API base: `https://api.yuany3721.site/carcost` + +### Key Files + +**Frontend Auth**: +- `web/src/router.js` - Route guards handle OIDC flow +- `web/src/api.js` - Unified Axios instance with auth header management +- `web/src/composables/useAuth.js` - OIDC client configuration + +**Backend Auth**: +- `api/auth.py` - JWT token verification +- `api/main.py` - Protected routes with auth dependency ### Database Models -- **Vehicle**: `id`, `name`, `purchase_date`, `initial_mileage`, `is_deleted` -- **FuelRecord**: `id`, `vehicle_id`, `date`, `mileage`, `fuel_amount`, `fuel_price`, `total_cost`, `is_full_tank` -- **Cost**: `id`, `vehicle_id`, `date`, `type`, `amount`, `mileage`, `is_installment`, `installment_months` + +#### Vehicle +- `id`, `name`, `purchase_date`, `initial_mileage` +- `is_deleted` - Soft delete flag +- `created_at`, `updated_at` + +#### FuelRecord +- `id`, `vehicle_id`, `date`, `mileage` +- `fuel_amount` - Liters +- `fuel_price` - Price per liter +- `display_cost` - Machine display amount (机显总价) +- `actual_cost` - Actual paid amount (实付金额) +- `is_full_tank` - Boolean +- `notes` - Gas station name etc. +- `is_deleted` - Soft delete flag + +#### Cost +- `id`, `vehicle_id`, `date`, `type`, `amount` +- `mileage` - Optional +- `notes` +- `is_installment` - Whether to spread across months +- `installment_months` - Number of months to spread +- `is_deleted` - Soft delete flag + +**Cost Types**: 保养/维修/保险/停车/过路费/洗车/违章/其他 All models use soft delete (`is_deleted` boolean). +## Authentication (Authentik OIDC) + +### Authentik Configuration + +**OIDC Discovery URL**: `https://auth.yuany3721.site/application/o/car-cost/.well-known/openid-configuration` + +**Client ID**: `27UljrOp2LfCg3fjEo1n8vOiRyCFzqEcnBFiUK59` + +**Endpoints**: +- **Authorization**: `https://auth.yuany3721.site/application/o/authorize/` +- **Token**: `https://auth.yuany3721.site/application/o/token/` +- **UserInfo**: `https://auth.yuany3721.site/application/o/userinfo/` +- **End Session**: `https://auth.yuany3721.site/application/o/car-cost/end-session/` + +### Environment Variables + +```bash +# api/.env +DATABASE_URL=postgresql://user:pass@host:port/carcost +DEBUG=true +AUTHENTIK_ISSUER=https://auth.yuany3721.site/application/o/car-cost/ +AUTHENTIK_CLIENT_ID=27UljrOp2LfCg3fjEo1n8vOiRyCFzqEcnBFiUK59 +``` + ## Development Commands ### Backend (api/) ```bash -# Setup (requires Python 3.x) cd api -python -m venv venv source venv/bin/activate -pip install -r requirements.txt # Run dev server python main.py # or uvicorn main:app --host 0.0.0.0 --port 7030 --reload -# Run tests -python test_api.py +# Import fuel records from CSV +python import_fuel_records.py ``` ### Frontend (web/) @@ -72,7 +146,7 @@ python test_api.py cd web npm install -# Dev server (exposes to all hosts) +# Dev server npm run dev # Build for production @@ -82,39 +156,44 @@ npm run build npm run preview ``` +### Deployment +```bash +# Deploy frontend (from carcost root) +./deploy-web.sh +``` + ## Key Configuration Files ### api/.env ``` DATABASE_URL=postgresql://user:pass@host:port/carcost DEBUG=true +AUTHENTIK_ISSUER=https://auth.yuany3721.site/application/o/car-cost/ +AUTHENTIK_CLIENT_ID=27UljrOp2LfCg3fjEo1n8vOiRyCFzqEcnBFiUK59 ``` ### web/vite.config.js -- `server.allowedHosts: ['yuany3721.site', '.yuany3721.site']` - Required for production domain access +- `server.allowedHosts: ['yuany3721.site', '.yuany3721.site']` ## Code Patterns -### Frontend (Vue 3 Composition API) -- Uses ` - - diff --git a/web/src/api.js b/web/src/api.js new file mode 100644 index 0000000..356f9b4 --- /dev/null +++ b/web/src/api.js @@ -0,0 +1,16 @@ +import axios from 'axios' + +// 创建 axios 实例并导出 +const api = axios.create({ + baseURL: 'https://api.yuany3721.site/carcost', + headers: { + 'Content-Type': 'application/json' + } +}) + +// 导出设置 token 的函数 +export function setAuthToken(token) { + api.defaults.headers.common['Authorization'] = token ? `Bearer ${token}` : '' +} + +export default api diff --git a/web/src/components/cards/StatsCards.vue b/web/src/components/cards/StatsCards.vue index 903d45b..7d76a8f 100644 --- a/web/src/components/cards/StatsCards.vue +++ b/web/src/components/cards/StatsCards.vue @@ -1,13 +1,13 @@