conferences: rooms with PIN and admin PIN, per-room limits, recording
/api/conferences CRUD: PINs stored encrypted, shown to admins (they hand
them out), never in the audit log; an admin PIN needs a different room
PIN (checked against the stored values on PATCH). The generator renders
sp-conf-{id} per 03 §6.11 (PIN prompt, 3 tries, admin PIN joins with
sp_admin_user), the number in sp-local-numbers and destApp conference.
Two changes from the spec's snippet, both found with real calls and
written back into 03:
- Set(CONFBRIDGE(bridge,max_members)) is ignored once ConfBridge()
names a bridge profile: a 4th caller got into a room of 3. Each room
now has its own [sp_room_<n>](default_bridge) profile in
confbridge_selfphoneng.conf.
- A recorded room runs sp-sub-record on each caller's leg (just before
ConfBridge) instead of record_conference, so the files are ordinary
call recordings: call history, Recordings page, retention.
Web: Conference rooms page (list, drawer form), "Conference room" in
the DestinationPicker. Golden case conferences. e2e m8-t3: room built
in the UI, wrong PIN refused and asked again, admin PIN joins with
flags AM, a 4th caller turned away, recordings listed, and a no-PIN
room with two desk phones.
Task: M8-T3
This commit is contained in:
1 parent
964a3c0b25
commit
a6c423de97
45 files changed
+1456
-14
No files matched your search
@@ -0,0 +1,38 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Conference rooms (02 conferences, 03 §6.11): a number people dial to talk together.
|
||||
import { z } from 'zod';
|
||||
import { InternalNumber } from './extensions.ts';
|
||||
|
||||
const Pin = z.string().regex(/^[0-9]{4,10}$/, '4 to 10 digits');
|
||||
|
||||
const ConferenceFields = z.object({
|
||||
number: InternalNumber,
|
||||
name: z.string().trim().min(1, 'Required').max(40),
|
||||
/** null = anyone who dials the number gets in. */
|
||||
pin: Pin.nullable(),
|
||||
/** Callers who enter it join as the room's admin (marked user: can kick, mute others). */
|
||||
adminPin: Pin.nullable(),
|
||||
maxMembers: z.number().int().min(2).max(100),
|
||||
/** Every caller's leg is recorded, like a recorded call (call history, Recordings). */
|
||||
record: z.boolean(),
|
||||
});
|
||||
|
||||
const pinsMakeSense = (c: { pin?: string | null; adminPin?: string | null }) =>
|
||||
!c.adminPin || (!!c.pin && c.pin !== c.adminPin);
|
||||
const PINS_MESSAGE = {
|
||||
message: 'An admin PIN needs a (different) room PIN',
|
||||
path: ['adminPin'],
|
||||
};
|
||||
|
||||
export const ConferenceCreateSchema = ConferenceFields.partial()
|
||||
.required({ number: true, name: true })
|
||||
.refine(pinsMakeSense, PINS_MESSAGE);
|
||||
/** The route checks the PINs again against what is stored. */
|
||||
export const ConferenceUpdateSchema = ConferenceFields.partial();
|
||||
export type ConferenceCreate = z.input<typeof ConferenceCreateSchema>;
|
||||
export type ConferenceUpdate = z.input<typeof ConferenceUpdateSchema>;
|
||||
|
||||
/** Admins see the PINs: they hand them out. */
|
||||
export type ConferenceDto = z.output<typeof ConferenceFields> & { id: string; createdAt: string };
|
||||
|
||||
export { pinsMakeSense as conferencePinsMakeSense };
|
||||
@@ -14,6 +14,7 @@ export * from './voicemail.ts';
|
||||
export * from './sounds.ts';
|
||||
export * from './ringgroups.ts';
|
||||
export * from './queues.ts';
|
||||
export * from './conferences.ts';
|
||||
export * from './ivrs.ts';
|
||||
export * from './timeconditions.ts';
|
||||
export * from './announcements.ts';
|
||||
|
||||
Reference in new issue
Block a user