Initial commit
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
# 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 <token>) -> { 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=<long random value>
|
||||
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 <email> <pw> "Name" # create an app login
|
||||
node scripts/set-role.mjs <email> 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.
|
||||
Reference in New Issue
Block a user