Commit Graph
12 Commits
Author SHA1 Message Date
iamdoubz 97e67e347d auth: security audit fixes, rate limits on every route
ci / check (push) Successful in 1m3s
ci / images (push) Successful in 19s
ci / check (pull_request) Successful in 1m5s
ci / images (pull_request) Successful in 5s
Two separate reviews of the SSO branch and all 207 API routes (spec 12 §8.6). Every route was
guarded as meant. Fixed, each with a test:
- login: a directory 503 counts as a failed sign-in; a locked directory account is a refused
  password, not "unavailable"
- open redirect after OpenID sign-in (a tab in next)
- directory email matching only for non-admins without their own password; others connect on
  My settings (POST /api/me/identities/ldap); only admins disconnect a directory account
- OpenID session check reads the refreshed ID token's claims too
- connect is POST /api/auth/oidc/connect (CSRF header)
- stored bind password, client secret and SMTP password tied to their server
- 409 role_managed when a provider sets the role; empty Contrack PATCH is a 400
- @fastify/rate-limit on every route (RATE_LIMIT_PER_MINUTE), 10/min on Test buttons; a
  password asked again shares the login limiter
Live tests pass against lldap 0.6.3, Samba 4 AD and Keycloak 26.8.1.
2026-10-26 09:46:23 -05:00
iamdoubz 164a5f0990 backups: daily backup, Back up now, download, restore.sh
ci / check (push) Successful in 54s
ci / images (push) Successful in 20s
A backup is data/backups/selfphoneng-YYYYMMDD-HHMM.tar.gz (mode 0600):
pg_dump, voicemail, custom sounds, music on hold, recordings if asked,
and a manifest. No generated Asterisk config: it holds plaintext
passwords and comes back from the database. The api makes one a day after
backup.time (once-a-minute check, one try a day) and the dashboard warns
when today's is missing. Settings -> Backups sets the schedule, lists them
for download and has Back up now.

scripts/restore.sh <file> refuses a backup made under another APP_SECRET
(the manifest carries a fingerprint, not the secret), asks for yes, then
replaces the database and the folders the backup has.

Tested on two throwaway servers (docker:dind running the real images):
back up on one, restore on the other. That found the api image crashing
on start since M1: tsc doesn't copy auth/common-passwords.txt into dist.
The Dockerfile copies it now and CI loads the app inside the image.

Task: M11-T3
2026-10-19 17:07:58 -05:00
iamdoubz 33b1456c0f voipms: API client, POP server list, import numbers
integrations/voipms/client.ts: voipmsApi(url, creds, method, params)
per 06 §5.1 (GET rest.php, 15 s timeout, status "success" or an error
code). The API password is only in the query string: never logged,
never in an error. Known codes get a message for people
(invalid_credentials, ip_not_enabled, ...). getServers and getDids
parse forgivingly; no_did is an empty list.

Routes: GET /api/trunks/:id/voipms-servers, GET and POST
/api/trunks/:id/import-dids. Import adds only numbers voip.ms lists
and that aren't added yet, label from the description, destination
hangup. 400 without API credentials, 502 provider_error with voip.ms's
reason. VOIPMS_API_URL overrides the endpoint for tests.

Web: the trunk form's server list comes from voip.ms once the trunk
has API access, falling back to the built-in list with the reason.
"Import numbers from voip.ms" sits in the trunk form's API section and
the setup wizard's numbers step. Saving a new API password asks
voip.ms again (found in e2e: the old error stayed).

The response shapes are from the 06 spec, not a real account: voip.ms's
API docs need a login. Marked TODO(spec) and (verify) in 06. Tests use
a fake rest.php; e2e m9-t1 too (trunk created off, so nothing registers
to the real voip.ms).

Task: M9-T1
2026-10-19 12:41:45 -05:00
iamdoubz b8ff6e3386 moh: music-on-hold classes, uploads, pickers for users and queues
ci / check (push) Successful in 51s
ci / images (push) Successful in 18s
/api/moh-classes: create (slug, not "default"), delete (409 while an
extension or queue uses it), add/play/remove files. Files are 8 kHz
wav in MOH_DIR/<class>/ and sound_files rows of kind moh. Extensions
and queues may only pick default or an existing class.

Two things found with real calls, written back into 03 and 04:
- An empty class plays silence on Asterisk 24. Only classes with
  music are rendered, so a channel asking for an empty one gets
  default.
