Merge origin/master (TURN/coturn + BizGaze-only login) into feature tree

Resolved conflicts in routes.js and share.html: kept the dev tree's superset
(ALLOW_LOCAL_LOGIN dev escape, avatar sync, richer login errors) which already
includes the incoming production BizGaze-only behavior; took the more descriptive
incoming comments. Restored 5 untracked modules (chat, calls, directory,
reminders, webhooks) that were missing from disk — required by routes/signaling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 16:27:59 +05:30
12 changed files with 523 additions and 13 deletions
+18 -8
View File
@@ -124,7 +124,8 @@ route('POST', '/api/mfa/enable', async (req, res) => {
// Provision (or refresh) a local user from a successful BizGaze identity check.
// The local row exists so sessions, audit, and team-scoped data work; BizGaze stays
// the source of truth for credentials (the local password is random + unused).
// Emails that must always be admins regardless of what BizGaze returns (lockout safety net).
// Emails that must always be admins regardless of what BizGaze returns (safety net so an
// admin can't be locked out of the report if BizGaze doesn't flag them isAdmin). Optional.
const ADMIN_EMAILS = (process.env.ADMIN_EMAILS || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
function provisionFromBizgaze(email, bz) {
const role = (bz.isAdmin || ADMIN_EMAILS.includes(String(email).toLowerCase())) ? 'admin' : 'technician';
@@ -144,8 +145,10 @@ function provisionFromBizgaze(email, bz) {
return R.users.byId(existing.id);
}
// Login: validates locally first (seeded/bootstrap accounts), then against BizGaze
// (the identity provider) when BIZGAZE_LOGIN_URL is configured. Sets a session cookie.
// Login: when BizGaze (BIZGAZE_LOGIN_URL) is configured it is the ONLY authority — the
// credentials are verified against BizGaze and the user is provisioned/synced locally
// (local passwords are not accepted). Without it (dev/tests) the local password is
// checked. Sets a session cookie.
route('POST', '/api/login', async (req, res) => {
const { email, password, remember } = await readBody(req);
if (!email || !password) return json(res, 400, { error: 'email and password required' });
@@ -154,7 +157,8 @@ route('POST', '/api/login', async (req, res) => {
// Production: when BizGaze is the IdP, verify ONLY against BizGaze (no local-password
// fallback) so stale in-app accounts can't shadow a BizGaze login and everyone lands in
// the same tenant. Local accounts stay usable for dev/testing via ALLOW_LOCAL_LOGIN=1.
// the same tenant (admins then see all sessions). Local accounts stay usable for
// dev/testing via ALLOW_LOCAL_LOGIN=1.
const bizgazeOnly = BZ.isEnabled() && process.env.ALLOW_LOCAL_LOGIN !== '1';
let u = null, bzMsg = null;
if (bizgazeOnly) {
@@ -164,6 +168,8 @@ route('POST', '/api/login', async (req, res) => {
u = provisionFromBizgaze(email, bz);
if (u && u.active === 0) return json(res, 403, { error: 'This account has been deactivated' });
} else {
// Local/dev/tests, or ALLOW_LOCAL_LOGIN=1: verify the local password, then fall back
// to BizGaze if a local password isn't set/correct (so SSO users can still sign in).
u = (existing && A.verifyPassword(password, existing.pw_salt, existing.pw_hash)) ? existing : null;
if (!u) {
const bz = await BZ.validateLogin(email, password);
@@ -234,8 +240,11 @@ route('GET', '/api/setup-state', async (req, res) => {
});
// ICE servers for WebRTC. Always includes a public STUN; adds our TURN relay if
// configured. TURN_SECRET (coturn use-auth-secret) -> time-limited HMAC credentials
// (no permanent password exposed); otherwise static TURN_USERNAME + TURN_CREDENTIAL.
// configured. Two credential modes:
// - Shared secret (recommended, coturn `use-auth-secret`): set TURN_SECRET and we mint
// time-limited credentials per request (no permanent password is ever handed out, so
// outsiders can't reuse your relay). Optional TURN_TTL seconds (default 24h).
// - Static: set TURN_USERNAME + TURN_CREDENTIAL for a fixed long-term credential.
route('GET', '/api/ice', async (req, res) => {
const iceServers = [{ urls: 'stun:stun.l.google.com:19302' }];
if (process.env.TURN_URLS) {
@@ -244,7 +253,7 @@ route('GET', '/api/ice', async (req, res) => {
let credential = process.env.TURN_CREDENTIAL || '';
if (process.env.TURN_SECRET) {
const ttl = parseInt(process.env.TURN_TTL || '86400', 10);
username = String(Math.floor(Date.now() / 1000) + ttl);
username = String(Math.floor(Date.now() / 1000) + ttl); // coturn expects "<expiry>"
credential = require('crypto').createHmac('sha1', process.env.TURN_SECRET).update(username).digest('base64');
}
iceServers.push({ urls, username, credential });
@@ -299,7 +308,8 @@ route('POST', '/api/users', async (req, res) => {
if (!u) return json(res, 401, { error: 'unauthorized' });
if (u.role !== 'admin') return json(res, 403, { error: 'only admins can add agents' });
// With BizGaze as the sole IdP, logins are created in BizGaze, not here (creating local
// accounts is what previously shadowed BizGaze and split tenants). Allowed in dev.
// accounts is what previously shadowed BizGaze and split tenants). Allowed in dev via
// ALLOW_LOCAL_LOGIN=1.
if (BZ.isEnabled() && process.env.ALLOW_LOCAL_LOGIN !== '1') return json(res, 400, { error: 'Logins are managed in BizGaze. Add the user there; they appear here on first sign-in.' });
const { email, password, name, role } = await readBody(req);
if (!email || !password) return json(res, 400, { error: 'email and temporary password required' });