provisioning: phones, /prov with Basic auth, rate limit and IP block
/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:
1 parent
dc79120de0
commit
fa63908261
22 files changed
+1266
-43
No files matched your search
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
Reference in new issue
Block a user