# Car Control — API Server The central API Server for the Car Control project. Written in Go (standard library only, module `carcontrol/api`). It is the **single gateway** between all clients (web app, phone app, Home Assistant plugin, ESP32 device) and the PocketBase database — **clients never talk to PocketBase directly**. The API Server authenticates to PocketBase as superuser and all collection access rules are left null, so data is only reachable through this server. ## Data model (from `Car Service.xlsx`) | Collection | Purpose | Key fields | |---|---|---| | `cars` | one per car (was: one spreadsheet sheet) | name, make, model, year, registration, vin, `currentKm`, `serviceIntervalDays` (365), `serviceIntervalKm` (15000), `oilSpec`, `transmissionOilSpec`, `differentialOilSpec`, `brakeFluidSpec`, `coolantSpec`, `owner` | | `service_records` | the service log | car, date, km, changed_oil, changed_engine_air_filter, changed_cabin_air_filter, notes | | `parts` | per-car parts catalog (cols M/N) | car, name, part_number, category | | `car_shares` | grants another user access to a car | car, user, `permission` (read \| write) | | `sessions` | active login sessions (device/IP/expiry/revoked) | user, label, ip, user_agent, expires, revoked | | `users` | login + profile (built-in auth collection) | name, email, avatar, `role` (user \| admin), bio, theme, locale, date_format, font_size, deletion_requested_at | **Spreadsheet formulas**, reproduced by the API on read: ``` Next Service Date = service date + serviceIntervalDays (Excel: =A+365) Next Service Km = service km + serviceIntervalKm (Excel: =B+15000) ``` These come back on each service record as `nextServiceDate` / `nextServiceKm`. ## Auth & access control All endpoints except `/api/health` and `/api/auth/login` require a bearer token. Login verifies credentials against the PocketBase `users` collection, then the API Server issues its own HS256 JWT (valid 7 days). - **Sessions** — each login also creates a `sessions` record (device label, IP, user-agent, expiry) whose id is embedded as the JWT `jti`. `withAuth` rejects any token whose session is missing or revoked, which powers the "active sessions" list and remote logout in Settings. (Any JWT minted before sessions were introduced has no `jti` and is treated as revoked.) - **Per-user car ownership + sharing** — cars are not a global list. `cars.owner` marks ownership and `car_shares` grants other users `read` or `write` access. Every car/service/part handler is gated by `requireCarAccess`: - **read** — view the car, its service records and parts. - **write** — edit the car and full service/part CRUD. - **owner only** — delete the car and manage its shares. - **Admin role** — `users.role` (`user` | `admin`), embedded in the JWT and re-checked from PocketBase on each admin call (so demotion is immediate). Admins manage users under `/api/admin/*`. Guards prevent deleting your own account or removing/demoting the last admin. ``` POST /api/auth/login { "email": "...", "password": "..." } -> { token, user } GET /api/auth/me (Authorization: Bearer ) -> { id, email, name, role } ``` ## Endpoints ``` GET /api/health POST /api/auth/login GET /api/auth/me # current user (profile / appearance / avatar / data / account lifecycle) GET /api/me PATCH /api/me POST /api/me/password POST /api/me/avatar GET /api/me/avatar DELETE /api/me/avatar POST /api/me/verify/request GET /api/me/export POST /api/me/import POST /api/me/delete POST /api/me/delete/cancel DELETE /api/me # active sessions GET /api/sessions DELETE /api/sessions/{id} DELETE /api/sessions (revoke all others) # admin (admin role required) GET /api/admin/users POST /api/admin/users PATCH /api/admin/users/{id} POST /api/admin/users/{id}/password DELETE /api/admin/users/{id} # cars + sharing GET /api/cars POST /api/cars GET /api/cars/{id} PATCH /api/cars/{id} DELETE /api/cars/{id} GET /api/cars/{id}/service-records GET /api/cars/{id}/parts GET /api/cars/{id}/shares POST /api/cars/{id}/shares DELETE /api/cars/{id}/shares/{userId} # service records + parts GET /api/service-records POST /api/service-records GET /api/service-records/{id} PATCH /api/service-records/{id} DELETE /api/service-records/{id} GET /api/parts POST /api/parts GET /api/parts/{id} PATCH /api/parts/{id} DELETE /api/parts/{id} ``` `GET /api/cars` returns the caller's owned cars plus any shared with them, each annotated with an `access` field. `GET /api/service-records?car={id}` and `GET /api/parts?car={id}` filter by car. > **Gotcha:** `updateCar` rewrites **all** car columns from the payload, so a > `PATCH /api/cars/{id}` must send the **full** car object — omitted spec fields > get blanked. (The phone's odometer quick-edit sends the whole car for this > reason.) ## Layout ``` main.go internal/ ├── api/ # HTTP handlers + router (server.go) │ ├── auth.go services.go records.go parts.go cars.go │ ├── me.go sessions.go shares.go admin.go ├── auth/jwt.go # HS256 JWT mint/verify ├── config/config.go ├── models/models.go └── pb/client.go # PocketBase superuser client scripts/ # Node/Python maintenance scripts (see below) bin/api-server.exe # prebuilt binary the deployment runs ``` ## Configuration Copy `.env.example` to `.env` and fill in: ``` PORT=8080 PB_URL=http://10.2.1.10:8027 PB_ADMIN_EMAIL=... PB_ADMIN_PASSWORD=... AUTH_SECRET= CORS_ORIGINS=http://localhost:5173 ``` `CORS_ORIGINS` only matters for **browser** clients (the web app). Native mobile apps are not subject to CORS. Set `AUTH_SECRET` to a long random value — the server warns and falls back to an insecure dev secret if it is unset. ## Scripts (`scripts/`) ```powershell node scripts/setup-pocketbase.mjs # create/reconcile collections (idempotent) node scripts/create-user.mjs "Name" # create an app login node scripts/set-role.mjs user|admin # promote/demote node scripts/backfill-car-owners.mjs # one-off: assign owner to legacy cars python scripts/seed_from_excel.py "C:/Users/jania/Desktop/Car Service.xlsx" ``` The Node scripts read `PB_URL` / `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD` from the environment (or `.env`). > **PocketBase note:** collections created by the setup script do **not** get > automatic `created`/`updated` autodate fields in this PocketBase version — > sorting on `created` fails unless an explicit `F.autodate(...)` is added. When > adding a new car spec field, extend `DESIRED.cars` in `setup-pocketbase.mjs`, > add it to `models.Car` + the record mapping in `records.go`, then rebuild. ## First-time setup 1. **Create the PocketBase collections** (idempotent): ```powershell $env:PB_URL="http://10.2.1.10:8027" $env:PB_ADMIN_EMAIL="you@example.com" $env:PB_ADMIN_PASSWORD="secret" node scripts/setup-pocketbase.mjs ``` 2. **Run the server** — `go run .`, or build and run the binary (below). 3. **(Optional) Seed from the spreadsheet** with the server running (see scripts). ## Build & run ```powershell go build -o bin/api-server.exe . ``` The deployment runs the **prebuilt binary** `bin/api-server.exe` (not `go run`), started detached so it survives the shell: ```powershell Start-Process -FilePath ".\bin\api-server.exe" -WorkingDirectory "." ` -WindowStyle Hidden -RedirectStandardOutput api-server.out.log ` -RedirectStandardError api-server.err.log ``` Go's `log` package writes to **stderr**, so check `api-server.err.log` for request logs and errors. After editing any Go source, rebuild and restart the process — editing source alone does nothing until the binary is rebuilt.