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:
iamdoubz committed 2026-10-17 16:35:07 -05:00
1 parent a4da81cf8f
commit 1cfc13ead3
12 files changed
+1167 -141

No files matched your search

+25 -1
View File
@@ -1,6 +1,17 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// Typed fetch wrappers (05 §1). Every request sends X-Requested-With (CSRF, 04 §2.2).
import type { AuthUser, ListResponse, UserCreate, UserDto, UserUpdate } from '@selfphoneng/shared';
import type {
AuthUser,
ExtensionCreate,
ExtensionDto,
ExtensionUpdate,
ListResponse,
NumberKind,
SipCredentials,
UserCreate,
UserDto,
UserUpdate,
} from '@selfphoneng/shared';
export class ApiError extends Error {
readonly status: number;
@@ -50,3 +61,16 @@ export const usersApi = {
resetPassword: (id: string) =>
api<{ temporaryPassword: string }>('POST', `/api/users/${id}/reset-password`),
};
export const extensionsApi = {
get: (id: string) => api<ExtensionDto>('GET', `/api/extensions/${id}`),
create: (e: ExtensionCreate) => api<ExtensionDto>('POST', '/api/extensions', e),
update: (id: string, e: ExtensionUpdate) =>
api<ExtensionDto>('PATCH', `/api/extensions/${id}`, e),
remove: (id: string) => api<{ ok: true }>('DELETE', `/api/extensions/${id}`),
credentials: (id: string) => api<SipCredentials>('GET', `/api/extensions/${id}/sip-credentials`),
regenerateSipPassword: (id: string) =>
api<{ ok: true }>('POST', `/api/extensions/${id}/regenerate-sip-password`),
nextNumber: (kind: NumberKind) =>
api<{ number: string | null }>('GET', `/api/numbering/next?kind=${kind}`).then((r) => r.number),
};