import express from 'express'; import { dbRun, dbGet, dbAll } from '../db/database.js'; import { hashPassword, authenticateToken, optionalAuth, requireAdmin, requireActiveUser } from '../middleware/auth.js'; import { generateId, sanitizeUser, getPagination, ApiResponse, asyncHandler } from '../utils/helpers.js'; const router = express.Router(); // ============================================ // Public Routes // ============================================ /** * @route GET /api/users/me * @desc Get current user profile (from Auth0 JWT) * @access Private (requires Auth0 token) */ router.get('/me', authenticateToken, asyncHandler(async (req, res) => { const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.user.id]); if (!user) { return res.status(404).json(ApiResponse(false, null, 'User not found')); } res.json(ApiResponse(true, { user: sanitizeUser(user) })); })); /** * @route PUT /api/users/me * @desc Update current user profile * @access Private (requires Auth0 token) */ router.put('/me', authenticateToken, asyncHandler(async (req, res) => { const { name, picture } = req.body; const updates = []; const values = []; if (name !== undefined) { updates.push('name = ?'); values.push(name); } if (picture !== undefined) { updates.push('picture = ?'); values.push(picture); } if (updates.length === 0) { return res.status(400).json(ApiResponse(false, null, 'No fields to update')); } updates.push('updated_at = CURRENT_TIMESTAMP'); values.push(req.user.id); await dbRun(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, values); const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.user.id]); res.json(ApiResponse(true, { user: sanitizeUser(user) }, 'Profile updated successfully')); })); /** * @route DELETE /api/users/me * @desc Delete user account (soft delete by deactivating) * @access Private (requires Auth0 token) */ router.delete('/me', authenticateToken, asyncHandler(async (req, res) => { // Soft delete - just deactivate the account await dbRun('UPDATE users SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [req.user.id]); // Delete all API keys await dbRun('DELETE FROM api_keys WHERE user_id = ?', [req.user.id]); // Delete all sessions await dbRun('DELETE FROM sessions WHERE user_id = ?', [req.user.id]); res.json(ApiResponse(true, null, 'Account deactivated successfully')); })); // ============================================ // Credits Routes // ============================================ /** * @route GET /api/users/credits * @desc Get user credits and transaction history * @access Private (requires Auth0 token) */ router.get('/credits', authenticateToken, asyncHandler(async (req, res) => { const user = await dbGet('SELECT credits FROM users WHERE id = ?', [req.user.id]); const { page, limit, offset } = getPagination(req.query.page, req.query.limit); const transactions = await dbAll( `SELECT * FROM credit_transactions WHERE user_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?`, [req.user.id, limit, offset] ); const totalResult = await dbGet( 'SELECT COUNT(*) as count FROM credit_transactions WHERE user_id = ?', [req.user.id] ); res.json(ApiResponse(true, { credits: user.credits, transactions, pagination: { page, limit, total: totalResult.count, totalPages: Math.ceil(totalResult.count / limit) } })); })); // ============================================ // API Keys Routes // ============================================ /** * @route GET /api/users/api-keys * @desc Get user API keys * @access Private (requires Auth0 token) */ router.get('/api-keys', authenticateToken, asyncHandler(async (req, res) => { const keys = await dbAll( `SELECT id, name, last_used, created_at, is_active FROM api_keys WHERE user_id = ? ORDER BY created_at DESC`, [req.user.id] ); res.json(ApiResponse(true, { keys })); })); /** * @route POST /api/users/api-keys * @desc Create a new API key * @access Private (requires Auth0 token) */ router.post('/api-keys', authenticateToken, asyncHandler(async (req, res) => { const { name } = req.body; // Check if user already has max API keys (optional limit) const existingKeys = await dbGet( 'SELECT COUNT(*) as count FROM api_keys WHERE user_id = ? AND is_active = 1', [req.user.id] ); if (existingKeys.count >= 10) { return res.status(400).json(ApiResponse(false, null, 'Maximum of 10 API keys allowed')); } const keyId = generateId(); const keyValue = `moxie_${generateId()}_${generateId()}`; const keyHash = await hashPassword(keyValue); await dbRun( `INSERT INTO api_keys (id, user_id, key_hash, name) VALUES (?, ?, ?, ?)`, [keyId, req.user.id, keyHash, name || 'API Key'] ); // Return the key value only once (can't be retrieved later) res.status(201).json(ApiResponse(true, { key: { id: keyId, name: name || 'API Key', key: keyValue } }, 'API key created. Save this key - it cannot be retrieved again.')); })); /** * @route DELETE /api/users/api-keys/:keyId * @desc Revoke an API key * @access Private (requires Auth0 token) */ router.delete('/api-keys/:keyId', authenticateToken, asyncHandler(async (req, res) => { const result = await dbRun( 'DELETE FROM api_keys WHERE id = ? AND user_id = ?', [req.params.keyId, req.user.id] ); if (result.changes === 0) { return res.status(404).json(ApiResponse(false, null, 'API key not found')); } res.json(ApiResponse(true, null, 'API key revoked')); })); // ============================================ // Admin Routes // ============================================ /** * @route GET /api/users * @desc List all users (admin) * @access Admin */ router.get('/', authenticateToken, requireAdmin, asyncHandler(async (req, res) => { const { page, limit, offset } = getPagination(req.query.page, req.query.limit); const search = req.query.search; let whereClause = ''; let params = []; if (search) { whereClause = 'WHERE email LIKE ? OR name LIKE ?'; const searchPattern = `%${search}%`; params = [searchPattern, searchPattern]; } const users = await dbAll( `SELECT id, auth0_id, email, name, picture, role, credits, subscription_status, subscription_tier, is_active, created_at, last_login FROM users ${whereClause} ORDER BY created_at DESC LIMIT ? OFFSET ?`, [...params, limit, offset] ); const totalResult = await dbGet(`SELECT COUNT(*) as count FROM users ${whereClause}`, params); res.json(ApiResponse(true, { users, pagination: { page, limit, total: totalResult.count, totalPages: Math.ceil(totalResult.count / limit) } })); })); /** * @route GET /api/users/:userId * @desc Get user by ID (admin) * @access Admin */ router.get('/:userId', authenticateToken, requireAdmin, asyncHandler(async (req, res) => { const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.params.userId]); if (!user) { return res.status(404).json(ApiResponse(false, null, 'User not found')); } res.json(ApiResponse(true, { user: sanitizeUser(user) })); })); /** * @route PUT /api/users/:userId * @desc Update user by ID (admin) * @access Admin */ router.put('/:userId', authenticateToken, requireAdmin, asyncHandler(async (req, res) => { const { name, role, credits, subscription_status, subscription_tier, is_active, email } = req.body; const updates = []; const values = []; if (name !== undefined) { updates.push('name = ?'); values.push(name); } if (role !== undefined) { updates.push('role = ?'); values.push(role); } if (credits !== undefined) { updates.push('credits = ?'); values.push(credits); } if (subscription_status !== undefined) { updates.push('subscription_status = ?'); values.push(subscription_status); } if (subscription_tier !== undefined) { updates.push('subscription_tier = ?'); values.push(subscription_tier); } if (is_active !== undefined) { updates.push('is_active = ?'); values.push(is_active ? 1 : 0); } if (email !== undefined) { updates.push('email = ?'); values.push(email); } if (updates.length === 0) { return res.status(400).json(ApiResponse(false, null, 'No fields to update')); } updates.push('updated_at = CURRENT_TIMESTAMP'); values.push(req.params.userId); await dbRun(`UPDATE users SET ${updates.join(', ')} WHERE id = ?`, values); const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.params.userId]); res.json(ApiResponse(true, { user: sanitizeUser(user) }, 'User updated successfully')); })); /** * @route DELETE /api/users/:userId * @desc Delete user by ID (admin) - hard delete * @access Admin */ router.delete('/:userId', authenticateToken, requireAdmin, asyncHandler(async (req, res) => { const result = await dbRun('DELETE FROM users WHERE id = ?', [req.params.userId]); if (result.changes === 0) { return res.status(404).json(ApiResponse(false, null, 'User not found')); } res.json(ApiResponse(true, null, 'User deleted successfully')); })); /** * @route POST /api/users/:userId/credits * @desc Add or remove credits from user (admin) * @access Admin */ router.post('/:userId/credits', authenticateToken, requireAdmin, asyncHandler(async (req, res) => { const { amount, description } = req.body; if (!amount || typeof amount !== 'number') { return res.status(400).json(ApiResponse(false, null, 'Valid amount is required')); } const user = await dbGet('SELECT credits FROM users WHERE id = ?', [req.params.userId]); if (!user) { return res.status(404).json(ApiResponse(false, null, 'User not found')); } const newBalance = user.credits + amount; if (newBalance < 0) { return res.status(400).json(ApiResponse(false, null, 'Insufficient credits')); } // Update user credits await dbRun('UPDATE users SET credits = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [newBalance, req.params.userId]); // Record transaction const transactionId = generateId(); await dbRun( `INSERT INTO credit_transactions (id, user_id, amount, type, description) VALUES (?, ?, ?, ?, ?)`, [transactionId, req.params.userId, amount, amount > 0 ? 'credit' : 'debit', description || 'Admin adjustment'] ); res.json(ApiResponse(true, { previousBalance: user.credits, amount, newBalance, transactionId }, 'Credits updated successfully')); })); /** * @route PUT /api/users/:userId/role * @desc Change user role (admin) * @access Admin */ router.put('/:userId/role', authenticateToken, requireAdmin, asyncHandler(async (req, res) => { const { role } = req.body; if (!role || !['user', 'admin'].includes(role)) { return res.status(400).json(ApiResponse(false, null, 'Valid role is required (user or admin)')); } const result = await dbRun( 'UPDATE users SET role = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', [role, req.params.userId] ); if (result.changes === 0) { return res.status(404).json(ApiResponse(false, null, 'User not found')); } const user = await dbGet('SELECT * FROM users WHERE id = ?', [req.params.userId]); res.json(ApiResponse(true, { user: sanitizeUser(user) }, 'User role updated successfully')); })); export default router;