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:
@@ -24,6 +24,9 @@ const users = {
|
||||
byId: (id) => db.prepare('SELECT * FROM users WHERE id=?').get(id),
|
||||
byEmail: (email) => db.prepare('SELECT * FROM users WHERE email=? COLLATE NOCASE').get(email),
|
||||
emailExists: (email) => !!db.prepare('SELECT 1 FROM users WHERE email=? COLLATE NOCASE').get(email),
|
||||
// Match on the stable BizGaze person-id so email- and mobile-logins resolve to one account (#2).
|
||||
byBizgazeId: (bizId) => (bizId ? db.prepare('SELECT * FROM users WHERE bizgaze_user_id=?').get(String(bizId)) : undefined),
|
||||
setBizgazeId: (id, bizId) => db.prepare('UPDATE users SET bizgaze_user_id=? WHERE id=?').run(bizId != null ? String(bizId) : null, id),
|
||||
listByTenant: (tenantId) =>
|
||||
db.prepare('SELECT id,email,name,role,active,avatar_url,created_at FROM users WHERE team_id=?').all(tenantId),
|
||||
inTenant: (id, tenantId) =>
|
||||
@@ -43,6 +46,49 @@ const users = {
|
||||
setAvatar: (id, url) => db.prepare('UPDATE users SET avatar_url=? WHERE id=?').run(url || null, id),
|
||||
setStatus: (id, status) => db.prepare('UPDATE users SET status=? WHERE id=?').run(status, id),
|
||||
remove: (id) => db.prepare('DELETE FROM users WHERE id=?').run(id),
|
||||
// Fold one account (fromId) into another (intoId): reassign everything the merged-away user
|
||||
// owns/authored to the survivor, then delete the empty row. Used when a person turns out to
|
||||
// have two Biz Connect accounts — one per login identifier (email vs mobile) — for the same
|
||||
// BizGaze person (#2). Runs in a single transaction so a failure leaves the data untouched.
|
||||
// For composite-key tables we UPDATE OR IGNORE (move what won't collide) then DELETE the rest
|
||||
// (the survivor already has that membership/reaction/vote).
|
||||
mergeInto: (fromId, intoId) => {
|
||||
if (!fromId || !intoId || fromId === intoId) return;
|
||||
const run = (sql, ...a) => db.prepare(sql).run(...a);
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
run('UPDATE messages SET sender_id=? WHERE sender_id=?', intoId, fromId);
|
||||
run('UPDATE messages SET recipient_id=? WHERE recipient_id=?', intoId, fromId);
|
||||
run('UPDATE OR IGNORE message_reactions SET user_id=? WHERE user_id=?', intoId, fromId);
|
||||
run('DELETE FROM message_reactions WHERE user_id=?', fromId);
|
||||
run('UPDATE OR IGNORE conversation_members SET user_id=? WHERE user_id=?', intoId, fromId);
|
||||
run('DELETE FROM conversation_members WHERE user_id=?', fromId);
|
||||
run('UPDATE OR IGNORE poll_votes SET user_id=? WHERE user_id=?', intoId, fromId);
|
||||
run('DELETE FROM poll_votes WHERE user_id=?', fromId);
|
||||
run('UPDATE OR IGNORE favorites SET user_id=? WHERE user_id=?', intoId, fromId);
|
||||
run('DELETE FROM favorites WHERE user_id=?', fromId);
|
||||
// A DM favourite pointing AT the merged-away user should now point at the survivor.
|
||||
run("UPDATE OR IGNORE favorites SET target='dm:'||? WHERE target='dm:'||?", intoId, fromId);
|
||||
run("DELETE FROM favorites WHERE target='dm:'||?", fromId);
|
||||
run('UPDATE conversations SET created_by=? WHERE created_by=?', intoId, fromId);
|
||||
run('UPDATE polls SET created_by=? WHERE created_by=?', intoId, fromId);
|
||||
run('UPDATE scheduled_meetings SET created_by=? WHERE created_by=?', intoId, fromId);
|
||||
run('UPDATE recordings SET created_by=? WHERE created_by=?', intoId, fromId);
|
||||
run('UPDATE attachments SET uploader_id=? WHERE uploader_id=?', intoId, fromId);
|
||||
run('UPDATE push_subscriptions SET user_id=? WHERE user_id=?', intoId, fromId);
|
||||
run('UPDATE OR IGNORE device_tokens SET user_id=? WHERE user_id=?', intoId, fromId);
|
||||
run('DELETE FROM device_tokens WHERE user_id=?', fromId);
|
||||
run('UPDATE app_installs SET user_id=? WHERE user_id=?', intoId, fromId);
|
||||
// Drop the merged-away account's auth so a stale token can't resurrect the empty row.
|
||||
run('DELETE FROM sessions_auth WHERE user_id=?', fromId);
|
||||
run('DELETE FROM refresh_tokens WHERE user_id=?', fromId);
|
||||
run('DELETE FROM users WHERE id=?', fromId);
|
||||
db.exec('COMMIT');
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const authSessions = {
|
||||
|
||||
Reference in New Issue
Block a user