314 lines
7.8 KiB
JavaScript
314 lines
7.8 KiB
JavaScript
/**
|
|
* 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
|
|
};
|