#!/usr/bin/env node /** * Migration script - Migrate data from JSON files to database */ const fs = require('fs'); const path = require('path'); const { initDatabase, getDatabase, closeDatabase } = require('../src/database/connection'); const { initEncryption, encrypt } = require('../src/utils/encryption'); const { STATE_DIR, DB_PATH, KEY_FILE } = require('../src/database/config'); const DATABASE_PATH = DB_PATH; const DATABASE_ENCRYPTION_KEY = process.env.DATABASE_ENCRYPTION_KEY; const DATABASE_USE_SQLCIPHER = process.env.DATABASE_USE_SQLCIPHER !== '0' && process.env.DATABASE_USE_SQLCIPHER !== 'false'; const USERS_FILE = path.join(STATE_DIR, 'users.json'); const SESSIONS_FILE = path.join(STATE_DIR, 'user-sessions.json'); const AFFILIATES_FILE = path.join(STATE_DIR, 'affiliates.json'); async function loadJsonFile(filePath, defaultValue = []) { try { const data = fs.readFileSync(filePath, 'utf8'); return JSON.parse(data); } catch (error) { if (error.code === 'ENOENT') { console.log(` File not found: ${filePath}, using default`); return defaultValue; } throw error; } } async function migrateUsers() { console.log('\nšŸ“¦ Migrating users...'); const users = await loadJsonFile(USERS_FILE, []); console.log(` Found ${users.length} users in JSON`); if (users.length === 0) { console.log(' No users to migrate'); return { success: 0, failed: 0 }; } let success = 0; let failed = 0; const db = getDatabase(); const stmt = db.prepare(` INSERT INTO users ( id, email, email_encrypted, name, name_encrypted, password_hash, providers, email_verified, verification_token, verification_expires_at, reset_token, reset_expires_at, plan, billing_status, billing_email, payment_method_last4, subscription_renews_at, referred_by_affiliate_code, affiliate_attribution_at, affiliate_payouts, two_factor_secret, two_factor_enabled, created_at, updated_at, last_login_at, data ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET email = excluded.email, email_encrypted = excluded.email_encrypted, name = excluded.name, name_encrypted = excluded.name_encrypted, password_hash = excluded.password_hash, providers = excluded.providers, email_verified = excluded.email_verified, verification_token = excluded.verification_token, verification_expires_at = excluded.verification_expires_at, reset_token = excluded.reset_token, reset_expires_at = excluded.reset_expires_at, plan = excluded.plan, billing_status = excluded.billing_status, billing_email = excluded.billing_email, payment_method_last4 = excluded.payment_method_last4, subscription_renews_at = excluded.subscription_renews_at, referred_by_affiliate_code = excluded.referred_by_affiliate_code, affiliate_attribution_at = excluded.affiliate_attribution_at, affiliate_payouts = excluded.affiliate_payouts, two_factor_secret = excluded.two_factor_secret, two_factor_enabled = excluded.two_factor_enabled, created_at = excluded.created_at, updated_at = excluded.updated_at, last_login_at = excluded.last_login_at, data = excluded.data `); for (const user of users) { try { const normalizedEmail = (user.email || '').trim().toLowerCase(); if (!normalizedEmail) { console.log(' Skipping user without email'); failed++; continue; } const passwordHash = user.passwordHash || user.password_hash || user.password || ''; const providers = Array.isArray(user.providers) ? user.providers : []; const affiliatePayouts = Array.isArray(user.affiliatePayouts) ? user.affiliatePayouts : []; const dataPayload = JSON.stringify({ ...user, email: normalizedEmail || user.email || '' }); const emailEncrypted = normalizedEmail ? encrypt(normalizedEmail) : ''; const nameEncrypted = user.name ? encrypt(user.name) : null; const twoFactorEncrypted = user.twoFactorSecret ? encrypt(user.twoFactorSecret) : null; stmt.run( user.id, normalizedEmail, emailEncrypted, user.name || null, nameEncrypted, passwordHash, JSON.stringify(providers), user.emailVerified ? 1 : 0, user.verificationToken || null, user.verificationExpiresAt || null, user.resetToken || null, user.resetExpiresAt || null, user.plan || 'hobby', user.billingStatus || 'active', user.billingEmail || user.email || '', user.paymentMethodLast4 || '', user.subscriptionRenewsAt || null, user.referredByAffiliateCode || null, user.affiliateAttributionAt || null, JSON.stringify(affiliatePayouts), twoFactorEncrypted, user.twoFactorEnabled ? 1 : 0, user.createdAt || Date.now(), user.updatedAt || user.createdAt || Date.now(), user.lastLoginAt || null, dataPayload ); console.log(` āœ“ Migrated user: ${user.email}`); success++; } catch (error) { console.error(` āœ— Failed to migrate user ${user.email}:`, error.message); failed++; } } console.log(` Completed: ${success} success, ${failed} failed`); return { success, failed }; } async function migrateSessions() { console.log('\nšŸ“¦ Migrating sessions...'); const sessions = await loadJsonFile(SESSIONS_FILE, {}); const sessionCount = Object.keys(sessions).length; console.log(` Found ${sessionCount} sessions in JSON`); if (sessionCount === 0) { console.log(' No sessions to migrate'); return { success: 0, failed: 0, expired: 0 }; } const now = Date.now(); let success = 0; let failed = 0; let expired = 0; const db = getDatabase(); const userExistsStmt = db.prepare('SELECT id FROM users WHERE id = ?'); const sessionStmt = db.prepare(` INSERT INTO sessions ( id, user_id, token, refresh_token_hash, device_fingerprint, ip_address, user_agent, expires_at, created_at, last_accessed_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET user_id = excluded.user_id, token = excluded.token, expires_at = excluded.expires_at, last_accessed_at = excluded.last_accessed_at `); for (const [token, session] of Object.entries(sessions)) { try { // Skip expired sessions if (session.expiresAt && session.expiresAt <= now) { expired++; continue; } // Check if user exists const user = userExistsStmt.get(session.userId); if (!user) { console.log(` Skipping session for non-existent user: ${session.userId}`); failed++; continue; } sessionStmt.run( token, session.userId, token, null, session.deviceFingerprint || null, session.ipAddress || null, session.userAgent || null, session.expiresAt, session.createdAt || now, session.lastAccessedAt || now ); success++; } catch (error) { console.error(` āœ— Failed to migrate session:`, error.message); failed++; } } console.log(` Completed: ${success} success, ${failed} failed, ${expired} expired`); return { success, failed, expired }; } async function migrateAffiliates() { console.log('\nšŸ“¦ Migrating affiliates...'); const affiliates = await loadJsonFile(AFFILIATES_FILE, []); console.log(` Found ${affiliates.length} affiliates in JSON`); if (affiliates.length === 0) { console.log(' No affiliates to migrate'); return { success: 0, failed: 0 }; } let success = 0; let failed = 0; const db = getDatabase(); const stmt = db.prepare(` INSERT INTO affiliate_accounts ( id, email, name, password_hash, codes, earnings, commission_rate, created_at, updated_at, last_login_at, last_payout_at, email_verified, verification_token, verification_expires_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET email = excluded.email, name = excluded.name, password_hash = excluded.password_hash, codes = excluded.codes, earnings = excluded.earnings, commission_rate = excluded.commission_rate, updated_at = excluded.updated_at, last_login_at = excluded.last_login_at, last_payout_at = excluded.last_payout_at, email_verified = excluded.email_verified, verification_token = excluded.verification_token, verification_expires_at = excluded.verification_expires_at `); for (const affiliate of affiliates) { try { if (!affiliate.email) { console.log(' Skipping affiliate without email'); failed++; continue; } const affiliateEmail = (affiliate.email || '').trim().toLowerCase(); stmt.run( affiliate.id || require('crypto').randomUUID(), affiliateEmail, affiliate.name || '', affiliate.password || affiliate.passwordHash || '', JSON.stringify(affiliate.codes || []), JSON.stringify(affiliate.earnings || []), affiliate.commissionRate || 0.15, affiliate.createdAt || new Date().toISOString(), affiliate.updatedAt || affiliate.createdAt || new Date().toISOString(), affiliate.lastLoginAt || null, affiliate.lastPayoutAt || null, affiliate.emailVerified ? 1 : 0, affiliate.verificationToken || null, affiliate.verificationExpiresAt || null ); console.log(` āœ“ Migrated affiliate: ${affiliate.email}`); success++; } catch (error) { console.error(` āœ— Failed to migrate affiliate:`, error.message); failed++; } } console.log(` Completed: ${success} success, ${failed} failed`); return { success, failed }; } async function createBackup() { console.log('\nšŸ’¾ Creating backup of JSON files...'); const backupDir = path.join(DATA_ROOT, `migration_backup_${Date.now()}`); if (!fs.existsSync(backupDir)) { fs.mkdirSync(backupDir, { recursive: true }); } const files = [USERS_FILE, SESSIONS_FILE, AFFILIATES_FILE]; let backedUp = 0; for (const file of files) { if (fs.existsSync(file)) { const fileName = path.basename(file); const backupPath = path.join(backupDir, fileName); fs.copyFileSync(file, backupPath); console.log(` āœ“ Backed up: ${fileName}`); backedUp++; } } console.log(` Created backup in: ${backupDir}`); console.log(` Backed up ${backedUp} files`); return backupDir; } async function runMigration() { console.log('šŸ”„ Starting database migration...'); console.log(' Source: JSON files in', STATE_DIR); console.log(' Target: Database at', DATABASE_PATH); // Check if database exists if (!fs.existsSync(DATABASE_PATH)) { console.error('āŒ Database not found. Please run setup-database.js first.'); process.exit(1); } // Initialize encryption let encryptionKey = DATABASE_ENCRYPTION_KEY; if (!encryptionKey && KEY_FILE && fs.existsSync(KEY_FILE)) { encryptionKey = fs.readFileSync(KEY_FILE, 'utf8').trim(); if (encryptionKey) { process.env.DATABASE_ENCRYPTION_KEY = encryptionKey; console.log('āœ… Loaded encryption key from file'); } } if (!encryptionKey) { console.error('āŒ DATABASE_ENCRYPTION_KEY not set'); process.exit(1); } try { initEncryption(encryptionKey); console.log('āœ… Encryption initialized'); } catch (error) { console.error('āŒ Failed to initialize encryption:', error.message); process.exit(1); } // Initialize database try { initDatabase(DATABASE_PATH, { verbose: false, sqlcipherKey: DATABASE_USE_SQLCIPHER ? encryptionKey : null, cipherCompatibility: process.env.DATABASE_CIPHER_COMPAT || 4, kdfIter: process.env.DATABASE_KDF_ITER || 64000 }); console.log('āœ… Database connected'); } catch (error) { console.error('āŒ Failed to connect to database:', error.message); process.exit(1); } // Create backup const backupDir = await createBackup(); // Run migrations const results = { users: await migrateUsers(), sessions: await migrateSessions(), affiliates: await migrateAffiliates() }; // Close database closeDatabase(); // Print summary console.log('\nšŸ“Š Migration Summary:'); console.log(' Users:'); console.log(` āœ“ Success: ${results.users.success}`); console.log(` āœ— Failed: ${results.users.failed}`); console.log(' Sessions:'); console.log(` āœ“ Success: ${results.sessions.success}`); console.log(` āœ— Failed: ${results.sessions.failed}`); console.log(` ā° Expired: ${results.sessions.expired}`); console.log(' Affiliates:'); console.log(` āœ“ Success: ${results.affiliates.success}`); console.log(` āœ— Failed: ${results.affiliates.failed}`); const totalSuccess = results.users.success + results.sessions.success + results.affiliates.success; const totalFailed = results.users.failed + results.sessions.failed + results.affiliates.failed; console.log('\n Total:'); console.log(` āœ“ Success: ${totalSuccess}`); console.log(` āœ— Failed: ${totalFailed}`); console.log('\nāœ… Migration complete!'); console.log(` Backup created in: ${backupDir}`); console.log('\nNext steps:'); console.log(' 1. Verify migration: node scripts/verify-migration.js'); console.log(' 2. Test the application with: USE_JSON_DATABASE=1 npm start'); console.log(' 3. Switch to database mode: unset USE_JSON_DATABASE && npm start'); } // Run migration runMigration().catch(error => { console.error('āŒ Migration failed:', error); closeDatabase(); process.exit(1); });