provisioning: Linphone setup QR, "Set up a phone app"
GET /api/extensions/:id/softphone-qr (admin) and /api/me/softphone-qr
return {url, qr, expiresAt}: the link Linphone fetches and its QR as a
PNG data URL (qrcode), valid 24 h. Both are audited like revealing the
SIP password. JSON rather than a bare PNG, so the page can show the
link for desktop Linphone too.
The token is stateless per 07 §5: base64url(extensionId.expiry.hmac),
HMAC-SHA256 with APP_SECRET, checked timing-safe. /prov/linphone/
<token>.xml matches it before the other file names are lower-cased
(the token is case-sensitive); a bad or expired token is a 404 and a
counted failure. auth_info keys checked against liblinphone's config
reader; proxy keys stay (verify).
Web: a "Set up a phone app" section on My settings and in the admin
Credentials tab: Linphone with the QR, MicroSIP/Baresip by hand, and
the plain warning about mobile apps without push. e2e m10-t3: as a
user, the QR's link gives the XML and a baresip phone registers with
only what it holds.
Task: M10-T3
This commit is contained in:
1 parent
6195aca2fb
commit
c1f8b04e7e
17 files changed
+631
-4
No files matched your search
@@ -12,8 +12,10 @@ import {
|
||||
type ListResponse,
|
||||
type MeDto,
|
||||
type VoicemailDto,
|
||||
type SoftphoneQr,
|
||||
type WebRtcCreds,
|
||||
} from '@selfphoneng/shared';
|
||||
import { softphoneQr } from '../provisioning/linphone.ts';
|
||||
import { decrypt, encrypt, isTrivialPin } from '../crypto/secrets.ts';
|
||||
import { setDnd, setForward } from '../services/astdb.ts';
|
||||
import { audit } from '../services/audit.ts';
|
||||
@@ -175,6 +177,21 @@ export async function meRoutes(app: FastifyInstance) {
|
||||
return myState(req.user!.id);
|
||||
});
|
||||
|
||||
/** "Set up a phone app" (07 §5): a Linphone setup QR for my own extension. */
|
||||
app.get('/api/me/softphone-qr', async (req): Promise<SoftphoneQr> => {
|
||||
const [ext] = await db.select().from(extensions).where(eq(extensions.userId, req.user!.id));
|
||||
if (!ext) throw notFound("You don't have an extension yet. Ask your admin.");
|
||||
await audit(db, {
|
||||
actorUserId: req.user!.id,
|
||||
actorIp: req.ip,
|
||||
action: 'reveal_sip_credentials',
|
||||
entityType: 'extension',
|
||||
entityId: ext.id,
|
||||
summary: `Made a phone app setup QR for extension ${ext.number}`,
|
||||
});
|
||||
return softphoneQr(app.config.APP_SECRET, app.config.PUBLIC_HOSTNAME, ext.id);
|
||||
});
|
||||
|
||||
app.get('/api/me/webrtc', async (req): Promise<WebRtcCreds> => {
|
||||
const [ext] = await db.select().from(extensions).where(eq(extensions.userId, req.user!.id));
|
||||
if (!ext) throw notFound("You don't have an extension yet. Ask your admin.");
|
||||
|
||||
Reference in new issue
Block a user