|
|
@@ -42,44 +42,49 @@ route('POST', '/api/mfa/enable', async (req, res) => {
|
|
42
|
42
|
// Provision (or refresh) a local user from a successful BizGaze identity check.
|
|
43
|
43
|
// The local row exists so sessions, audit, and team-scoped data work; BizGaze stays
|
|
44
|
44
|
// the source of truth for credentials (the local password is random + unused).
|
|
|
45
|
+// Emails that must always be admins regardless of what BizGaze returns (safety net so an
|
|
|
46
|
+// admin can't be locked out of the report if BizGaze doesn't flag them isAdmin). Optional.
|
|
|
47
|
+const ADMIN_EMAILS = (process.env.ADMIN_EMAILS || '').split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
45
|
48
|
function provisionFromBizgaze(email, bz) {
|
|
|
49
|
+ const role = (bz.isAdmin || ADMIN_EMAILS.includes(String(email).toLowerCase())) ? 'admin' : 'technician';
|
|
46
|
50
|
const existing = R.users.byEmail(email);
|
|
47
|
51
|
if (!existing) {
|
|
48
|
52
|
const team = R.teams.first() || R.teams.create('BizGaze');
|
|
49
|
53
|
const { hash, salt } = A.hashPassword(A.token());
|
|
50
|
|
- const role = bz.isAdmin ? 'admin' : 'technician';
|
|
51
|
54
|
const id = R.users.create({ tenantId: team.id, email, hash, salt, role, name: bz.name || null, mfaSecret: A.newMfaSecret() });
|
|
52
|
55
|
audit({ team_id: team.id, user_id: id, user_email: email, action: 'sso_user_created', detail: 'via BizGaze' });
|
|
53
|
56
|
return R.users.byId(id);
|
|
54
|
57
|
}
|
|
|
58
|
+ // BizGaze is the source of truth: keep name + role in sync on each login.
|
|
55
|
59
|
if (bz.name && bz.name !== existing.name) R.users.setName(existing.id, bz.name);
|
|
|
60
|
+ if (existing.role !== role) R.users.setRole(existing.id, role);
|
|
56
|
61
|
return R.users.byId(existing.id);
|
|
57
|
62
|
}
|
|
58
|
63
|
|
|
59
|
|
-// Login: validates locally first (seeded/bootstrap accounts), then against BizGaze
|
|
60
|
|
-// (the identity provider) when BIZGAZE_LOGIN_URL is configured. Sets a session cookie.
|
|
|
64
|
+// Login: when BizGaze (BIZGAZE_LOGIN_URL) is configured it is the ONLY authority — the
|
|
|
65
|
+// credentials are verified against BizGaze and the user is provisioned/synced locally
|
|
|
66
|
+// (local passwords are not accepted). Without it (dev/tests) the local password is
|
|
|
67
|
+// checked. Sets a session cookie.
|
|
61
|
68
|
route('POST', '/api/login', async (req, res) => {
|
|
62
|
69
|
const { email, password, remember } = await readBody(req);
|
|
63
|
70
|
if (!email || !password) return json(res, 400, { error: 'email and password required' });
|
|
64
|
71
|
const existing = R.users.byEmail(email);
|
|
65
|
72
|
if (existing && existing.active === 0) return json(res, 403, { error: 'This account has been deactivated' });
|
|
66
|
73
|
|
|
67
|
|
- let u = (existing && A.verifyPassword(password, existing.pw_salt, existing.pw_hash)) ? existing : null;
|
|
68
|
|
- let bzMsg = null;
|
|
69
|
|
- if (!u) {
|
|
|
74
|
+ let u = null;
|
|
|
75
|
+ if (BZ.isEnabled()) {
|
|
|
76
|
+ // BizGaze is the identity provider: credentials are ALWAYS verified against BizGaze.
|
|
|
77
|
+ // Local passwords are NOT accepted, so stale in-app accounts can't shadow a BizGaze
|
|
|
78
|
+ // login and everyone provisions into the same tenant (admins then see all sessions).
|
|
70
|
79
|
const bz = await BZ.validateLogin(email, password);
|
|
71
|
|
- if (bz.ok) u = provisionFromBizgaze(email, bz);
|
|
72
|
|
- else if (bz.error) return json(res, 503, { error: bz.error });
|
|
73
|
|
- else bzMsg = bz.message || null; // BizGaze was configured and rejected the credentials
|
|
74
|
|
- }
|
|
75
|
|
- if (!u) {
|
|
76
|
|
- // Specific feedback where we can be truthful:
|
|
77
|
|
- if (existing) return json(res, 401, { error: 'Incorrect password. Please try again.' });
|
|
78
|
|
- // No local account. BizGaze (the identity provider) doesn't reveal whether an email
|
|
79
|
|
- // exists, so when it rejects we surface its own message (covers wrong password +
|
|
80
|
|
- // any lockout warning). Only when BizGaze isn't in play can we say "not registered".
|
|
81
|
|
- if (bzMsg) return json(res, 401, { error: bzMsg });
|
|
82
|
|
- return json(res, 404, { error: 'This email is not registered.' });
|
|
|
80
|
+ if (bz.error) return json(res, 503, { error: bz.error });
|
|
|
81
|
+ if (!bz.ok) return json(res, 401, { error: bz.message || 'Username or password do not match.' });
|
|
|
82
|
+ u = provisionFromBizgaze(email, bz);
|
|
|
83
|
+ if (u && u.active === 0) return json(res, 403, { error: 'This account has been deactivated' });
|
|
|
84
|
+ } else {
|
|
|
85
|
+ // No identity provider configured (local/dev/tests): verify the local password.
|
|
|
86
|
+ u = (existing && A.verifyPassword(password, existing.pw_salt, existing.pw_hash)) ? existing : null;
|
|
|
87
|
+ if (!u) return json(res, existing ? 401 : 404, { error: existing ? 'Incorrect password. Please try again.' : 'This email is not registered.' });
|
|
83
|
88
|
}
|
|
84
|
89
|
|
|
85
|
90
|
const tok = A.token();
|
|
|
@@ -177,6 +182,9 @@ route('POST', '/api/users', async (req, res) => {
|
|
177
|
182
|
const u = currentUser(req);
|
|
178
|
183
|
if (!u) return json(res, 401, { error: 'unauthorized' });
|
|
179
|
184
|
if (u.role !== 'admin') return json(res, 403, { error: 'only admins can add agents' });
|
|
|
185
|
+ // With BizGaze as the identity provider, logins are created in BizGaze — not here.
|
|
|
186
|
+ // (Creating local accounts is what previously shadowed BizGaze and split tenants.)
|
|
|
187
|
+ if (BZ.isEnabled()) return json(res, 400, { error: 'Logins are managed in BizGaze. Add the user in BizGaze; they appear here on first sign-in.' });
|
|
180
|
188
|
const { email, password, name, role } = await readBody(req);
|
|
181
|
189
|
if (!email || !password) return json(res, 400, { error: 'email and temporary password required' });
|
|
182
|
190
|
if (R.users.emailExists(email))
|