Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 11x 11x 11x 11x 11x 1x 1x 10x 10x 10x 10x 10x 430x 430x 430x 430x 10x 10x 10x 10x 80x 80x 80x 80x 80x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x | import { eq, sql } from 'drizzle-orm'
import { db, schema } from '@/db'
/**
* Merge a guest user's data into an existing authenticated user.
*
* This handles the "same person, different device" scenario:
* User signs in on Device B with Google, but they already have an
* authenticated account from Device A. Device B has a guest user row
* with its own players/data that needs to be merged into the existing account.
*
* All data from sourceUserId is re-parented to targetUserId, then the
* source user record is deleted.
*/
export async function mergeGuestIntoUser(
sourceUserId: string,
targetUserId: string
): Promise<void> {
if (sourceUserId === targetUserId) {
return
}
console.log(`[auth] merging guest user ${sourceUserId} into ${targetUserId}`)
// Helper to re-parent rows from source to target in a given table/column
async function reparent(table: string, column: string) {
await db.run(
sql`UPDATE ${sql.identifier(table)} SET ${sql.identifier(column)} = ${targetUserId} WHERE ${sql.identifier(column)} = ${sourceUserId}`
)
}
// Helper for tables with unique constraint on user_id (one row per user).
// Delete the guest's row if the target user already has one; otherwise re-parent.
async function reparentOrDrop(table: string, column: string) {
await db.run(
sql`DELETE FROM ${sql.identifier(table)} WHERE ${sql.identifier(column)} = ${sourceUserId} AND EXISTS (SELECT 1 FROM ${sql.identifier(table)} AS t2 WHERE t2.${sql.identifier(column)} = ${targetUserId})`
)
await reparent(table, column)
}
// Tables with user_id FK (multi-row, no unique constraint on user_id)
await reparent('players', 'user_id')
await reparent('arcade_sessions', 'user_id')
await reparent('custom_skills', 'user_id')
await reparent('mcp_api_keys', 'user_id')
await reparent('teacher_flowcharts', 'user_id')
await reparent('skill_customizations', 'user_id')
await reparent('workshop_sessions', 'user_id')
// Arcade tables
await reparent('arcade_rooms', 'created_by')
await reparentOrDrop('room_members', 'user_id')
await reparent('room_bans', 'user_id')
await reparent('room_bans', 'banned_by')
await reparent('room_reports', 'reporter_id')
await reparent('room_reports', 'reported_user_id')
await reparent('room_reports', 'reviewed_by')
await reparent('room_invitations', 'user_id')
await reparent('room_invitations', 'invited_by')
await reparent('room_join_requests', 'user_id')
await reparent('room_join_requests', 'reviewed_by')
await reparent('room_member_history', 'user_id')
// Worksheet tables
await reparentOrDrop('worksheet_settings', 'user_id')
await reparent('worksheet_mastery', 'user_id')
await reparent('worksheet_attempts', 'user_id')
await reparent('problem_attempts', 'user_id')
// Game results and observation shares
await reparent('game_results', 'user_id')
await reparent('session_observation_shares', 'created_by')
await reparent('session_songs', 'content_reviewed_by')
// Tables with unique constraint on user_id (one settings row per user)
// Prefer the target user's settings; drop the guest's if both exist
await reparentOrDrop('abacus_settings', 'user_id')
await reparentOrDrop('scanner_settings', 'user_id')
await reparentOrDrop('user_stats', 'user_id')
await reparentOrDrop('feature_flag_overrides', 'user_id')
await reparentOrDrop('subscriptions', 'user_id')
// Tables with differently named FK columns
await reparent('classrooms', 'teacher_id')
await reparent('parent_child', 'parent_user_id')
await reparent('entry_prompts', 'teacher_id')
await reparent('entry_prompts', 'responded_by')
await reparent('practice_attachments', 'uploaded_by')
await reparent('classroom_presence', 'entered_by')
await reparent('enrollment_requests', 'requested_by')
await reparent('enrollment_requests', 'parent_approved_by')
// Family audit log
await reparent('family_events', 'actor_user_id')
// Household tables
await reparent('household_members', 'user_id')
await reparentOrDrop('households', 'owner_id')
// Notification subscriptions
await reparent('practice_notification_subscriptions', 'user_id')
// Delete the source user (CASCADE will clean up auth_accounts and any remaining FKs)
await db.delete(schema.users).where(eq(schema.users.id, sourceUserId))
console.log(`[auth] merge complete: ${sourceUserId} → ${targetUserId}`)
}
|