const express = require("express"); const bcrypt = require("bcryptjs"); const jwt = require("jsonwebtoken"); const { getPrismaClient } = require("../config/prisma"); const { body, validationResult } = require("express-validator"); const { authenticateToken, _requireAdmin } = require("../middleware/auth"); const { requireViewUsers, requireManageUsers, } = require("../middleware/permissions"); const { v4: uuidv4 } = require("uuid"); const { createDefaultDashboardPreferences, } = require("./dashboardPreferencesRoutes"); const { create_session, refresh_access_token, revoke_session, revoke_all_user_sessions, generate_device_fingerprint, } = require("../utils/session_manager"); const router = express.Router(); const prisma = getPrismaClient(); /** * Parse user agent string to extract browser and OS info */ function parse_user_agent(user_agent) { if (!user_agent) return { browser: "Unknown", os: "Unknown", device: "Unknown" }; const ua = user_agent.toLowerCase(); // Browser detection let browser = "Unknown"; if (ua.includes("chrome") && !ua.includes("edg")) browser = "Chrome"; else if (ua.includes("firefox")) browser = "Firefox"; else if (ua.includes("safari") && !ua.includes("chrome")) browser = "Safari"; else if (ua.includes("edg")) browser = "Edge"; else if (ua.includes("opera")) browser = "Opera"; // OS detection let os = "Unknown"; if (ua.includes("windows")) os = "Windows"; else if (ua.includes("macintosh") || ua.includes("mac os")) os = "macOS"; else if (ua.includes("linux")) os = "Linux"; else if (ua.includes("android")) os = "Android"; else if (ua.includes("iphone") || ua.includes("ipad")) os = "iOS"; // Device type let device = "Desktop"; if (ua.includes("mobile")) device = "Mobile"; else if (ua.includes("tablet") || ua.includes("ipad")) device = "Tablet"; return { browser, os, device }; } /** * Get basic location info from IP (simplified - in production you'd use a service) */ function get_location_from_ip(ip) { if (!ip) return { country: "Unknown", city: "Unknown" }; // For localhost/private IPs if ( ip === "127.0.0.1" || ip === "::1" || ip.startsWith("192.168.") || ip.startsWith("10.") ) { return { country: "Local", city: "Local Network" }; } // In a real implementation, you'd use a service like MaxMind GeoIP2 // For now, return unknown for external IPs return { country: "Unknown", city: "Unknown" }; } // Check if any admin users exist (for first-time setup) router.get("/check-admin-users", async (_req, res) => { try { const adminCount = await prisma.users.count({ where: { role: "admin" }, }); res.json({ hasAdminUsers: adminCount > 0, adminCount: adminCount, }); } catch (error) { console.error("Error checking admin users:", error); res.status(500).json({ error: "Failed to check admin users", hasAdminUsers: true, // Assume admin exists for security }); } }); // Create first admin user (for first-time setup) router.post( "/setup-admin", [ body("firstName") .isLength({ min: 1 }) .withMessage("First name is required"), body("lastName").isLength({ min: 1 }).withMessage("Last name is required"), body("username").isLength({ min: 1 }).withMessage("Username is required"), body("email").isEmail().withMessage("Valid email is required"), body("password") .isLength({ min: 8 }) .withMessage("Password must be at least 8 characters for security"), ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ error: "Validation failed", details: errors.array(), }); } const { firstName, lastName, username, email, password } = req.body; // Check if any admin users already exist const adminCount = await prisma.users.count({ where: { role: "admin" }, }); if (adminCount > 0) { return res.status(400).json({ error: "Admin users already exist. This endpoint is only for first-time setup.", }); } // Check if username or email already exists const existingUser = await prisma.users.findFirst({ where: { OR: [{ username: username.trim() }, { email: email.trim() }], }, }); if (existingUser) { return res.status(400).json({ error: "Username or email already exists", }); } // Hash password const passwordHash = await bcrypt.hash(password, 12); // Create admin user const user = await prisma.users.create({ data: { id: uuidv4(), username: username.trim(), email: email.trim(), password_hash: passwordHash, first_name: firstName.trim(), last_name: lastName.trim(), role: "admin", is_active: true, created_at: new Date(), updated_at: new Date(), }, select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, created_at: true, }, }); // Create default dashboard preferences for the new admin user await createDefaultDashboardPreferences(user.id, "admin"); // Create session for immediate login const ip_address = req.ip || req.connection.remoteAddress; const user_agent = req.get("user-agent"); const session = await create_session(user.id, ip_address, user_agent); res.status(201).json({ message: "Admin user created successfully", token: session.access_token, refresh_token: session.refresh_token, expires_at: session.expires_at, user: { id: user.id, username: user.username, email: user.email, role: user.role, first_name: user.first_name, last_name: user.last_name, is_active: user.is_active, }, }); } catch (error) { console.error("Error creating admin user:", error); res.status(500).json({ error: "Failed to create admin user", }); } }, ); // Generate JWT token const generateToken = (userId) => { if (!process.env.JWT_SECRET) { throw new Error("JWT_SECRET environment variable is required"); } return jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: process.env.JWT_EXPIRES_IN || "24h", }); }; // Admin endpoint to list all users router.get( "/admin/users", authenticateToken, requireViewUsers, async (_req, res) => { try { const users = await prisma.users.findMany({ select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, is_active: true, last_login: true, created_at: true, updated_at: true, }, orderBy: { created_at: "desc", }, }); res.json(users); } catch (error) { console.error("List users error:", error); res.status(500).json({ error: "Failed to fetch users" }); } }, ); // Admin endpoint to create a new user router.post( "/admin/users", authenticateToken, requireManageUsers, [ body("username") .isLength({ min: 3 }) .withMessage("Username must be at least 3 characters"), body("email").isEmail().withMessage("Valid email is required"), body("password") .isLength({ min: 6 }) .withMessage("Password must be at least 6 characters"), body("first_name") .optional() .isLength({ min: 1 }) .withMessage("First name must be at least 1 character"), body("last_name") .optional() .isLength({ min: 1 }) .withMessage("Last name must be at least 1 character"), body("role") .optional() .custom(async (value) => { if (!value) return true; // Optional field // Allow built-in roles even if not in role_permissions table yet const builtInRoles = ["admin", "user"]; if (builtInRoles.includes(value)) return true; const rolePermissions = await prisma.role_permissions.findUnique({ where: { role: value }, }); if (!rolePermissions) { throw new Error("Invalid role specified"); } return true; }), ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, email, password, first_name, last_name, role } = req.body; // Get default user role from settings if no role specified let userRole = role; if (!userRole) { const settings = await prisma.settings.findFirst(); userRole = settings?.default_user_role || "user"; } // Check if user already exists const existingUser = await prisma.users.findFirst({ where: { OR: [{ username }, { email }], }, }); if (existingUser) { return res .status(409) .json({ error: "Username or email already exists" }); } // Hash password const passwordHash = await bcrypt.hash(password, 12); // Create user const user = await prisma.users.create({ data: { id: uuidv4(), username, email, password_hash: passwordHash, first_name: first_name || null, last_name: last_name || null, role: userRole, updated_at: new Date(), }, select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, is_active: true, created_at: true, }, }); // Create default dashboard preferences for the new user await createDefaultDashboardPreferences(user.id, userRole); res.status(201).json({ message: "User created successfully", user, }); } catch (error) { console.error("User creation error:", error); res.status(500).json({ error: "Failed to create user" }); } }, ); // Admin endpoint to update a user router.put( "/admin/users/:userId", authenticateToken, requireManageUsers, [ body("username") .optional() .isLength({ min: 3 }) .withMessage("Username must be at least 3 characters"), body("email").optional().isEmail().withMessage("Valid email is required"), body("first_name") .optional() .isLength({ min: 1 }) .withMessage("First name must be at least 1 character"), body("last_name") .optional() .isLength({ min: 1 }) .withMessage("Last name must be at least 1 character"), body("role") .optional() .custom(async (value) => { if (!value) return true; // Optional field const rolePermissions = await prisma.role_permissions.findUnique({ where: { role: value }, }); if (!rolePermissions) { throw new Error("Invalid role specified"); } return true; }), body("is_active") .optional() .isBoolean() .withMessage("is_active must be a boolean"), ], async (req, res) => { try { const { userId } = req.params; const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, email, first_name, last_name, role, is_active } = req.body; const updateData = {}; if (username) updateData.username = username; if (email) updateData.email = email; if (first_name !== undefined) updateData.first_name = first_name || null; if (last_name !== undefined) updateData.last_name = last_name || null; if (role) updateData.role = role; if (typeof is_active === "boolean") updateData.is_active = is_active; // Check if user exists const existingUser = await prisma.users.findUnique({ where: { id: userId }, }); if (!existingUser) { return res.status(404).json({ error: "User not found" }); } // Check if username/email already exists (excluding current user) if (username || email) { const duplicateUser = await prisma.users.findFirst({ where: { AND: [ { id: { not: userId } }, { OR: [ ...(username ? [{ username }] : []), ...(email ? [{ email }] : []), ], }, ], }, }); if (duplicateUser) { return res .status(409) .json({ error: "Username or email already exists" }); } } // Prevent deactivating the last admin if (is_active === false && existingUser.role === "admin") { const adminCount = await prisma.users.count({ where: { role: "admin", is_active: true, }, }); if (adminCount <= 1) { return res .status(400) .json({ error: "Cannot deactivate the last admin user" }); } } // Update user const updatedUser = await prisma.users.update({ where: { id: userId }, data: updateData, select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, is_active: true, last_login: true, created_at: true, updated_at: true, }, }); res.json({ message: "User updated successfully", user: updatedUser, }); } catch (error) { console.error("User update error:", error); res.status(500).json({ error: "Failed to update user" }); } }, ); // Admin endpoint to delete a user router.delete( "/admin/users/:userId", authenticateToken, requireManageUsers, async (req, res) => { try { const { userId } = req.params; // Prevent self-deletion if (userId === req.user.id) { return res .status(400) .json({ error: "Cannot delete your own account" }); } // Check if user exists const user = await prisma.users.findUnique({ where: { id: userId }, }); if (!user) { return res.status(404).json({ error: "User not found" }); } // Prevent deleting the last admin if (user.role === "admin") { const adminCount = await prisma.users.count({ where: { role: "admin", is_active: true, }, }); if (adminCount <= 1) { return res .status(400) .json({ error: "Cannot delete the last admin user" }); } } // Delete user await prisma.users.delete({ where: { id: userId }, }); res.json({ message: "User deleted successfully", }); } catch (error) { console.error("User deletion error:", error); res.status(500).json({ error: "Failed to delete user" }); } }, ); // Admin endpoint to reset user password router.post( "/admin/users/:userId/reset-password", authenticateToken, requireManageUsers, [ body("newPassword") .isLength({ min: 6 }) .withMessage("New password must be at least 6 characters"), ], async (req, res) => { try { const { userId } = req.params; const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { newPassword } = req.body; // Check if user exists const user = await prisma.users.findUnique({ where: { id: userId }, select: { id: true, username: true, email: true, role: true, is_active: true, }, }); if (!user) { return res.status(404).json({ error: "User not found" }); } // Prevent resetting password of inactive users if (!user.is_active) { return res .status(400) .json({ error: "Cannot reset password for inactive user" }); } // Hash new password const passwordHash = await bcrypt.hash(newPassword, 12); // Update user password await prisma.users.update({ where: { id: userId }, data: { password_hash: passwordHash }, }); // Log the password reset action (you might want to add an audit log table) console.log( `Password reset for user ${user.username} (${user.email}) by admin ${req.user.username}`, ); res.json({ message: "Password reset successfully", user: { id: user.id, username: user.username, email: user.email, }, }); } catch (error) { console.error("Password reset error:", error); res.status(500).json({ error: "Failed to reset password" }); } }, ); // Check if signup is enabled (public endpoint) router.get("/signup-enabled", async (_req, res) => { try { const settings = await prisma.settings.findFirst(); res.json({ signupEnabled: settings?.signup_enabled || false }); } catch (error) { console.error("Error checking signup status:", error); res.status(500).json({ error: "Failed to check signup status" }); } }); // Public signup endpoint router.post( "/signup", [ body("firstName") .isLength({ min: 1 }) .withMessage("First name is required"), body("lastName").isLength({ min: 1 }).withMessage("Last name is required"), body("username") .isLength({ min: 3 }) .withMessage("Username must be at least 3 characters"), body("email").isEmail().withMessage("Valid email is required"), body("password") .isLength({ min: 6 }) .withMessage("Password must be at least 6 characters"), ], async (req, res) => { try { // Check if signup is enabled const settings = await prisma.settings.findFirst(); if (!settings?.signup_enabled) { return res .status(403) .json({ error: "User signup is currently disabled" }); } const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { firstName, lastName, username, email, password } = req.body; // Check if user already exists const existingUser = await prisma.users.findFirst({ where: { OR: [{ username }, { email }], }, }); if (existingUser) { return res .status(409) .json({ error: "Username or email already exists" }); } // Hash password const passwordHash = await bcrypt.hash(password, 12); // Get default user role from settings or environment variable const defaultRole = settings?.default_user_role || process.env.DEFAULT_USER_ROLE || "user"; // Create user with default role from settings const user = await prisma.users.create({ data: { id: uuidv4(), username, email, password_hash: passwordHash, first_name: firstName.trim(), last_name: lastName.trim(), role: defaultRole, updated_at: new Date(), }, select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, is_active: true, created_at: true, }, }); // Create default dashboard preferences for the new user await createDefaultDashboardPreferences(user.id, defaultRole); console.log(`New user registered: ${user.username} (${user.email})`); // Generate token for immediate login const token = generateToken(user.id); res.status(201).json({ message: "Account created successfully", token, user: { id: user.id, username: user.username, email: user.email, role: user.role, }, }); } catch (error) { console.error("Signup error:", error); console.error("Signup error message:", error.message); console.error("Signup error stack:", error.stack); res.status(500).json({ error: "Failed to create account" }); } }, ); // Login router.post( "/login", [ body("username").notEmpty().withMessage("Username is required"), body("password").notEmpty().withMessage("Password is required"), ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, password } = req.body; // Find user by username or email const user = await prisma.users.findFirst({ where: { OR: [{ username }, { email: username }], is_active: true, }, select: { id: true, username: true, email: true, first_name: true, last_name: true, password_hash: true, role: true, is_active: true, last_login: true, created_at: true, updated_at: true, tfa_enabled: true, }, }); if (!user) { return res.status(401).json({ error: "Invalid credentials" }); } // Verify password const isValidPassword = await bcrypt.compare( password, user.password_hash, ); if (!isValidPassword) { return res.status(401).json({ error: "Invalid credentials" }); } // Check if TFA is enabled if (user.tfa_enabled) { // Get device fingerprint from X-Device-ID header const device_fingerprint = generate_device_fingerprint(req); // Check if this device has a valid TFA bypass if (device_fingerprint) { const remembered_session = await prisma.user_sessions.findFirst({ where: { user_id: user.id, device_fingerprint: device_fingerprint, tfa_remember_me: true, tfa_bypass_until: { gt: new Date() }, // Bypass still valid }, }); if (remembered_session) { // Device is remembered and bypass is still valid - skip TFA // Continue with login below } else { // No valid bypass for this device - require TFA return res.status(200).json({ message: "TFA verification required", requiresTfa: true, username: user.username, }); } } else { // No device ID provided - require TFA return res.status(200).json({ message: "TFA verification required", requiresTfa: true, username: user.username, }); } } // Update last login await prisma.users.update({ where: { id: user.id }, data: { last_login: new Date(), updated_at: new Date(), }, }); // Create session with access and refresh tokens const ip_address = req.ip || req.connection.remoteAddress; const user_agent = req.get("user-agent"); const session = await create_session( user.id, ip_address, user_agent, false, req, ); res.json({ message: "Login successful", token: session.access_token, refresh_token: session.refresh_token, expires_at: session.expires_at, user: { id: user.id, username: user.username, email: user.email, first_name: user.first_name, last_name: user.last_name, role: user.role, is_active: user.is_active, last_login: user.last_login, created_at: user.created_at, updated_at: user.updated_at, // Include user preferences so they're available immediately after login theme_preference: user.theme_preference, color_theme: user.color_theme, }, }); } catch (error) { console.error("Login error:", error); res.status(500).json({ error: "Login failed" }); } }, ); // TFA verification for login router.post( "/verify-tfa", [ body("username").notEmpty().withMessage("Username is required"), body("token") .isLength({ min: 6, max: 6 }) .withMessage("Token must be 6 characters"), body("token") .matches(/^[A-Z0-9]{6}$/) .withMessage("Token must be 6 alphanumeric characters"), body("remember_me") .optional() .isBoolean() .withMessage("Remember me must be a boolean"), ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, token, remember_me = false } = req.body; // Find user const user = await prisma.users.findFirst({ where: { OR: [{ username }, { email: username }], is_active: true, tfa_enabled: true, }, select: { id: true, username: true, email: true, role: true, tfa_secret: true, tfa_backup_codes: true, }, }); if (!user) { return res .status(401) .json({ error: "Invalid credentials or TFA not enabled" }); } // Verify TFA token using the TFA routes logic const speakeasy = require("speakeasy"); // Check if it's a backup code const backupCodes = user.tfa_backup_codes ? JSON.parse(user.tfa_backup_codes) : []; const isBackupCode = backupCodes.includes(token); let verified = false; if (isBackupCode) { // Remove the used backup code const updatedBackupCodes = backupCodes.filter((code) => code !== token); await prisma.users.update({ where: { id: user.id }, data: { tfa_backup_codes: JSON.stringify(updatedBackupCodes), }, }); verified = true; } else { // Verify TOTP token verified = speakeasy.totp.verify({ secret: user.tfa_secret, encoding: "base32", token: token, window: 2, }); } if (!verified) { return res.status(401).json({ error: "Invalid verification code" }); } // Update last login and fetch complete user data const updatedUser = await prisma.users.update({ where: { id: user.id }, data: { last_login: new Date() }, select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, is_active: true, last_login: true, created_at: true, updated_at: true, theme_preference: true, color_theme: true, }, }); // Create session with access and refresh tokens const ip_address = req.ip || req.connection.remoteAddress; const user_agent = req.get("user-agent"); const session = await create_session( user.id, ip_address, user_agent, remember_me, req, ); res.json({ message: "Login successful", token: session.access_token, refresh_token: session.refresh_token, expires_at: session.expires_at, tfa_bypass_until: session.tfa_bypass_until, user: updatedUser, }); } catch (error) { console.error("TFA verification error:", error); res.status(500).json({ error: "TFA verification failed" }); } }, ); // Get current user profile router.get("/profile", authenticateToken, async (req, res) => { try { res.json({ user: req.user, }); } catch (error) { console.error("Get profile error:", error); res.status(500).json({ error: "Failed to get profile" }); } }); // Update user profile router.put( "/profile", authenticateToken, [ body("username") .optional() .isLength({ min: 3 }) .withMessage("Username must be at least 3 characters"), body("email").optional().isEmail().withMessage("Valid email is required"), body("first_name") .optional({ nullable: true, checkFalsy: true }) .custom((value) => { // Allow null, undefined, or empty string to clear the field if (value === null || value === undefined || value === "") { return true; } // If provided, must be at least 1 character after trimming return typeof value === "string" && value.trim().length >= 1; }) .withMessage("First name must be at least 1 character if provided"), body("last_name") .optional({ nullable: true, checkFalsy: true }) .custom((value) => { // Allow null, undefined, or empty string to clear the field if (value === null || value === undefined || value === "") { return true; } // If provided, must be at least 1 character after trimming return typeof value === "string" && value.trim().length >= 1; }) .withMessage("Last name must be at least 1 character if provided"), ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { username, email, first_name, last_name } = req.body; const updateData = { updated_at: new Date(), }; // Handle all fields consistently - trim and update if provided if (username) updateData.username = username.trim(); if (email) updateData.email = email.trim(); if (first_name !== undefined) { // Allow null or empty string to clear the field, otherwise trim updateData.first_name = first_name === "" || first_name === null ? null : first_name.trim() || null; } if (last_name !== undefined) { // Allow null or empty string to clear the field, otherwise trim updateData.last_name = last_name === "" || last_name === null ? null : last_name.trim() || null; } // Check if username/email already exists (excluding current user) if (username || email) { const existingUser = await prisma.users.findFirst({ where: { AND: [ { id: { not: req.user.id } }, { OR: [ ...(username ? [{ username }] : []), ...(email ? [{ email }] : []), ], }, ], }, }); if (existingUser) { return res .status(409) .json({ error: "Username or email already exists" }); } } // Update user with explicit commit const updatedUser = await prisma.users.update({ where: { id: req.user.id }, data: updateData, select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, is_active: true, last_login: true, updated_at: true, }, }); // Explicitly refresh user data from database to ensure we return latest data // This ensures consistency especially in high-concurrency scenarios const freshUser = await prisma.users.findUnique({ where: { id: req.user.id }, select: { id: true, username: true, email: true, first_name: true, last_name: true, role: true, is_active: true, last_login: true, updated_at: true, }, }); // Use fresh data if available, otherwise fallback to updatedUser const responseUser = freshUser || updatedUser; res.json({ message: "Profile updated successfully", user: responseUser, }); } catch (error) { console.error("Update profile error:", error); res.status(500).json({ error: "Failed to update profile" }); } }, ); // Change password router.put( "/change-password", authenticateToken, [ body("currentPassword") .notEmpty() .withMessage("Current password is required"), body("newPassword") .isLength({ min: 6 }) .withMessage("New password must be at least 6 characters"), ], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { currentPassword, newPassword } = req.body; // Get user with password hash const user = await prisma.users.findUnique({ where: { id: req.user.id }, }); // Verify current password const isValidPassword = await bcrypt.compare( currentPassword, user.password_hash, ); if (!isValidPassword) { return res.status(401).json({ error: "Current password is incorrect" }); } // Hash new password const newPasswordHash = await bcrypt.hash(newPassword, 12); // Update password await prisma.users.update({ where: { id: req.user.id }, data: { password_hash: newPasswordHash }, }); res.json({ message: "Password changed successfully", }); } catch (error) { console.error("Change password error:", error); res.status(500).json({ error: "Failed to change password" }); } }, ); // Logout (revoke current session) router.post("/logout", authenticateToken, async (req, res) => { try { // Revoke the current session if (req.session_id) { await revoke_session(req.session_id); } res.json({ message: "Logout successful", }); } catch (error) { console.error("Logout error:", error); res.status(500).json({ error: "Logout failed" }); } }); // Logout all sessions (revoke all user sessions) router.post("/logout-all", authenticateToken, async (req, res) => { try { await revoke_all_user_sessions(req.user.id); res.json({ message: "All sessions logged out successfully", }); } catch (error) { console.error("Logout all error:", error); res.status(500).json({ error: "Logout all failed" }); } }); // Refresh access token using refresh token router.post( "/refresh-token", [body("refresh_token").notEmpty().withMessage("Refresh token is required")], async (req, res) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { refresh_token } = req.body; const result = await refresh_access_token(refresh_token); if (!result.success) { return res.status(401).json({ error: result.error }); } res.json({ message: "Token refreshed successfully", token: result.access_token, user: { id: result.user.id, username: result.user.username, email: result.user.email, role: result.user.role, is_active: result.user.is_active, }, }); } catch (error) { console.error("Refresh token error:", error); res.status(500).json({ error: "Token refresh failed" }); } }, ); // Get user's active sessions router.get("/sessions", authenticateToken, async (req, res) => { try { const sessions = await prisma.user_sessions.findMany({ where: { user_id: req.user.id, is_revoked: false, expires_at: { gt: new Date() }, }, select: { id: true, ip_address: true, user_agent: true, device_fingerprint: true, last_activity: true, created_at: true, expires_at: true, tfa_remember_me: true, tfa_bypass_until: true, login_count: true, last_login_ip: true, }, orderBy: { last_activity: "desc" }, }); // Enhance sessions with device info const enhanced_sessions = sessions.map((session) => { const is_current_session = session.id === req.session_id; const device_info = parse_user_agent(session.user_agent); return { ...session, is_current_session, device_info, location_info: get_location_from_ip(session.ip_address), }; }); res.json({ sessions: enhanced_sessions, }); } catch (error) { console.error("Get sessions error:", error); res.status(500).json({ error: "Failed to fetch sessions" }); } }); // Revoke a specific session router.delete("/sessions/:session_id", authenticateToken, async (req, res) => { try { const { session_id } = req.params; // Verify the session belongs to the user const session = await prisma.user_sessions.findUnique({ where: { id: session_id }, }); if (!session || session.user_id !== req.user.id) { return res.status(404).json({ error: "Session not found" }); } // Don't allow revoking the current session if (session_id === req.session_id) { return res.status(400).json({ error: "Cannot revoke current session" }); } await revoke_session(session_id); res.json({ message: "Session revoked successfully", }); } catch (error) { console.error("Revoke session error:", error); res.status(500).json({ error: "Failed to revoke session" }); } }); // Revoke all sessions except current one router.delete("/sessions", authenticateToken, async (req, res) => { try { // Revoke all sessions except the current one await prisma.user_sessions.updateMany({ where: { user_id: req.user.id, id: { not: req.session_id }, }, data: { is_revoked: true }, }); res.json({ message: "All other sessions revoked successfully", }); } catch (error) { console.error("Revoke all sessions error:", error); res.status(500).json({ error: "Failed to revoke sessions" }); } }); module.exports = router;