/** * User Repository - Data access layer for users * Handles encryption/decryption of sensitive fields */ const { getDatabase } = require('../database/connection'); const { encrypt, decrypt } = require('../utils/encryption'); const crypto = require('crypto'); /** * Create a new user * @param {Object} userData - User data * @returns {Object} Created user */ function createUser(userData) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const now = Date.now(); const id = userData.id || crypto.randomUUID(); // Encrypt sensitive fields const emailEncrypted = encrypt(userData.email); const nameEncrypted = userData.name ? encrypt(userData.name) : null; const stmt = db.prepare(` INSERT INTO users ( id, email, email_encrypted, name, name_encrypted, password_hash, providers, email_verified, verification_token, verification_expires_at, plan, billing_status, billing_email, created_at, updated_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); stmt.run( id, userData.email, emailEncrypted, userData.name || null, nameEncrypted, userData.passwordHash, JSON.stringify(userData.providers || []), userData.emailVerified ? 1 : 0, userData.verificationToken || null, userData.verificationExpiresAt || null, userData.plan || 'hobby', userData.billingStatus || 'active', userData.billingEmail || userData.email, now, now ); return getUserById(id); } /** * Get user by ID * @param {string} userId - User ID * @returns {Object|null} User object or null */ function getUserById(userId) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const stmt = db.prepare('SELECT * FROM users WHERE id = ?'); const row = stmt.get(userId); return row ? deserializeUser(row) : null; } /** * Get user by email * @param {string} email - User email * @returns {Object|null} User object or null */ function getUserByEmail(email) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const stmt = db.prepare('SELECT * FROM users WHERE email = ?'); const row = stmt.get(email); return row ? deserializeUser(row) : null; } /** * Get user by verification token * @param {string} token - Verification token * @returns {Object|null} User object or null */ function getUserByVerificationToken(token) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const stmt = db.prepare('SELECT * FROM users WHERE verification_token = ?'); const row = stmt.get(token); return row ? deserializeUser(row) : null; } /** * Get user by reset token * @param {string} token - Reset token * @returns {Object|null} User object or null */ function getUserByResetToken(token) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const stmt = db.prepare('SELECT * FROM users WHERE reset_token = ?'); const row = stmt.get(token); return row ? deserializeUser(row) : null; } /** * Update user * @param {string} userId - User ID * @param {Object} updates - Fields to update * @returns {Object|null} Updated user */ function updateUser(userId, updates) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const user = getUserById(userId); if (!user) { return null; } const sets = []; const values = []; // Handle regular fields const simpleFields = [ 'email', 'name', 'password_hash', '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', 'two_factor_enabled', 'last_login_at' ]; simpleFields.forEach(field => { if (updates.hasOwnProperty(field)) { sets.push(`${field} = ?`); // Handle boolean fields if (field.includes('_verified') || field.includes('_enabled')) { values.push(updates[field] ? 1 : 0); } else { values.push(updates[field]); } // Handle encrypted fields if (field === 'email' && updates.email) { sets.push('email_encrypted = ?'); values.push(encrypt(updates.email)); } else if (field === 'name' && updates.name) { sets.push('name_encrypted = ?'); values.push(encrypt(updates.name)); } } }); // Handle JSON fields if (updates.providers) { sets.push('providers = ?'); values.push(JSON.stringify(updates.providers)); } if (updates.affiliatePayouts) { sets.push('affiliate_payouts = ?'); values.push(JSON.stringify(updates.affiliatePayouts)); } // Handle encrypted 2FA secret if (updates.twoFactorSecret) { sets.push('two_factor_secret = ?'); values.push(encrypt(updates.twoFactorSecret)); } if (sets.length === 0) { return user; } // Add updated_at sets.push('updated_at = ?'); values.push(Date.now()); // Add userId for WHERE clause values.push(userId); const sql = `UPDATE users SET ${sets.join(', ')} WHERE id = ?`; const stmt = db.prepare(sql); stmt.run(...values); return getUserById(userId); } /** * Delete user * @param {string} userId - User ID * @returns {boolean} True if deleted */ function deleteUser(userId) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const stmt = db.prepare('DELETE FROM users WHERE id = ?'); const result = stmt.run(userId); return result.changes > 0; } /** * Get all users (with pagination) * @param {Object} options - Query options (limit, offset) * @returns {Array} Array of users */ function getAllUsers(options = {}) { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const limit = options.limit || 100; const offset = options.offset || 0; const stmt = db.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?'); const rows = stmt.all(limit, offset); return rows.map(deserializeUser); } /** * Count total users * @returns {number} Total user count */ function countUsers() { const db = getDatabase(); if (!db) { throw new Error('Database not initialized'); } const stmt = db.prepare('SELECT COUNT(*) as count FROM users'); const result = stmt.get(); return result.count; } /** * Deserialize user row from database * Converts database row to user object with decrypted fields * @param {Object} row - Database row * @returns {Object} User object */ function deserializeUser(row) { if (!row) { return null; } return { id: row.id, email: row.email, name: row.name, passwordHash: row.password_hash, providers: JSON.parse(row.providers || '[]'), emailVerified: Boolean(row.email_verified), verificationToken: row.verification_token, verificationExpiresAt: row.verification_expires_at, resetToken: row.reset_token, resetExpiresAt: row.reset_expires_at, plan: row.plan, billingStatus: row.billing_status, billingEmail: row.billing_email, paymentMethodLast4: row.payment_method_last4, subscriptionRenewsAt: row.subscription_renews_at, referredByAffiliateCode: row.referred_by_affiliate_code, affiliateAttributionAt: row.affiliate_attribution_at, affiliatePayouts: JSON.parse(row.affiliate_payouts || '[]'), twoFactorSecret: row.two_factor_secret ? decrypt(row.two_factor_secret) : null, twoFactorEnabled: Boolean(row.two_factor_enabled), createdAt: row.created_at, updatedAt: row.updated_at, lastLoginAt: row.last_login_at }; } module.exports = { createUser, getUserById, getUserByEmail, getUserByVerificationToken, getUserByResetToken, updateUser, deleteUser, getAllUsers, countUsers };