trunks: CRUD API, PJSIP trunk config, live registration status, Trunks page
voip.ms-first trunk form with the portal checklist, POP server list and Test button; passwords are write-only and encrypted. Registration and IP-auth trunks render per 03 §5.3 (TCP/TLS add ;transport=). The API refuses a server name that doesn't resolve and transports that are off, because either makes Asterisk drop the trunk. Live state: registration from PJSIPShowRegistrationsOutbound (re-read on every Registry event: two sub-accounts on one server share Username/Domain), reachability from PJSIPShowEndpoint and trunk ContactStatus (static contacts aren't in PJSIPShowContacts). Trunk state is admin-only. Dashboard gets a "Trunks up" card. trunk-test.ts (in pnpm test:asterisk) runs a fake voip.ms (second Asterisk): the good trunk registers, a wrong password is Rejected, qualify reaches the provider; its recorded AMI drives the unit tests. scripts/e2e/m3-t1.mjs does the same through the UI. The acceptance check with a real voip.ms sub-account is still to do. Tests: settle() now awaits an apply instead of a fixed sleep (flaked under full-suite load). Task: M3-T1
This commit is contained in:
1 parent
c0c4036887
commit
1b7f60b5f2
48 files changed
+2749
-33
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { alias } from 'drizzle-orm/pg-core';
|
||||
import type { Db } from '../db/client.ts';
|
||||
import { dids, extensions, users } from '../db/schema.ts';
|
||||
import { dids, extensions, trunks, users } from '../db/schema.ts';
|
||||
import { decrypt } from '../crypto/secrets.ts';
|
||||
import { getAllSettings } from '../services/settings.ts';
|
||||
|
||||
@@ -28,12 +28,30 @@ export type SnapExtension = {
|
||||
|
||||
export type SnapDid = { id: string; number: string };
|
||||
|
||||
export type SnapTrunk = {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
serverHost: string;
|
||||
serverPort: number;
|
||||
transport: 'udp' | 'tcp' | 'tls';
|
||||
authMode: 'registration' | 'ip';
|
||||
username: string | null;
|
||||
password: string | null; // decrypted
|
||||
fromUser: string | null;
|
||||
fromDomain: string | null;
|
||||
codecs: string[];
|
||||
srtp: boolean;
|
||||
maxChannels: number | null;
|
||||
};
|
||||
|
||||
export type Snapshot = {
|
||||
/** "YYYY-MM-DD" in general.timezone; render() never reads the clock. */
|
||||
today: string;
|
||||
settings: Awaited<ReturnType<typeof getAllSettings>>;
|
||||
extensions: SnapExtension[];
|
||||
dids: SnapDid[]; // M3 adds routing fields
|
||||
trunks: SnapTrunk[];
|
||||
};
|
||||
|
||||
export function todayIn(timezone: string, now = new Date()): string {
|
||||
@@ -58,10 +76,27 @@ export async function loadSnapshot(db: Db, now = new Date()): Promise<Snapshot>
|
||||
.leftJoin(e911Did, eq(e911Did.id, extensions.emergencyCallerIdDidId));
|
||||
|
||||
const didRows = await db.select({ id: dids.id, number: dids.number }).from(dids);
|
||||
const trunkRows = await db.select().from(trunks);
|
||||
return {
|
||||
today: todayIn(settings['general.timezone'], now),
|
||||
settings,
|
||||
dids: didRows,
|
||||
trunks: trunkRows.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
enabled: t.enabled,
|
||||
serverHost: t.serverHost,
|
||||
serverPort: t.serverPort,
|
||||
transport: t.transport,
|
||||
authMode: t.authMode,
|
||||
username: t.username,
|
||||
password: t.passwordEnc ? decrypt(t.passwordEnc) : null,
|
||||
fromUser: t.fromUser,
|
||||
fromDomain: t.fromDomain,
|
||||
codecs: t.codecs,
|
||||
srtp: t.srtp,
|
||||
maxChannels: t.maxChannels,
|
||||
})),
|
||||
extensions: rows.map(({ ext, role, outCid, e911Cid }) => ({
|
||||
id: ext.id,
|
||||
number: ext.number,
|
||||
|
||||
Reference in new issue
Block a user