const express = require('express'); const router = express.Router(); const Joi = require('joi'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const rateLimit = require('express-rate-limit'); // Use test models if available (for testing), otherwise use regular models const models = global.__TEST_MODELS__ || require('../models'); const { User, Tenant } = models; const { Op } = require('sequelize'); const { validateRequest } = require('../middleware/validation'); const { authenticateToken, requireRole } = require('../middleware/auth'); const MultiTenantAuth = require('../middleware/multi-tenant-auth'); // Rate limiting for registration endpoint - EXTRA SECURITY const registrationLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 3, // Limit each IP to 3 registration attempts per windowMs message: { success: false, message: 'Too many registration attempts. Please try again later.' }, standardHeaders: true, legacyHeaders: false, }); // Enhanced validation schema with stronger requirements const registerSchema = Joi.object({ username: Joi.string() .min(3) .max(50) .pattern(/^[a-zA-Z0-9._-]+$/) .required() .messages({ 'string.pattern.base': 'Username can only contain letters, numbers, dots, underscores, and hyphens' }), email: Joi.string() .email() .required() .max(255), password: Joi.string() .min(8) .max(100) .pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) .required() .messages({ 'string.pattern.base': 'Password must contain at least one lowercase letter, one uppercase letter, and one number' }), first_name: Joi.string() .max(100) .optional() .allow(''), last_name: Joi.string() .max(100) .optional() .allow(''), phone_number: Joi.string() .pattern(/^[\+]?[1-9][\d]{0,15}$/) .optional() .allow('') .messages({ 'string.pattern.base': 'Please enter a valid phone number' }), role: Joi.string() .valid('viewer') // Only allow viewer role for self-registration .default('viewer') }); const loginSchema = Joi.object({ username: Joi.string().required(), password: Joi.string().required() }); const updateProfileSchema = Joi.object({ first_name: Joi.string().optional(), last_name: Joi.string().optional(), phone_number: Joi.string().optional(), sms_alerts_enabled: Joi.boolean().optional(), email_alerts_enabled: Joi.boolean().optional(), timezone: Joi.string().optional() }); // POST /api/users/register - Register new user (ULTRA SECURE) router.post('/register', registrationLimiter, validateRequest(registerSchema), async (req, res) => { try { console.log('🔒 Registration attempt started'); // Step 1: Determine tenant context - CRITICAL SECURITY CHECK const multiAuth = new MultiTenantAuth(); const tenantId = await multiAuth.determineTenant(req); console.log('🔍 Registration - Determined tenant:', tenantId); if (!tenantId) { console.log('❌ Registration BLOCKED - No tenant determined'); return res.status(400).json({ success: false, message: 'Unable to determine tenant context' }); } // Step 2: Get tenant from database - VERIFY TENANT EXISTS const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { console.log('❌ Registration BLOCKED - Tenant not found:', tenantId); return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Step 3: TRIPLE CHECK - Tenant must be active if (!tenant.is_active) { console.log('❌ Registration BLOCKED - Tenant inactive:', tenantId); return res.status(403).json({ success: false, message: 'Registration not available for this tenant' }); } // Step 4: CRITICAL SECURITY CHECK - Only local auth tenants allow registration if (tenant.auth_provider !== 'local') { console.log('❌ Registration BLOCKED - Non-local auth provider:', tenant.auth_provider); return res.status(403).json({ success: false, message: 'Registration not available. This tenant uses external authentication.' }); } // Step 5: ULTIMATE SECURITY CHECK - Explicit registration permission if (!tenant.allow_registration) { console.log('❌ Registration BLOCKED - Registration disabled for tenant:', tenantId); return res.status(403).json({ success: false, message: 'Registration is not enabled for this tenant. Please contact your administrator.' }); } // Step 6: Additional security - Check if registration is explicitly enabled in auth config const authConfig = await multiAuth.getTenantAuthConfig(tenantId); if (!authConfig.enabled) { console.log('❌ Registration BLOCKED - Auth config disabled'); return res.status(403).json({ success: false, message: 'Authentication is disabled for this tenant' }); } console.log('✅ Registration security checks passed for tenant:', tenantId); // Step 7: Process registration - User data validation const { password, ...userData } = req.body; // Check if user already exists in this tenant const existingUser = await User.findOne({ where: { [Op.or]: [ { username: userData.username }, { email: userData.email } ], tenant_id: tenant.id // Scope to specific tenant } }); if (existingUser) { console.log('❌ Registration BLOCKED - User already exists in tenant'); return res.status(409).json({ success: false, message: 'Username or email already exists' }); } // Step 8: Hash password with high security const saltRounds = 12; const password_hash = await bcrypt.hash(password, saltRounds); // Step 9: Create user with tenant association const user = await User.create({ ...userData, password_hash, tenant_id: tenant.id, // CRITICAL: Associate with specific tenant is_active: true, created_at: new Date(), updated_at: new Date() }); console.log('✅ User registered successfully:', user.username, 'for tenant:', tenantId); // Remove password hash from response const { password_hash: _, ...userResponse } = user.toJSON(); res.status(201).json({ success: true, data: userResponse, message: 'User registered successfully' }); } catch (error) { console.error('❌ Registration error:', error); if (error.name === 'SequelizeUniqueConstraintError') { return res.status(409).json({ success: false, message: 'Username or email already exists' }); } res.status(500).json({ success: false, message: 'Registration failed', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // POST /api/users/login - User login (direct API access - for legacy support) router.post('/login', validateRequest(loginSchema), async (req, res) => { try { const { username, password } = req.body; // Initialize multi-tenant auth const multiAuth = new MultiTenantAuth(); // Determine tenant from request const tenantId = await multiAuth.determineTenant(req); // Find tenant const tenant = await Tenant.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Find user by username or email within this tenant const user = await User.findOne({ where: { [Op.or]: [ { username: username }, { email: username } ], is_active: true, tenant_id: tenant.id } }); if (!user || !await bcrypt.compare(password, user.password_hash)) { return res.status(401).json({ success: false, message: 'Invalid credentials' }); } // Update last login await user.update({ last_login: new Date() }); // Generate JWT token with tenant information const token = multiAuth.generateJWTToken(user, tenantId); // Remove password hash from response const { password_hash: _, ...userResponse } = user.toJSON(); res.json({ success: true, data: { user: userResponse, token, expires_in: '24h', tenant: { id: tenant.slug, name: tenant.name } }, message: 'Login successful' }); } catch (error) { console.error('Error during login:', error); res.status(500).json({ success: false, message: 'Login failed', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // GET /api/users/profile - Get current user profile router.get('/profile', authenticateToken, async (req, res) => { try { // Log the user object for debugging console.log('📍 /users/profile - req.user:', { id: req.user.id, username: req.user.username, role: req.user.role, email: req.user.email, is_active: req.user.is_active, tenant_id: req.user.tenant_id }); const { password_hash: _, ...userProfile } = req.user.toJSON(); console.log('📤 /users/profile - Response:', userProfile); res.json({ success: true, data: userProfile }); } catch (error) { console.error('Error fetching user profile:', error); res.status(500).json({ success: false, message: 'Failed to fetch user profile', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // PUT /api/users/profile - Update user profile router.put('/profile', authenticateToken, validateRequest(updateProfileSchema), async (req, res) => { try { await req.user.update(req.body); const { password_hash: _, ...userResponse } = req.user.toJSON(); res.json({ success: true, data: userResponse, message: 'Profile updated successfully' }); } catch (error) { console.error('Error updating user profile:', error); res.status(500).json({ success: false, message: 'Failed to update profile', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); // GET /api/users - Get all users (admin only) router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => { try { const { limit = 50, offset = 0, role, is_active } = req.query; const whereClause = {}; if (role) whereClause.role = role; if (is_active !== undefined) whereClause.is_active = is_active === 'true'; const users = await User.findAndCountAll({ where: whereClause, attributes: { exclude: ['password_hash'] }, limit: Math.min(parseInt(limit), 100), offset: parseInt(offset), order: [['created_at', 'DESC']] }); res.json({ success: true, data: users.rows, pagination: { total: users.count, limit: parseInt(limit), offset: parseInt(offset), pages: Math.ceil(users.count / parseInt(limit)) } }); } catch (error) { console.error('Error fetching users:', error); res.status(500).json({ success: false, message: 'Failed to fetch users', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } }); /** * Tenant-aware local authentication function * Called by multi-tenant auth middleware */ async function loginLocal(req, res, next) { try { const { username, password } = req.body; // Get tenant information from request (set by multi-tenant auth middleware) let tenantId = null; if (req.tenant && req.tenant.id) { // Find the actual tenant in database const tenant = await Tenant.findOne({ where: { slug: req.tenant.id } }); if (tenant) { tenantId = tenant.id; } } console.log(`🔐 Login attempt for user "${username}" in tenant "${req.tenant?.id}" (${tenantId})`); // Find user by username or email within the specific tenant const whereClause = { [Op.and]: [ { [Op.or]: [ { username: username }, { email: username } ] }, { is_active: true } ] }; // Add tenant filtering - only include users from this tenant if (tenantId) { whereClause[Op.and].push({ tenant_id: tenantId }); } else { // For default tenant, look for users with null tenant_id whereClause[Op.and].push({ tenant_id: null }); } const user = await User.findOne({ where: whereClause }); if (!user) { console.log(`❌ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - User not found`); return res.status(401).json({ success: false, message: 'Invalid credentials' }); } const passwordMatch = await bcrypt.compare(password, user.password_hash); if (!passwordMatch) { console.log(`❌ Authentication failed for "${username}" in tenant "${req.tenant?.id}" - Invalid password`); return res.status(401).json({ success: false, message: 'Invalid credentials' }); } console.log(`✅ Authentication successful for "${username}" in tenant "${req.tenant?.id}"`); // Update last login await user.update({ last_login: new Date() }); // Generate JWT token with tenant information const token = jwt.sign( { userId: user.id, username: user.username, role: user.role, tenantId: user.tenant_id, tenantSlug: req.tenant?.id }, process.env.JWT_SECRET, { expiresIn: '24h' } ); // Remove password hash from response const { password_hash: _, ...userResponse } = user.toJSON(); res.json({ success: true, data: { user: userResponse, token, expires_in: '24h' }, message: 'Login successful' }); } catch (error) { console.error('Error during tenant-aware login:', error); res.status(500).json({ success: false, message: 'Login failed', error: process.env.NODE_ENV === 'development' ? error.message : 'Internal server error' }); } } module.exports = router; module.exports.loginLocal = loginLocal;