- Add verify-migration.js script for testing database migrations - Add database config module for centralized configuration - Add chutes.txt prompt for system responses - Update database implementation and testing documentation - Add database migration and setup scripts - Update session system and LLM tool configuration - Update deployment checklist and environment example - Update Dockerfile and docker-compose configuration
408 lines
14 KiB
JavaScript
Executable File
408 lines
14 KiB
JavaScript
Executable File
#!/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);
|
|
});
|