extensions: numbering service, CRUD API, credentials, Users page tabs
One namespace for internal numbers (range, reserved codes, parking, every object table) with a next-free lookup. Creating an extension generates its SIP password and voicemail PIN and a runtime-state row; every change and password regeneration triggers a config apply. Credentials are revealed only on request and each reveal is audited. Users get an Ext. column and a drawer with General (number + next free), Phone and Credentials tabs. scripts/e2e/m2-t5.mjs checks the flow in a browser and that the generated PJSIP file follows; the files it produced load cleanly in Asterisk. Task: M2-T5
This commit is contained in:
1 parent
a4da81cf8f
commit
1cfc13ead3
12 files changed
+1167
-141
No files matched your search
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Extension (the telephony identity of a user) as the API accepts and returns it (02, 04 §7.3).
|
||||
// The patterns match the generator's sanitizers (03 §2), so valid input always renders.
|
||||
import { z } from 'zod';
|
||||
|
||||
export const InternalNumber = z.string().regex(/^\d{3,6}$/, 'Use 3 to 6 digits');
|
||||
const Slug = z.string().regex(/^[a-z0-9-]{1,32}$/, 'Lowercase letters, digits and dashes');
|
||||
const Pin = z.string().regex(/^\d{4,10}$/, 'Use 4 to 10 digits');
|
||||
|
||||
const ExtensionFields = z.object({
|
||||
number: InternalNumber,
|
||||
userId: z.uuid().nullable(),
|
||||
name: z.string().trim().min(1, 'Required').max(40),
|
||||
webrtcEnabled: z.boolean(),
|
||||
maxContacts: z.number().int().min(1).max(10),
|
||||
ringSeconds: z.number().int().min(5).max(120),
|
||||
voicemailEnabled: z.boolean(),
|
||||
voicemailPin: Pin, // write-only
|
||||
voicemailEmail: z.boolean(),
|
||||
voicemailAttachAudio: z.boolean(),
|
||||
voicemailDeleteAfterEmail: z.boolean(),
|
||||
outboundClass: z.enum(['internal', 'local', 'national', 'international']),
|
||||
outboundCallerIdDidId: z.uuid().nullable(),
|
||||
emergencyCallerIdDidId: z.uuid().nullable(),
|
||||
emergencyLocation: z.string().trim().max(200).nullable(),
|
||||
recordMode: z.enum(['never', 'inbound', 'outbound', 'all']),
|
||||
pickupGroup: Slug.nullable(),
|
||||
language: z.string().regex(/^[a-z]{2,3}$/, 'A language code like en'),
|
||||
mohClass: Slug,
|
||||
blfNumbers: z.array(InternalNumber).max(32),
|
||||
});
|
||||
|
||||
export const ExtensionCreateSchema = ExtensionFields.partial().required({
|
||||
number: true,
|
||||
name: true,
|
||||
});
|
||||
export const ExtensionUpdateSchema = ExtensionFields.partial();
|
||||
|
||||
export type ExtensionCreate = z.input<typeof ExtensionCreateSchema>;
|
||||
export type ExtensionUpdate = z.input<typeof ExtensionUpdateSchema>;
|
||||
|
||||
export type ExtensionDto = Omit<z.output<typeof ExtensionFields>, 'voicemailPin'> & {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
export type SipCredentials = {
|
||||
server: string;
|
||||
port: number;
|
||||
transport: 'udp' | 'tcp';
|
||||
username: string;
|
||||
password: string;
|
||||
/** Browser softphone identity (when webrtc is on). */
|
||||
webUsername: string | null;
|
||||
};
|
||||
|
||||
export const NUMBER_KINDS = ['user', 'ringGroup', 'queue', 'ivr', 'conference', 'paging'] as const;
|
||||
export type NumberKind = (typeof NUMBER_KINDS)[number];
|
||||
@@ -5,3 +5,4 @@ export * from './destination.ts';
|
||||
export * from './settings.ts';
|
||||
export * from './auth.ts';
|
||||
export * from './users.ts';
|
||||
export * from './extensions.ts';
|
||||
@@ -31,6 +31,7 @@ export type UserDto = {
|
||||
mustChangePassword: boolean;
|
||||
lastLoginAt: string | null;
|
||||
createdAt: string;
|
||||
extension: { id: string; number: string } | null;
|
||||
};
|
||||
|
||||
export type ListResponse<T> = { items: T[]; nextCursor: string | null };
|
||||
Reference in new issue
Block a user