Files
DriverVault/API Server/README.md
T
tajniak81andClaude Opus 4.8 ae6ed4ac1e Rebuild API Server on the PilotVault structure
Mirror PilotVault's API Server layout and add the superadmin console,
plugin system, runtime PocketBase settings, and user/organization
management. The car domain (cars, service records, parts, sharing) is
carried over unchanged apart from the auth switch.

Layout: main.go -> cmd/server/main.go; module carcontrol/api ->
drivervault/apiserver. internal/api is split by concern (auth, users,
orgs, settings, plugins, status, health, respond).

Auth: replace the server-minted HS256 JWT and the sessions collection
with a PocketBase token proxy. /api/auth/login relays PocketBase's
{token, record}, and every protected request re-resolves that token
against PocketBase, so a role change or deletion takes effect at once
instead of waiting out a token. AUTH_SECRET is obsolete and internal/auth
is gone. Per-device session listing/revocation goes with it: PocketBase
tokens are stateless. Changing a password rotates the user's token key,
which invalidates every token already issued.

Roles: add superadmin alongside user/admin, plus an organizations
collection and users.organization. Admins are scoped to their own
organization; superadmins span all of them. Guards prevent changing your
own role, deleting your own account, an admin touching a superadmin, and
deleting an organization that still has members.

Plugins: new internal/plugins package with one contract over two kinds --
builtin (compiled in) and external (any HTTP service, registered at
runtime with no rebuild). State persists to plugins.json; secrets are
masked on read and preserved when saved back at the mask.

PocketBase settings: /api/admin/pb-config applies a new connection at
runtime and persists it to .env. It deliberately does not require a
working service account, so a wrong or unreachable connection can still
be fixed from the panel.

Panel: rebuilt as the superadmin console -- login gate, status, users,
organizations, PocketBase, plugins, and the endpoint reference.

Clients: update the Web App and Phone App for the PocketBase token shape,
the move of user management to /api/users ({users}/{user} envelopes, with
password resets folded into PATCH), and the removal of sessions. Both now
mirror the server's real guards rather than the old last-admin rule, and
parse PocketBase's field-level error shape.

Config: modern POCKETBASE_*/API_ADDR names with legacy PB_*/PORT
fallbacks, so existing .env files keep working. Also fixes /api/status
probing the Web App on 8090 instead of DriverVault's 5173.

Run scripts/setup-pocketbase.mjs to add the organizations collection and
grow users.role; every client must log in once more.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 22:29:45 +02:00

313 lines
14 KiB
Markdown

