mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-06 06:53:24 +00:00
Made changes to the host details area to add notes
Reconfigured JWT session timeouts
This commit is contained in:
319
backend/src/utils/session_manager.js
Normal file
319
backend/src/utils/session_manager.js
Normal file
@@ -0,0 +1,319 @@
|
||||
const jwt = require("jsonwebtoken");
|
||||
const crypto = require("crypto");
|
||||
const { PrismaClient } = require("@prisma/client");
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
/**
|
||||
* Session Manager - Handles secure session management with inactivity timeout
|
||||
*/
|
||||
|
||||
// Configuration
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user