Files
patchmon.net/backend/src/utils/session_manager.js
Muhammad Ibrahim 6988ecab12 Made github version checking better
Added functionality of Logo branding
Modified sidebar width
2025-10-05 10:55:34 +01:00

323 lines
7.4 KiB
JavaScript

const jwt = require("jsonwebtoken");
const crypto = require("node:crypto");
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
/**
* Session Manager - Handles secure session management with inactivity timeout
*/
// Configuration
if (!process.env.JWT_SECRET) {
throw new Error("JWT_SECRET environment variable is required");
}
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || "1h";
const JWT_REFRESH_EXPIRES_IN = process.env.JWT_REFRESH_EXPIRES_IN || "7d";
const INACTIVITY_TIMEOUT_MINUTES = parseInt(
process.env.SESSION_INACTIVITY_TIMEOUT_MINUTES || "30",
10,
);
/**
* Generate access token (short-lived)
*/
function generate_access_token(user_id, session_id) {
return jwt.sign({ userId: user_id, sessionId: session_id }, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
}
/**
* Generate refresh token (long-lived)
*/
function generate_refresh_token() {
return crypto.randomBytes(64).toString("hex");
}
/**
* Hash token for storage
*/
function hash_token(token) {
return crypto.createHash("sha256").update(token).digest("hex");
}
/**
* Parse expiration string to Date
*/
function parse_expiration(expiration_string) {
const match = expiration_string.match(/^(\d+)([smhd])$/);
if (!match) {
throw new Error("Invalid expiration format");
}
const value = parseInt(match[1], 10);
const unit = match[2];
const now = new Date();
switch (unit) {
case "s":
return new Date(now.getTime() + value * 1000);
case "m":
return new Date(now.getTime() + value * 60 * 1000);
case "h":
return new Date(now.getTime() + value * 60 * 60 * 1000);
case "d":
return new Date(now.getTime() + value * 24 * 60 * 60 * 1000);
default:
throw new Error("Invalid time unit");
}
}
/**
* Create a new session for user
*/
async function create_session(user_id, ip_address, user_agent) {
try {
const session_id = crypto.randomUUID();
const refresh_token = generate_refresh_token();
const access_token = generate_access_token(user_id, session_id);
const expires_at = parse_expiration(JWT_REFRESH_EXPIRES_IN);
// Store session in database
await prisma.user_sessions.create({
data: {
id: session_id,
user_id: user_id,
refresh_token: hash_token(refresh_token),
access_token_hash: hash_token(access_token),
ip_address: ip_address || null,
user_agent: user_agent || null,
last_activity: new Date(),
expires_at: expires_at,
},
});
return {
session_id,
access_token,
refresh_token,
expires_at,
};
} catch (error) {
console.error("Error creating session:", error);
throw error;
}
}
/**
* Validate session and check for inactivity timeout
*/
async function validate_session(session_id, access_token) {
try {
const session = await prisma.user_sessions.findUnique({
where: { id: session_id },
include: { users: true },
});
if (!session) {
return { valid: false, reason: "Session not found" };
}
// Check if session is revoked
if (session.is_revoked) {
return { valid: false, reason: "Session revoked" };
}
// Check if session has expired
if (new Date() > session.expires_at) {
await revoke_session(session_id);
return { valid: false, reason: "Session expired" };
}
// Check for inactivity timeout
const inactivity_threshold = new Date(
Date.now() - INACTIVITY_TIMEOUT_MINUTES * 60 * 1000,
);
if (session.last_activity < inactivity_threshold) {
await revoke_session(session_id);
return {
valid: false,
reason: "Session inactive",
message: `Session timed out after ${INACTIVITY_TIMEOUT_MINUTES} minutes of inactivity`,
};
}
// Validate access token hash (optional security check)
if (session.access_token_hash) {
const provided_hash = hash_token(access_token);
if (session.access_token_hash !== provided_hash) {
return { valid: false, reason: "Token mismatch" };
}
}
// Check if user is still active
if (!session.users.is_active) {
await revoke_session(session_id);
return { valid: false, reason: "User inactive" };
}
return {
valid: true,
session,
user: session.users,
};
} catch (error) {
console.error("Error validating session:", error);
return { valid: false, reason: "Validation error" };
}
}
/**
* Update session activity timestamp
*/
async function update_session_activity(session_id) {
try {
await prisma.user_sessions.update({
where: { id: session_id },
data: { last_activity: new Date() },
});
return true;
} catch (error) {
console.error("Error updating session activity:", error);
return false;
}
}
/**
* Refresh access token using refresh token
*/
async function refresh_access_token(refresh_token) {
try {
const hashed_token = hash_token(refresh_token);
const session = await prisma.user_sessions.findUnique({
where: { refresh_token: hashed_token },
include: { users: true },
});
if (!session) {
return { success: false, error: "Invalid refresh token" };
}
// Validate session
const validation = await validate_session(session.id, "");
if (!validation.valid) {
return { success: false, error: validation.reason };
}
// Generate new access token
const new_access_token = generate_access_token(session.user_id, session.id);
// Update access token hash
await prisma.user_sessions.update({
where: { id: session.id },
data: {
access_token_hash: hash_token(new_access_token),
last_activity: new Date(),
},
});
return {
success: true,
access_token: new_access_token,
user: session.users,
};
} catch (error) {
console.error("Error refreshing access token:", error);
return { success: false, error: "Token refresh failed" };
}
}
/**
* Revoke a session
*/
async function revoke_session(session_id) {
try {
await prisma.user_sessions.update({
where: { id: session_id },
data: { is_revoked: true },
});
return true;
} catch (error) {
console.error("Error revoking session:", error);
return false;
}
}
/**
* Revoke all sessions for a user
*/
async function revoke_all_user_sessions(user_id) {
try {
await prisma.user_sessions.updateMany({
where: { user_id: user_id },
data: { is_revoked: true },
});
return true;
} catch (error) {
console.error("Error revoking user sessions:", error);
return false;
}
}
/**
* Clean up expired sessions (should be run periodically)
*/
async function cleanup_expired_sessions() {
try {
const result = await prisma.user_sessions.deleteMany({
where: {
OR: [{ expires_at: { lt: new Date() } }, { is_revoked: true }],
},
});
console.log(`Cleaned up ${result.count} expired sessions`);
return result.count;
} catch (error) {
console.error("Error cleaning up sessions:", error);
return 0;
}
}
/**
* Get active sessions for a user
*/
async function get_user_sessions(user_id) {
try {
return await prisma.user_sessions.findMany({
where: {
user_id: user_id,
is_revoked: false,
expires_at: { gt: new Date() },
},
select: {
id: true,
ip_address: true,
user_agent: true,
last_activity: true,
created_at: true,
expires_at: true,
},
orderBy: { last_activity: "desc" },
});
} catch (error) {
console.error("Error getting user sessions:", error);
return [];
}
}
module.exports = {
create_session,
validate_session,
update_session_activity,
refresh_access_token,
revoke_session,
revoke_all_user_sessions,
cleanup_expired_sessions,
get_user_sessions,
generate_access_token,
INACTIVITY_TIMEOUT_MINUTES,
};