# DriverVault — API Server
The central API Server for the DriverVault project. Written in Go (standard
library only, module `drivervault/apiserver`). 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 a superuser and all collection access
rules are left null, so data is only reachable through this server.
It also serves the **superadmin web panel** at the server root (`/`).
## Layout
```
cmd/server/main.go # entry point
internal/
├── api/ # HTTP handlers + router (server.go)
│ ├── auth.go # PocketBase token proxy + role gates
│ ├── users.go # user management (role/org scoped)
│ ├── orgs.go # organization management
│ ├── settings.go # runtime PocketBase connection (pb-config)
│ ├── plugins.go # plugin management endpoints
│ ├── status.go # upstream health probes
│ ├── health.go respond.go panel.go
│ ├── cars.go records.go services.go parts.go shares.go me.go
│ └── dist/ # built panel, embedded via go:embed
├── config/config.go # env + .env load, .env write-back
├── pb/client.go # PocketBase superuser client (runtime-retargetable)
└── plugins/ # plugin system — see plugins/README.md
├── plugin.go manager.go external.go doc.go
└── builtin/ # built-in connectors (none yet)
panel/ # Vue 3 + Tailwind panel source
scripts/ # Node/Python maintenance scripts
bin/api-server.exe # prebuilt binary the deployment runs
```
## Auth & access control
Authentication is **PocketBase's own**. `POST /api/auth/login` is proxied to the
PocketBase users collection and the client keeps the token PocketBase minted;
this server does not issue its own JWT. Every protected request re-resolves that
token against PocketBase (`auth-refresh`), so a role change or a deletion takes
effect **immediately** rather than lingering until a token expires.
```
POST /api/auth/login { "email": "...", "password": "..." } -> PocketBase { token, record }
GET /api/auth/me (Authorization: <token>) -> { id, email, name, role }
GET /api/identity (Authorization: <token>) -> + organization
```
Both `Authorization: Bearer <token>` and a raw `Authorization: <token>` are
accepted (PocketBase's own SDKs send the latter).
### Roles
`users.role` is `user` | `admin` | `superadmin` (empty is treated as `user`).
| Role | Can |
|---|---|
| **user** | their own cars, service records, parts, profile |
| **admin** | the above, plus manage users **within their own organization** |
| **superadmin** | everything, across all organizations, plus the PocketBase connection and plugins |
Guards worth knowing: an admin cannot create or edit a superadmin, cannot move
users between organizations, and nobody can change their own role or delete
their own account. An organization cannot be deleted while it still has members.
### Organizations
`organizations` is the tenant collection; `users.organization` is the
membership. A superadmin spans all organizations; an admin is scoped by the
server to their own. Users may have no organization at all.
### 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.
## Data model
| Collection | Purpose | Key fields |
|---|---|---|
| `cars` | one per car | 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 | car, name, part_number, category |
| `car_shares` | grants another user access to a car | car, user, `permission` (read \| write) |
| `organizations` | tenants | name (unique) |
| `users` | login + profile (built-in auth collection) | name, email, avatar, `role` (user \| admin \| superadmin), `organization`, bio, theme, locale, date_format, font_size, deletion_requested_at |
**Spreadsheet formulas** (from the original `Car Service.xlsx`), 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`.
## Endpoints
```
# public
GET /healthz
GET /api/health
GET /api/status # health of PocketBase + Web App, probed server-side
POST /api/auth/login
GET /api/auth/validate
# identity
GET /api/auth/me
GET /api/identity
# 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
# users + organizations (admin or superadmin; org writes are superadmin-only)
GET /api/users POST /api/users
PATCH /api/users/{id} DELETE /api/users/{id}
GET /api/orgs POST /api/orgs
PATCH /api/orgs/{id} DELETE /api/orgs/{id}
# superadmin
GET /api/admin/pb-config PUT /api/admin/pb-config
POST /api/admin/pb-config/test
GET /api/admin/plugins POST /api/admin/plugins
GET /api/admin/plugins/{name} PUT /api/admin/plugins/{name}
DELETE /api/admin/plugins/{name} POST /api/admin/plugins/{name}/health
# 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.)
## The panel (`/`)
A Vue 3 + Tailwind app (source in `panel/`, built into `internal/api/dist` and
embedded at compile time). It is the superadmin console:
- **Overview** — live health of the API Server, PocketBase and the Web App.
- **Users / Organizations** — full management, scoped to the caller's role.
- **PocketBase** — retarget the database connection at runtime; test before
saving. Applied immediately **and** persisted to `.env`, so it survives a
restart. This is the escape hatch when the configured PocketBase is wrong or
unreachable — the settings endpoints deliberately do **not** require a working
service account.
- **Plugins** — enable/disable/configure integrations, run health checks,
register external plugins.
- **API** — the endpoint reference.
Log in with any DriverVault account; the sections you see depend on your role.
```powershell
cd panel
npm install
npm run dev # localhost:5174, proxies /api to localhost:8080
npm run build # -> ../internal/api/dist (then rebuild the Go binary)
```
Editing panel source alone does nothing to the served panel — run `npm run build`
and then rebuild the Go binary, since `dist` is embedded.
## Plugins
An extension system for integrating third-party services, managed by a
superadmin. Two kinds share one contract: **built-in** (Go, compiled in) and
**external** (any HTTP service, registered at runtime, **no rebuild**). State
persists to `plugins.json`.
See **[`internal/plugins/README.md`](internal/plugins/README.md)** for the full
guide. DriverVault ships no built-in connectors yet; the external kind is the
place to start.
## Configuration
Copy `.env.example` to `.env` and fill in. Summary:
| Variable | Default | Purpose |
|---|---|---|
| `API_ADDR` | `:8080` | listen address (bare port accepted) |
| `POCKETBASE_URL` | `http://10.2.1.10:8027` | PocketBase base URL |
| `POCKETBASE_ADMIN_EMAIL` / `_PASSWORD` | — | superuser service account |
| `CORS_ALLOW_ORIGINS` | `*` | comma-separated browser origins |
| `WEBAPP_URL` | `http://localhost:5173` | probed by `/api/status` |
| `AUTH_USERS_COLLECTION` | `users` | PocketBase auth collection |
| `PLUGINS_FILE` | `plugins.json` | plugin state store |
`PB_URL`, `PB_ADMIN_EMAIL`, `PB_ADMIN_PASSWORD`, `PORT` and `CORS_ORIGINS` are
still honoured for older deployments; the modern names win when both are set.
`CORS_ALLOW_ORIGINS` only matters for **browser** clients (the web app). Native
mobile apps are not subject to CORS.
The service account is **optional at startup**: without it the server still runs
and a superadmin can log in and configure it from the panel, while management
endpoints return 503.
## 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|superadmin
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 `POCKETBASE_URL` / `POCKETBASE_ADMIN_EMAIL` /
`POCKETBASE_ADMIN_PASSWORD` (or the legacy `PB_*` names) from the environment.
> **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 — safe to re-run on an
existing deployment; it adds `organizations`, `users.organization`, and grows
`users.role` to include `superadmin`):
```powershell
$env:POCKETBASE_URL="http://10.2.1.10:8027"
$env:POCKETBASE_ADMIN_EMAIL="you@example.com"
$env:POCKETBASE_ADMIN_PASSWORD="secret"
node scripts/setup-pocketbase.mjs
```
2. **Mint the first superadmin.** The panel's management screens need one, and
only a superadmin can promote another — so the first has to come from the
script:
```powershell
node scripts/create-user.mjs admin@example.com "a-long-password" "Admin"
node scripts/set-role.mjs admin@example.com superadmin
```
3. **Run the server** — `go run ./cmd/server`, or build and run the binary.
4. **Open the panel** at `http://localhost:8080/` and sign in.
5. **(Optional) Seed from the spreadsheet** with the server running (see scripts).
## Build & run
```powershell
go build -o bin/api-server.exe ./cmd/server
```
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.
## Migrating from the JWT build
Earlier builds minted their own HS256 JWT and tracked a `sessions` collection
for "active devices" / remote logout. That is gone — the server now relays
PocketBase tokens. Consequences:
- **`AUTH_SECRET` is obsolete** and ignored.
- **Login returns PocketBase's envelope** — `{token, record}`, not `{token, user}`.
- **`GET|DELETE /api/sessions*` are gone.** PocketBase tokens are stateless, so
there is nothing to revoke per-device. To lock every device out of an account,
change its password: PocketBase rotates the user's token key, which
invalidates every token already issued.
- **User management moved** from `/api/admin/users*` to `/api/users*`, and the
separate password-reset endpoint folded into `PATCH /api/users/{id}`
(`{"password": "..."}`). Responses are enveloped: `{users}` / `{user}`.
- **Existing tokens are invalid** — every client must log in once more.
- The `sessions` collection is left in PocketBase rather than dropped; delete it
by hand if you want it gone.
The Web App and Phone App in this repo are already updated for all of the above.