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

+47
View File
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Desk phones the PBX provisions (02 devices, 07). Admin only.
import { z } from 'zod';
export const PHONE_VENDORS = ['yealink', 'grandstream', 'generic'] as const;
export type PhoneVendor = (typeof PHONE_VENDORS)[number];
/** Any way of writing a MAC (00:15:65:AB:CD:EF, 0015.65ab.cdef, …) → 12 lowercase hex. */
export const Mac = z
.string()
.trim()
.transform((v, ctx) => {
const hex = v.toLowerCase().replace(/[^0-9a-f]/g, '');
if (hex.length === 12 && /^[0-9a-f:.\-\s]+$/i.test(v)) return hex;
ctx.addIssue({ code: 'custom', message: 'A MAC address like 00:15:65:ab:cd:ef' });
return z.NEVER;
});
const DeviceFields = z.object({
mac: Mac,
vendor: z.enum(PHONE_VENDORS),
model: z.string().trim().max(40).nullable(),
label: z.string().trim().max(60).nullable(),
extensionId: z.uuid('Pick a user'),
});
export const DeviceCreateSchema = DeviceFields.partial().required({
mac: true,
vendor: true,
extensionId: true,
});
export const DeviceUpdateSchema = DeviceFields.partial();
export type DeviceCreate = z.input<typeof DeviceCreateSchema>;
export type DeviceUpdate = z.input<typeof DeviceUpdateSchema>;
export type DeviceDto = z.output<typeof DeviceFields> & {
id: string;
extensionNumber: string;
extensionName: string;
/** What to type into the phone (or hand out with DHCP option 66). */
provisioningUrl: string;
/** Basic auth for the phone: the MAC and this password. Admins see it to type it in. */
provisioningPassword: string;
lastProvisionedAt: string | null;
lastProvisionedIp: string | null;
createdAt: string;
};
+1
View File
@@ -17,6 +17,7 @@ export * from './ringgroups.ts';
export * from './queues.ts';
export * from './conferences.ts';
export * from './paging.ts';
export * from './devices.ts';
export * from './ivrs.ts';
export * from './timeconditions.ts';
export * from './announcements.ts';