feat(auth): merge mobile+email logins into one account via BizGaze person-id (#2)
Provisioning now keys the Biz Connect account on bz.bizgazeUserId (the same value whether the person signs in with email or mobile) instead of the typed identifier, so both logins resolve to a single contact. Legacy rows get the person-id stamped on next login; an existing duplicate created under the same identifier is folded in via a transactional users.mergeInto() that reassigns all messages/memberships/reactions/votes/favorites/ownership to the survivor. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+28
-2
@@ -130,16 +130,42 @@ route('POST', '/api/mfa/enable', async (req, res) => {
|
||||
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';
|
||||
const existing = R.users.byEmail(email);
|
||||
const bizId = bz.bizgazeUserId || null;
|
||||
|
||||
// Identity is keyed on the BizGaze person-id, NOT the typed identifier: signing in with a
|
||||
// mobile number and with an email both return the same person-id, so both resolve to one
|
||||
// Biz Connect account (#2 — no more duplicate contacts for the same person).
|
||||
let existing = R.users.byBizgazeId(bizId);
|
||||
// Legacy account created before the person-id was stored: fall back to the typed identifier,
|
||||
// but only if it isn't already claimed by a different person, then stamp the id on below.
|
||||
if (!existing) {
|
||||
const byMail = R.users.byEmail(email);
|
||||
if (byMail && (!byMail.bizgaze_user_id || byMail.bizgaze_user_id === bizId)) existing = byMail;
|
||||
}
|
||||
|
||||
if (!existing) {
|
||||
const team = R.teams.first() || R.teams.create('BizGaze');
|
||||
const { hash, salt } = A.hashPassword(A.token());
|
||||
const id = R.users.create({ tenantId: team.id, email, hash, salt, role, name: bz.name || null, mfaSecret: A.newMfaSecret() });
|
||||
if (bizId) R.users.setBizgazeId(id, bizId);
|
||||
if (bz.avatarUrl) R.users.setAvatar(id, bz.avatarUrl);
|
||||
audit({ team_id: team.id, user_id: id, user_email: email, action: 'sso_user_created', detail: 'via BizGaze' });
|
||||
return R.users.byId(id);
|
||||
}
|
||||
// BizGaze is the source of truth: keep name + avatar + role in sync on each login.
|
||||
// Retroactive merge: if this same identifier already has its OWN legacy account (a separate
|
||||
// row created before person-id keying — e.g. the person used email before and is now signing
|
||||
// in with their mobile), fold that duplicate's history into the canonical account. BizGaze just
|
||||
// proved this identifier belongs to this person, so the merge is safe.
|
||||
if (bizId) {
|
||||
const dup = R.users.byEmail(email);
|
||||
if (dup && dup.id !== existing.id && (!dup.bizgaze_user_id || dup.bizgaze_user_id === bizId)) {
|
||||
R.users.mergeInto(dup.id, existing.id);
|
||||
audit({ team_id: existing.team_id, user_id: existing.id, user_email: email, action: 'account_merged', detail: 'folded duplicate ' + dup.id });
|
||||
}
|
||||
}
|
||||
// BizGaze is the source of truth: keep the person-id + name + avatar + role in sync each login.
|
||||
// Stamping the id links legacy rows so the person's other identifier converges here next time.
|
||||
if (bizId && existing.bizgaze_user_id !== bizId) R.users.setBizgazeId(existing.id, bizId);
|
||||
if (bz.name && bz.name !== existing.name) R.users.setName(existing.id, bz.name);
|
||||
if (bz.avatarUrl && bz.avatarUrl !== existing.avatar_url) R.users.setAvatar(existing.id, bz.avatarUrl);
|
||||
if (existing.role !== role) R.users.setRole(existing.id, role);
|
||||
|
||||
Reference in New Issue
Block a user