provisioning: phones, /prov with Basic auth, rate limit and IP block
ci / check (push) Successful in 52s
ci / images (push) Successful in 1m44s

/api/devices CRUD (MAC in any format → 12 lowercase hex; a 20-char
provisioning password, stored encrypted and shown to admins, who type
it into the phone) and GET /:id/config-preview with passwords masked.

GET /prov/... per 07 §2: <mac>.cfg (Yealink), cfg<mac>.xml
(Grandstream), the empty Yealink common file. Basic auth with the MAC
and the device's password (timing-safe), 404 for unknown MACs or the
other vendor's file, 403 outside the local nets when LAN-only, 404 when
provisioning is off, Cache-Control no-store, last provisioned time and
IP recorded. Limits: 20 requests / 10 min per IP outside the local nets
(an office's phones rebooting together would pass it), and 10 failures
block the IP for an hour. A first request without credentials isn't a
failure: phones ask that way and retry after the 401.

Templates (pure functions of a ProvisionContext) hold the account,
voicemail, pickup codes and the provisioning settings; BLF, time zone
and phonebook come with M10-T2.

Web: Phones page (table, drawer with what to type into the phone,
config preview; manual settings for generic phones). e2e m10-t1: phone
added in the UI, 401 then the file, and a baresip phone registers with
only the account from it.

Also: the loadSnapshot test made its database inside the test (20 s);
under a full parallel run that timed out now and then. It now does it
in a hook like the others.

Task: M10-T1
This commit is contained in:
iamdoubz committed 2026-10-19 13:38:18 -05:00
1 parent dc79120de0
commit fa63908261
22 files changed
+1266 -43

No files matched your search

-1
View File
@@ -648,7 +648,6 @@ COPY --from=build /out/node_modules ./node_modules
COPY --from=build /src/apps/api/dist ./dist
COPY --from=build /src/apps/web/dist ./public
COPY --from=build /src/apps/api/src/db/migrations ./dist/db/migrations
# ponytail: provisioning templates (M10) get their COPY line when they exist.
ENV NODE_ENV=production
ENTRYPOINT ["/usr/bin/tini","--"]
CMD ["node","dist/index.js"]
+1 -1
View File
@@ -386,7 +386,7 @@ Admin resources (all `requireAdmin`; standard CRUD = `GET /x`, `POST /x`, `GET /
| `/api/paging-groups` | members are set via `memberIds: [uuid]` in POST/PATCH (no order). Not a destination, so delete never returns 409 |
| `/api/sounds` | upload is `POST /api/sounds?name=&filename=` with the raw file as the body (like greetings, no multipart), `GET /:id/audio`, `PATCH /:id {name}`, `DELETE /:id` (409 with `usedBy`: IVR greeting/invalid sound, announcements, the recording notice), `POST /tts` (M12). M6 lists prompts only; MOH classes and files come with M8 |
| `/api/moh-classes` | `POST {name}` (slug, not `default`), `DELETE /:id` (409 with `usedBy` while an extension or queue uses it), `POST /:id/files?name=&filename=` (raw file body, like sounds), `GET /:id/files/:fileId/audio`, `DELETE /:id/files/:fileId`. No rename: the name is the folder and is stored on extensions and queues. Extensions and queues may only pick `default` or an existing class (400) |
| `/api/devices` | `GET /:id/config-preview` (rendered provisioning file, passwords masked) |
| `/api/devices` | `GET /:id/config-preview` (rendered provisioning file, passwords masked). As built (M10-T1): body `{mac (any format), vendor, model, label, extensionId}`; the DTO carries `provisioningUrl` and the `provisioningPassword` (admins type it into the phone); MAC taken → 409; preview of a generic phone → 400 |
| `/api/blocked-numbers` | |
| `/api/contacts` | company phonebook; `POST /import` CSV (`name,company,email,label1,number1,...`) |
| `/api/webhooks` | `POST /:id/test`, `GET /:id/deliveries` |
+1 -1
View File
@@ -40,7 +40,7 @@ Left sidebar navigation, in this order:
| Conferences | `/admin/conferences` | number, name, PIN, admin PIN, max members, record. As built (M8-T3): admins see the PINs in the list (they hand them out); an admin PIN needs a different room PIN |
| Paging | `/admin/paging` | number, name, members, duplex. As built (M8-T4): members are checkboxes (no order); the empty state explains `*80<ext>` |
| Sounds & music | `/admin/sounds` | Prompts list with play buttons, upload (drag-drop), TTS (M12). MOH classes with their files (M8). As built (M8-T5): a **Music on hold** section: make a set (slug name), upload, play and remove its files, delete it. The user drawer's Phone tab and the queue form pick a set from a select. An **Announcements** section (play a sound, then a destination) lives here too: no page of its own (M6-T5) |
| Phones (devices) | `/admin/devices` | Table: MAC, vendor, model, user, last provisioned. Form: MAC (any format → normalized), vendor, model, extension. Shows the provisioning URL to type into the phone, and "Preview config" |
| Phones (devices) | `/admin/devices` | Table: MAC, vendor, model, user, last provisioned. Form: MAC (any format → normalized), vendor, model, extension. Shows the provisioning URL to type into the phone, and "Preview config". As built (M10-T1): the drawer shows URL, user name (MAC) and password; a generic phone shows the manual settings instead (07 §4); Grandstream gets a note that line keys aren't set |
| Contacts | `/admin/contacts` | Company phonebook, CSV import |
| Call history | `/admin/calls` | Filters + table + CSV export; a row expands to show the CDR legs and plays the recording |
| Recordings | `/admin/recordings` | Searchable list, play, download, delete |
+15
View File
@@ -46,6 +46,21 @@ Rules:
- On success: update `last_provisioned_at`, `last_provisioned_ip`. Set `Cache-Control: no-store`.
- Never serve over plain HTTP. The reverse proxy only listens on 443. (Old phones that can't do modern TLS are not supported.)
As built (M10-T1), `routes/provisioning.ts` + `provisioning/limiter.ts`:
- A request **without** an `Authorization` header gets the 401 but isn't a failure: phones ask without
credentials first and retry after the challenge. Failures are unknown MACs (or a file for another
vendor) and wrong credentials; 10 within an hour block the IP for an hour (429).
- The 20 requests / 10 min limit applies to clients **outside** `network.localNets`: an office's
phones rebooting together behind one address would pass it in a minute. Failures count for all.
- The Basic auth user name is the 12-hex MAC (either case); it can't carry colons (Basic auth splits
at the first one). The Yealink common file `y0000000000xx.cfg` is served without auth (it's empty).
- Unrecognized file names (phones ask for several) are a plain 404 and not a failure.
- Desk phones are provisioned for UDP on `sip.udpPort`. Templates in M10-T1 hold the account,
voicemail, pickup codes and the provisioning settings; BLF keys, time zone and the phonebook come
with M10-T2. `scripts/e2e/m10-t1.mjs` registers a baresip phone with only the account from the
served file.
- Vendors offered: Yealink, Grandstream, generic (Polycom stays a stretch goal).
## 3. Templates
The key names below come from vendor documentation. Each one is marked