Files
patchmon.net/backend/src/routes/authRoutes.js
Muhammad Ibrahim c4d0d8bee8 Fixed repo count issue
Refactored code to remove duplicate backend api endpoints for counting
Improved connection persistence issues
Improved database connection pooling issues
Fixed redis connection efficiency
Changed version to 1.3.0
Fixed GO binary detection based on package manager rather than OS
2025-10-19 17:53:10 +01:00

1274 lines
31 KiB
JavaScript

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,
} = 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) {
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);
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,
},
});
} 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 digits"),
body("token").isNumeric().withMessage("Token must contain only numbers"),
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
await prisma.users.update({
where: { id: user.id },
data: { last_login: 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,
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: {
id: user.id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
role: user.role,
},
});
} 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()
.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"),
],
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 = {};
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;
// 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" });
}
}
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,
},
});
res.json({
message: "Profile updated successfully",
user: updatedUser,
});
} 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;