import { Router } from 'express'; import bcrypt from 'bcrypt'; import { pool } from '../db/pool'; import { authMiddleware, requireRole, AuthRequest } from '../auth/middleware'; const router = Router(); // All routes require authentication and admin/superadmin role router.use(authMiddleware); router.use(requireRole('admin', 'superadmin')); // Get all users with search and filter router.get('/', async (req: AuthRequest, res) => { try { const { search, domain } = req.query; // Check which columns exist (schema-tolerant) const columnsResult = await pool.query(` SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name IN ('first_name', 'last_name', 'phone', 'domain') `); const existingColumns = new Set(columnsResult.rows.map((r: any) => r.column_name)); // Build column list based on what exists const selectCols = ['id', 'email', 'role', 'created_at', 'updated_at']; if (existingColumns.has('first_name')) selectCols.push('first_name'); if (existingColumns.has('last_name')) selectCols.push('last_name'); if (existingColumns.has('phone')) selectCols.push('phone'); if (existingColumns.has('domain')) selectCols.push('domain'); let query = `SELECT ${selectCols.join(', ')} FROM users WHERE 1=1`; const params: any[] = []; let paramIndex = 1; // Search by email (and optionally first_name, last_name if they exist) if (search && typeof search === 'string') { const searchClauses = ['email ILIKE $' + paramIndex]; if (existingColumns.has('first_name')) searchClauses.push('first_name ILIKE $' + paramIndex); if (existingColumns.has('last_name')) searchClauses.push('last_name ILIKE $' + paramIndex); query += ` AND (${searchClauses.join(' OR ')})`; params.push(`%${search}%`); paramIndex++; } // Filter by domain (if column exists) if (domain && typeof domain === 'string' && existingColumns.has('domain')) { query += ` AND domain = $${paramIndex}`; params.push(domain); paramIndex++; } query += ` ORDER BY created_at DESC`; const result = await pool.query(query, params); res.json({ users: result.rows }); } catch (error) { console.error('Error fetching users:', error); res.status(500).json({ error: 'Failed to fetch users' }); } }); // Get single user router.get('/:id', async (req: AuthRequest, res) => { try { const { id } = req.params; // Check which columns exist (schema-tolerant) const columnsResult = await pool.query(` SELECT column_name FROM information_schema.columns WHERE table_name = 'users' AND column_name IN ('first_name', 'last_name', 'phone', 'domain') `); const existingColumns = new Set(columnsResult.rows.map((r: any) => r.column_name)); const selectCols = ['id', 'email', 'role', 'created_at', 'updated_at']; if (existingColumns.has('first_name')) selectCols.push('first_name'); if (existingColumns.has('last_name')) selectCols.push('last_name'); if (existingColumns.has('phone')) selectCols.push('phone'); if (existingColumns.has('domain')) selectCols.push('domain'); const result = await pool.query(` SELECT ${selectCols.join(', ')} FROM users WHERE id = $1 `, [id]); if (result.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } res.json({ user: result.rows[0] }); } catch (error) { console.error('Error fetching user:', error); res.status(500).json({ error: 'Failed to fetch user' }); } }); // Create user router.post('/', async (req: AuthRequest, res) => { try { const { email, password, role, first_name, last_name, phone, domain } = req.body; if (!email || !password) { return res.status(400).json({ error: 'Email and password are required' }); } // Check for valid role const validRoles = ['admin', 'analyst', 'viewer']; if (role && !validRoles.includes(role)) { return res.status(400).json({ error: 'Invalid role. Must be: admin, analyst, or viewer' }); } // Check for valid domain const validDomains = ['cannaiq.co', 'findagram.co', 'findadispo.com']; if (domain && !validDomains.includes(domain)) { return res.status(400).json({ error: 'Invalid domain. Must be: cannaiq.co, findagram.co, or findadispo.com' }); } // Check if email already exists const existing = await pool.query('SELECT id FROM users WHERE email = $1', [email]); if (existing.rows.length > 0) { return res.status(400).json({ error: 'Email already exists' }); } // Hash password const passwordHash = await bcrypt.hash(password, 10); const result = await pool.query(` INSERT INTO users (email, password_hash, role, first_name, last_name, phone, domain) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, email, role, first_name, last_name, phone, domain, created_at, updated_at `, [email, passwordHash, role || 'viewer', first_name || null, last_name || null, phone || null, domain || 'cannaiq.co']); res.status(201).json({ user: result.rows[0] }); } catch (error) { console.error('Error creating user:', error); res.status(500).json({ error: 'Failed to create user' }); } }); // Update user router.put('/:id', async (req: AuthRequest, res) => { try { const { id } = req.params; const { email, password, role, first_name, last_name, phone, domain } = req.body; // Check if user exists const existing = await pool.query('SELECT id FROM users WHERE id = $1', [id]); if (existing.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } // Check for valid role const validRoles = ['admin', 'analyst', 'viewer', 'superadmin']; if (role && !validRoles.includes(role)) { return res.status(400).json({ error: 'Invalid role' }); } // Check for valid domain const validDomains = ['cannaiq.co', 'findagram.co', 'findadispo.com']; if (domain && !validDomains.includes(domain)) { return res.status(400).json({ error: 'Invalid domain. Must be: cannaiq.co, findagram.co, or findadispo.com' }); } // Prevent non-superadmin from modifying superadmin users const targetUser = await pool.query('SELECT role FROM users WHERE id = $1', [id]); if (targetUser.rows[0].role === 'superadmin' && req.user?.role !== 'superadmin') { return res.status(403).json({ error: 'Cannot modify superadmin users' }); } // Build update query dynamically const updates: string[] = []; const values: any[] = []; let paramIndex = 1; if (email) { // Check if email already taken by another user const emailCheck = await pool.query('SELECT id FROM users WHERE email = $1 AND id != $2', [email, id]); if (emailCheck.rows.length > 0) { return res.status(400).json({ error: 'Email already in use' }); } updates.push(`email = $${paramIndex++}`); values.push(email); } if (password) { const passwordHash = await bcrypt.hash(password, 10); updates.push(`password_hash = $${paramIndex++}`); values.push(passwordHash); } if (role) { updates.push(`role = $${paramIndex++}`); values.push(role); } // Handle profile fields (allow setting to null with explicit undefined check) if (first_name !== undefined) { updates.push(`first_name = $${paramIndex++}`); values.push(first_name || null); } if (last_name !== undefined) { updates.push(`last_name = $${paramIndex++}`); values.push(last_name || null); } if (phone !== undefined) { updates.push(`phone = $${paramIndex++}`); values.push(phone || null); } if (domain !== undefined) { updates.push(`domain = $${paramIndex++}`); values.push(domain); } if (updates.length === 0) { return res.status(400).json({ error: 'No fields to update' }); } updates.push(`updated_at = NOW()`); values.push(id); const result = await pool.query(` UPDATE users SET ${updates.join(', ')} WHERE id = $${paramIndex} RETURNING id, email, role, first_name, last_name, phone, domain, created_at, updated_at `, values); res.json({ user: result.rows[0] }); } catch (error) { console.error('Error updating user:', error); res.status(500).json({ error: 'Failed to update user' }); } }); // Delete user router.delete('/:id', async (req: AuthRequest, res) => { try { const { id } = req.params; // Prevent deleting yourself if (req.user?.id === parseInt(id)) { return res.status(400).json({ error: 'Cannot delete your own account' }); } // Prevent non-superadmin from deleting superadmin users const targetUser = await pool.query('SELECT role FROM users WHERE id = $1', [id]); if (targetUser.rows.length === 0) { return res.status(404).json({ error: 'User not found' }); } if (targetUser.rows[0].role === 'superadmin' && req.user?.role !== 'superadmin') { return res.status(403).json({ error: 'Cannot delete superadmin users' }); } await pool.query('DELETE FROM users WHERE id = $1', [id]); res.json({ success: true }); } catch (error) { console.error('Error deleting user:', error); res.status(500).json({ error: 'Failed to delete user' }); } }); export default router;