Files
patchmon.net/backend/src/routes/authRoutes.js
2025-10-31 22:17:24 +00:00

1371 lines
34 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,
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;