- The generator reloads only changed files, and a file upload changes
  no config (except a class's first/last). The route sends moh reload
  after every file change.

Web: a Music on hold section on Sounds & music, and a select of sets
in the user drawer's Phone tab and the queue form. load-golden makes
the class folders like the api does. Golden case queue gains two
classes. e2e m8-t5: set made in the UI, upload listed by Asterisk, a
queue caller hears it, a user's moh_suggest, delete refused while
used, an emptied set falls back to default.

Task: M8-T5
2026-10-19 10:10:01 -05:00
iamdoubz 4b6b95090a recordings: record calls, link them to history, Recordings page
sp-sub-record now also sets CDR(SP_REC); phone numbers can record every
call (record_inbound), with the notice from Settings → Recording played
first (SP_REC_ANNOUNCE). Aggregation links the MixMonitor file as a
recordings row (size, ffprobe length). /api/recordings lists, plays
(Range; admins and the people on the call) and deletes; recordings past
recordings.retentionDays are deleted daily.

Web: Recordings page (play, download, delete), a player in Call history
and My calls, a Recording settings tab with the consent notice (08 §5),
and "Record every call" on phone numbers.

e2e m7-t3: the notice plays before a recorded inbound call and the
recording plays from Call history (the M7 acceptance). The AMI
Originate helper moved to scripts/e2e/ami.mjs.

Task: M7-T3
2026-10-18 14:57:52 -05:00
iamdoubz 17db68a8c5 sounds: upload, convert, play, rename, delete with references
/api/sounds (admin) takes the raw file, converts it with ffmpeg in one
pass to <id>.wav (8 kHz) and <id>.g722 (HD for G.722 calls) in SOUNDS_DIR,
measures it with ffprobe, and stores it as custom/<id>. Delete answers 409
with what still plays it (IVR greeting/invalid sound, announcements, the
recording notice). Sounds & music page: drop or pick a file (name
suggested from the file name), play, rename, delete. MOH classes stay in
M8. The web client and greetings share one raw-upload helper; the
api test audio helpers moved to test/audio.ts.

e2e m6-t1: drag-and-drop upload in the browser, Asterisk plays
custom/<id>, rename, delete refused while in use, then deleted.

Task: M6-T1
2026-10-18 10:13:58 -05:00
iamdoubz f60dda0bb0 me: voicemail page, greetings, My Phone home
ci / images (push) Canceled after 0s
ci / check (push) Canceled after 25s
/api/me/voicemail lists, streams (Range, download) and marks read or
deletes through AMI VoicemailMove/VoicemailRemove, then rescans so MWI
and the list agree. /api/me/greetings takes the raw recording or file and
converts it with ffmpeg to 8 kHz PCM (media/audio.ts). My Phone gets a
layout with Home (new voicemails) and Voicemail (play marks read,
download, delete, record/upload greetings via MediaRecorder); the list
refetches on the live voicemail.new event. e2e m4-t3 checks MWI goes off
within 5 s after play and delete, and a browser-recorded greeting is what
callers hear.

Task: M4-T3
2026-10-17 22:56:10 -05:00
iamdoubz 409724bc55 emergency: 911 dialplan, notifications, test button, settings, warnings
Dialplan: sp-emergency-dial (911, 9911) first in sp-from-ext;
sp-emergency has no class, channel-limit or DND check, dials every
enabled trunk in name order with the E911 caller ID, and raises
UserEvent SpEmergency. Static sp-emergency-alert (_X! so "0" matches)
for the ring-out.

api: SpEmergency -> emergency_events row, live emergency.call (admins
and notify users only), email via nodemailer (SMTP from settings) and
an Originate to every notify extension, each outcome recorded. Test
button runs the same with TEST in front, records and dials nothing.
GET/PATCH /api/settings (smtp password write-only, references
checked, read-only system keys), SMTP test, /api/system/warnings.
Fix: clearing a nullable setting stored SQL NULL (now JSON null).

UI: Settings page (Email, Emergency with the 06 §6 notice), user
Emergency tab, dashboard warnings banner, red 911 banner.

Verified: generator/emergency.test.ts; trunk-test.ts dials 911 from a
caller with no class to the fake provider (E911 caller ID in RPID,
SpEmergency on AMI, alert plays on baresip 101); scripts/e2e/m3-t4.mjs
(SMTP sink receives the test email, 101 rings, banner shows).
nodemailer pinned to 10.0.11 (newest release over two weeks old).

Task: M3-T4
2026-10-17 20:52:50 -05:00
iamdoubz 1b7f60b5f2 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
2026-10-17 19:42:14 -05:00
iamdoubz c0c4036887 live: AMI-fed live state, /api/live WebSocket, dashboard and status dots
ci / check (push) Canceled after 0s
ci / images (push) Canceled after 0s
LiveState loads extension states and registered devices from Asterisk on
every AMI connect and follows ExtensionStatus/ContactStatus; the hub sends
a snapshot then deltas batched every 250 ms (devices for admins only),
pings every 30 s and closes unauthenticated sockets with 4401. The web UI
keeps one reconnecting socket in a zustand store, shows a Reconnecting
banner, a Dashboard (online, on a call, telephony engine) and status dots.

Fix: Asterisk 24 has no 'pjsip reload'; reload with
'module reload res_pjsip.so'. AMI errors now carry the CLI output.

Milestone check, automated: call-test.ts registers two baresip phones and
holds a 30 s call with RTP both ways (pnpm test:asterisk); the recorded
session drives the live-state tests. scripts/e2e/m2-live.mjs runs api +
Asterisk + phones + browser on a private Docker network: a new extension
is live in Asterisk in ~1.3 s, the dashboard shows 2/2 online and red dots
during the call.

Task: M2-T6
2026-10-17 16:49:25 -05:00
iamdoubz a5de247948 generator: core apply loop, sanitizers, system config routes
render() is pure and gives every file the header (bodies arrive per task).
apply() runs one at a time, debounces requestApply, writes changed files
atomically (mode 0640), keeps .prev, sends each needed reload once, skips
pjsip reload and sets restartRequired when transports change, and records
generator_runs. AMI connects on startup and every reconnect forces a full
apply. Admin routes: config apply/rollback and a masked generated-files view.

Task: M2-T2
2026-10-17 16:17:13 -05:00
iamdoubz 13733ac262 auth: sessions, login/logout/me/change-password, CSRF, guards, audit
argon2id passwords with a min length and a common-password list (SecLists,
MIT). Login failures are limited per IP and per email; must_change_password
blocks everything outside /api/auth/*. The first admin is created from env
on startup. pnpm dev derives DATABASE_URL from .env.

Task: M1-T3
2026-10-17 15:08:44 -05:00