Update all documentation to current MySQL + React SPA + vm_api.py architecture
README, docs/{ARCHITECTURE,CONFIGURATION,INSTALL,OPERATIONS,SECURITY,TESTING}
and CHANGELOG were stale: they described the original SQLite + CardDAV +
zero-JS vm_web portal. Brought them in line with the actual code:
- MySQL (PyMySQL) as the default store; SQLite fallback only
- vm_contacts resolves from the MySQL contacts table (file/google/carddav removed)
- React SPA frontend + vm_api.py JSON API (:8098); vm_web.py now serves /audio/
- Apache vhost proxies /api/ -> :8098, /audio/ -> :8099, serves the SPA
- CSP relaxed to script-src 'self' 'unsafe-inline' (UI is now JavaScript)
- Added CHANGELOG 1.2.0 entry for the React/vm_api frontend work
This commit is contained in:
@ -1,9 +1,8 @@
|
||||
# Configuration
|
||||
|
||||
All config lives in `/opt/vm-transcribe/`. Templates are committed as
|
||||
`*.example`; the live files are created by `install.sh` or by you. Secret-bearing
|
||||
files (`telegram.conf`, `contacts.conf`) are mode `640 root:asterisk` and git-
|
||||
ignored.
|
||||
The backend services run from `/opt/vm-transcribe/`. Live secret-bearing files
|
||||
live there (some git-ignored). The repo ships `*.example` templates under
|
||||
`config/`.
|
||||
|
||||
Most settings can also be set via environment variables, which is how the systemd
|
||||
unit wires production values. Env vars override file values when both are present
|
||||
@ -11,17 +10,47 @@ unless noted.
|
||||
|
||||
---
|
||||
|
||||
## Environment variables (shared)
|
||||
## Environment variables
|
||||
|
||||
| Variable | Default | Used by | Meaning |
|
||||
|---|---|---|---|
|
||||
| `VM_DB` | `/var/lib/vm-transcribe/voicemail.db` | store, web, import | SQLite path. Created with WAL mode if missing. |
|
||||
| `VM_AUDIO_DIR` | `/var/lib/vm-transcribe/audio` | store, web, import | Where copied recordings live (`<sha[:2]>/<sha>.wav`). |
|
||||
| `VM_LOG` | `/var/log/asterisk/vm_mailcmd.log` | mailcmd | Log path. |
|
||||
| `VM_MODEL_CACHE` | `/opt/vm-transcribe/models` | mailcmd, import | Whisper model download/load dir. |
|
||||
| `VM_WHISPER_MODEL` | `base.en` | mailcmd, import | Model name. `tiny`/`base`/`small` trade speed for accuracy. |
|
||||
| `VM_SPOOL` | `/var/spool/asterisk/voicemail` | import | Spool root to scan. |
|
||||
| `VM_ASTERISK_CONF` | `/etc/asterisk/voicemail.conf` | web, auth | For PIN lookup. |
|
||||
### Shared / store (`vm_store.py`, used by mailcmd, web, api, import)
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `VM_DB` | `/var/lib/vm-transcribe/voicemail.db` | SQLite path — **used only as a fallback** when no MySQL is configured. |
|
||||
| `VM_AUDIO_DIR` | `/var/lib/vm-transcribe/audio` | Where copied recordings live (`<sha[:2]>/<sha>.wav`). |
|
||||
| `VM_LOG` | `/var/log/asterisk/vm_mailcmd.log` | Log path. |
|
||||
| `VM_MODEL_CACHE` | `/opt/vm-transcribe/models` | Whisper model download/load dir. |
|
||||
| `VM_WHISPER_MODEL` | `base.en` | Model name. `tiny`/`base`/`small` trade speed for accuracy. |
|
||||
| `VM_SPOOL` | `/var/spool/asterisk/voicemail` | Spool root to scan (import). |
|
||||
| `VM_ASTERISK_CONF` | `/etc/asterisk/voicemail.conf` | For PIN lookup. |
|
||||
|
||||
### MySQL (default store + contacts)
|
||||
|
||||
Set these as env vars (the API unit reads `/opt/vm-transcribe/api.env`) **or**
|
||||
put them in `/opt/vm-transcribe/db_secret` (mode `640 root:root`,
|
||||
`KEY=VALUE` lines). When a credentials source exists, `vm_store` uses MySQL;
|
||||
otherwise it falls back to SQLite.
|
||||
|
||||
| Variable | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `VM_MYSQL_HOST` | `localhost` | MySQL host. |
|
||||
| `VM_MYSQL_USER` | `asterisk` | Least-privilege DB user (owns the `asterisk` DB; not root). |
|
||||
| `VM_MYSQL_PASSWORD` | (from `db_secret`) | DB password. |
|
||||
| `VM_MYSQL_DB` | `asterisk` | Database name — the **same** DB Asterisk's CDR uses. |
|
||||
|
||||
`db_secret` key names: `MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`,
|
||||
`MYSQL_DB`.
|
||||
|
||||
### Portal / API hardening
|
||||
|
||||
| Var | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `VM_SESSION_HOURS` | `12` | Session lifetime. |
|
||||
| `VM_MAX_FAILS` | `5` | Failed attempts per (mailbox, IP) before locking. |
|
||||
| `VM_LOCK_MINUTES` | `15` | Lock duration. |
|
||||
| `VM_INSECURE_COOKIE` | unset | Set to `1`/`yes`/`true` only for plain-HTTP testing; normally cookies are `Secure`. |
|
||||
| `VM_BASE_PATH` | `""` | Set to e.g. `/voicemail` if served under a sub-path. |
|
||||
| `VM_CORS_ORIGINS` | `*` | Comma list of allowed CORS origins for the JSON API. |
|
||||
|
||||
---
|
||||
|
||||
@ -88,41 +117,38 @@ file still delivers the summary.
|
||||
|
||||
```ini
|
||||
[contacts]
|
||||
backends = file ; space/comma separated: file google carddav
|
||||
cache_ttl = 86400 ; seconds; cache hits AND misses
|
||||
cache_file = /var/lib/vm-transcribe/contacts-cache.json
|
||||
enabled = yes
|
||||
backends = mysql ; ONLY 'mysql' is implemented (MySQL contacts table).
|
||||
# The schema still parses file/google/carddav entries,
|
||||
# but the registry is {"mysql": lookup_mysql} so any
|
||||
# other value is ignored / logged as unknown.
|
||||
cache_path = /var/lib/vm-transcribe/contacts_cache.json
|
||||
cache_ttl = 86400
|
||||
|
||||
# --- 'file' backend (recommended): a vCard or CSV export -------------
|
||||
[file]
|
||||
path = /var/lib/vm-transcribe/contacts.vcf
|
||||
# CSV (Google export "All contact data") is auto-detected:
|
||||
# path = /var/lib/vm-transcribe/contacts.csv
|
||||
|
||||
# --- 'google' backend: People API via OAuth ---------------------------
|
||||
[google]
|
||||
token_file = /var/lib/vm-transcribe/google-token.json
|
||||
# Obtain a token with the People API (contacts.readonly) scope. See
|
||||
# docs/SECURITY.md. No app password here — OAuth only.
|
||||
|
||||
# --- 'carddav' backend: app password, Nextcloud/Fastmail/iCloud --------
|
||||
[carddav]
|
||||
# Google rejects app passwords (basic auth disabled 2024-09-30); a Google
|
||||
# URL here is refused with a log warning and the backend is skipped.
|
||||
url = https://contacts.fastmail.com/dav/addressbooks/user/xxx/Default
|
||||
username = jp@txt3.com
|
||||
app_password = ; app-specific password, not your login password
|
||||
[file] ; NOT USED — kept for reference only
|
||||
[google] ; NOT USED — kept for reference only
|
||||
[carddav] ; NOT USED — kept for reference only
|
||||
```
|
||||
|
||||
**Matching** is on the **last 9 digits** of the caller ID, so `+447700900123`,
|
||||
`07700900123` and `447700900123` all collapse to the same contact. Names resolve
|
||||
best-effort; on any backend error the caller ID is used as-is.
|
||||
**Contacts are resolved exclusively from the MySQL `contacts` table** on the
|
||||
`asterisk` DB. Numbers are stored canonical E.164; `normalize_uk()` collapses
|
||||
`+44…` and `0…` (and `0044…`) so both forms match. Matching uses the full digit
|
||||
string (up to 15 digits), not just last-9. Creating/updating a contact via the
|
||||
API backfills `messages.contact_name`/`contact_email` for matching callerids.
|
||||
|
||||
Load the table from a vCard/CSV export with:
|
||||
|
||||
```bash
|
||||
sudo -u asterisk /opt/vm-transcribe/venv/bin/python3 \
|
||||
/opt/vm-transcribe/vm_import_contacts.py /path/to/contacts.vcf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Portal settings (per mailbox, in the SQLite store)
|
||||
## Portal settings (per mailbox, in MySQL)
|
||||
|
||||
These are edited from the portal's **Settings** page by each user; they are not
|
||||
config files. Every value defaults to `yes`.
|
||||
These are edited from the React portal's **Settings** page by each user; they are
|
||||
not config files. Every value defaults to `yes`.
|
||||
|
||||
| Setting | Effect |
|
||||
|---|---|
|
||||
@ -134,26 +160,33 @@ config files. Every value defaults to `yes`.
|
||||
|
||||
---
|
||||
|
||||
## vm-portal.service
|
||||
## systemd units
|
||||
|
||||
Production values are passed as environment in the unit file:
|
||||
Two units, both running as `asterisk` and binding loopback only.
|
||||
|
||||
### vm-api.service (:8098 JSON API)
|
||||
|
||||
```ini
|
||||
Environment=VM_SESSION_HOURS=12
|
||||
EnvironmentFile=-/opt/vm-transcribe/api.env ; MySQL creds + VM_* overrides
|
||||
# Hardening: ProtectSystem=full, ProtectHome=read-only, NoNewPrivileges,
|
||||
# PrivateTmp, IPAddressAllow=127.0.0.1
|
||||
ExecStart=/opt/vm-transcribe/venv/bin/uvicorn vm_api:app --host 127.0.0.1 --port 8098 --workers 2
|
||||
```
|
||||
|
||||
### vm-portal.service (:8099 legacy HTML / audio)
|
||||
|
||||
```ini
|
||||
Environment=VM_DB=/var/lib/vm-transcribe/voicemail.db
|
||||
Environment=VM_AUDIO_DIR=/var/lib/vm-transcribe/audio
|
||||
Environment=VM_ASTERISK_CONF=/etc/asterisk/voicemail.conf
|
||||
Environment=VM_SESSION_HOURS=12
|
||||
ExecStart=/opt/vm-transcribe/venv/bin/python -m uvicorn vm_web:app \
|
||||
--host 127.0.0.1 --port 8099 --proxy-headers --forwarded-allow-ips 127.0.0.1
|
||||
# Hardening: NoNewPrivileges, PrivateTmp, ProtectSystem=full, ProtectHome,
|
||||
# ReadWritePaths=/var/lib/vm-transcribe /var/log/asterisk,
|
||||
# IPAddressAllow=localhost, IPAddressDeny=any
|
||||
```
|
||||
|
||||
Login hardening (in-process):
|
||||
|
||||
| Var | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `VM_MAX_FAILS` | `5` | Failed attempts per (mailbox, IP) before locking. |
|
||||
| `VM_LOCK_MINUTES` | `15` | Lock duration. |
|
||||
| `VM_SESSION_HOURS` | `12` | Session lifetime. |
|
||||
| `VM_INSECURE_COOKIE` | unset | Set to `1` only for plain-HTTP testing; normally cookies are `Secure`. |
|
||||
| `VM_BASE_PATH` | `""` | Set to e.g. `/voicemail` if the app is served under a sub-path. |
|
||||
|
||||
> A service restart clears the in-memory lockout counters. Testing lockout will
|
||||
> lock you out of your own verification for `VM_LOCK_MINUTES`.
|
||||
|
||||
Reference in New Issue
Block a user