diff --git a/backend/env.example b/backend/env.example
index e0d713c..1db1fe7 100644
--- a/backend/env.example
+++ b/backend/env.example
@@ -23,3 +23,9 @@ ENABLE_LOGGING=true
# User Registration
DEFAULT_USER_ROLE=user
+
+# JWT Configuration
+JWT_SECRET=your-secure-random-secret-key-change-this-in-production
+JWT_EXPIRES_IN=1h
+JWT_REFRESH_EXPIRES_IN=7d
+SESSION_INACTIVITY_TIMEOUT_MINUTES=30
diff --git a/backend/prisma/migrations/20250930234123_add_host_notes/migration.sql b/backend/prisma/migrations/20250930234123_add_host_notes/migration.sql
new file mode 100644
index 0000000..3683c64
--- /dev/null
+++ b/backend/prisma/migrations/20250930234123_add_host_notes/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "hosts" ADD COLUMN "notes" TEXT;
+
diff --git a/backend/prisma/migrations/add_user_sessions/migration.sql b/backend/prisma/migrations/add_user_sessions/migration.sql
new file mode 100644
index 0000000..04a45a9
--- /dev/null
+++ b/backend/prisma/migrations/add_user_sessions/migration.sql
@@ -0,0 +1,31 @@
+-- CreateTable
+CREATE TABLE "user_sessions" (
+ "id" TEXT NOT NULL,
+ "user_id" TEXT NOT NULL,
+ "refresh_token" TEXT NOT NULL,
+ "access_token_hash" TEXT,
+ "ip_address" TEXT,
+ "user_agent" TEXT,
+ "last_activity" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "expires_at" TIMESTAMP(3) NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "is_revoked" BOOLEAN NOT NULL DEFAULT false,
+
+ CONSTRAINT "user_sessions_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "user_sessions_refresh_token_key" ON "user_sessions"("refresh_token");
+
+-- CreateIndex
+CREATE INDEX "user_sessions_user_id_idx" ON "user_sessions"("user_id");
+
+-- CreateIndex
+CREATE INDEX "user_sessions_refresh_token_idx" ON "user_sessions"("refresh_token");
+
+-- CreateIndex
+CREATE INDEX "user_sessions_expires_at_idx" ON "user_sessions"("expires_at");
+
+-- AddForeignKey
+ALTER TABLE "user_sessions" ADD CONSTRAINT "user_sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index 662018e..160aa1c 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -86,6 +86,7 @@ model hosts {
selinux_status String?
swap_size Int?
system_uptime String?
+ notes String?
host_packages host_packages[]
host_repositories host_repositories[]
host_groups host_groups? @relation(fields: [host_group_id], references: [id])
@@ -186,4 +187,23 @@ model users {
first_name String?
last_name String?
dashboard_preferences dashboard_preferences[]
+ user_sessions user_sessions[]
+}
+
+model user_sessions {
+ id String @id
+ user_id String
+ refresh_token String @unique
+ access_token_hash String?
+ ip_address String?
+ user_agent String?
+ last_activity DateTime @default(now())
+ expires_at DateTime
+ created_at DateTime @default(now())
+ is_revoked Boolean @default(false)
+ users users @relation(fields: [user_id], references: [id], onDelete: Cascade)
+
+ @@index([user_id])
+ @@index([refresh_token])
+ @@index([expires_at])
}
diff --git a/backend/src/middleware/auth.js b/backend/src/middleware/auth.js
index e49ae81..d0d82cb 100644
--- a/backend/src/middleware/auth.js
+++ b/backend/src/middleware/auth.js
@@ -1,9 +1,13 @@
const jwt = require("jsonwebtoken");
const { PrismaClient } = require("@prisma/client");
+const {
+ validate_session,
+ update_session_activity,
+} = require("../utils/session_manager");
const prisma = new PrismaClient();
-// Middleware to verify JWT token
+// Middleware to verify JWT token with session validation
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers.authorization;
@@ -19,35 +23,40 @@ const authenticateToken = async (req, res, next) => {
process.env.JWT_SECRET || "your-secret-key",
);
- // Get user from database
- const user = await prisma.users.findUnique({
- where: { id: decoded.userId },
- select: {
- id: true,
- username: true,
- email: true,
- role: true,
- is_active: true,
- last_login: true,
- created_at: true,
- updated_at: true,
- },
- });
+ // Validate session and check inactivity timeout
+ const validation = await validate_session(decoded.sessionId, token);
- if (!user || !user.is_active) {
- return res.status(401).json({ error: "Invalid or inactive user" });
+ if (!validation.valid) {
+ const error_messages = {
+ "Session not found": "Session not found",
+ "Session revoked": "Session has been revoked",
+ "Session expired": "Session has expired",
+ "Session inactive":
+ validation.message || "Session timed out due to inactivity",
+ "Token mismatch": "Invalid token",
+ "User inactive": "User account is inactive",
+ };
+
+ return res.status(401).json({
+ error: error_messages[validation.reason] || "Authentication failed",
+ reason: validation.reason,
+ });
}
- // Update last login
+ // Update session activity timestamp
+ await update_session_activity(decoded.sessionId);
+
+ // Update last login (only on successful authentication)
await prisma.users.update({
- where: { id: user.id },
+ where: { id: validation.user.id },
data: {
last_login: new Date(),
updated_at: new Date(),
},
});
- req.user = user;
+ req.user = validation.user;
+ req.session_id = decoded.sessionId;
next();
} catch (error) {
if (error.name === "JsonWebTokenError") {
diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js
index bcffc02..9d04c58 100644
--- a/backend/src/routes/authRoutes.js
+++ b/backend/src/routes/authRoutes.js
@@ -12,6 +12,13 @@ const { v4: uuidv4 } = require("uuid");
const {
createDefaultDashboardPreferences,
} = require("./dashboardPreferencesRoutes");
+const {
+ create_session,
+ refresh_access_token,
+ revoke_session,
+ revoke_all_user_sessions,
+ get_user_sessions,
+} = require("../utils/session_manager");
const router = express.Router();
const prisma = new PrismaClient();
@@ -118,12 +125,16 @@ router.post(
// Create default dashboard preferences for the new admin user
await createDefaultDashboardPreferences(user.id, "admin");
- // Generate token for immediate login
- const token = generateToken(user.id);
+ // 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,
+ token: session.access_token,
+ refresh_token: session.refresh_token,
+ expires_at: session.expires_at,
user: {
id: user.id,
username: user.username,
@@ -722,12 +733,16 @@ router.post(
},
});
- // Generate token
- const token = generateToken(user.id);
+ // 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,
+ token: session.access_token,
+ refresh_token: session.refresh_token,
+ expires_at: session.expires_at,
user: {
id: user.id,
username: user.username,
@@ -829,12 +844,16 @@ router.post(
data: { last_login: new Date() },
});
- // Generate token
- const jwtToken = generateToken(user.id);
+ // 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: jwtToken,
+ token: session.access_token,
+ refresh_token: session.refresh_token,
+ expires_at: session.expires_at,
user: {
id: user.id,
username: user.username,
@@ -1001,9 +1020,14 @@ router.put(
},
);
-// Logout (client-side token removal)
-router.post("/logout", authenticateToken, async (_req, res) => {
+// 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",
});
@@ -1013,4 +1037,94 @@ router.post("/logout", authenticateToken, async (_req, res) => {
}
});
+// 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 get_user_sessions(req.user.id);
+
+ res.json({
+ sessions: 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" });
+ }
+
+ 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" });
+ }
+});
+
module.exports = router;
diff --git a/backend/src/routes/dashboardRoutes.js b/backend/src/routes/dashboardRoutes.js
index 48edf2e..fb54447 100644
--- a/backend/src/routes/dashboardRoutes.js
+++ b/backend/src/routes/dashboardRoutes.js
@@ -194,6 +194,7 @@ router.get("/hosts", authenticateToken, requireViewHosts, async (_req, res) => {
status: true,
agent_version: true,
auto_update: true,
+ notes: true,
host_groups: {
select: {
id: true,
diff --git a/backend/src/routes/hostRoutes.js b/backend/src/routes/hostRoutes.js
index 8a36a64..842f286 100644
--- a/backend/src/routes/hostRoutes.js
+++ b/backend/src/routes/hostRoutes.js
@@ -858,6 +858,7 @@ router.get(
auto_update: true,
created_at: true,
host_group_id: true,
+ notes: true,
host_groups: {
select: {
id: true,
@@ -1491,4 +1492,78 @@ router.patch(
},
);
+// Update host notes (admin only)
+router.patch(
+ "/:hostId/notes",
+ authenticateToken,
+ requireManageHosts,
+ [
+ body("notes")
+ .optional()
+ .isLength({ max: 1000 })
+ .withMessage("Notes must be less than 1000 characters"),
+ ],
+ async (req, res) => {
+ try {
+ const errors = validationResult(req);
+ if (!errors.isEmpty()) {
+ return res.status(400).json({ errors: errors.array() });
+ }
+
+ const { hostId } = req.params;
+ const { notes } = req.body;
+
+ // Check if host exists
+ const existingHost = await prisma.hosts.findUnique({
+ where: { id: hostId },
+ });
+
+ if (!existingHost) {
+ return res.status(404).json({ error: "Host not found" });
+ }
+
+ // Update the notes
+ const updatedHost = await prisma.hosts.update({
+ where: { id: hostId },
+ data: {
+ notes: notes || null,
+ updated_at: new Date(),
+ },
+ select: {
+ id: true,
+ friendly_name: true,
+ hostname: true,
+ ip: true,
+ os_type: true,
+ os_version: true,
+ architecture: true,
+ last_update: true,
+ status: true,
+ host_group_id: true,
+ agent_version: true,
+ auto_update: true,
+ created_at: true,
+ updated_at: true,
+ notes: true,
+ host_groups: {
+ select: {
+ id: true,
+ name: true,
+ color: true,
+ },
+ },
+ },
+ });
+
+ res.json({
+ message: "Notes updated successfully",
+ host: updatedHost,
+ });
+ } catch (error) {
+ console.error("Update notes error:", error);
+ res.status(500).json({ error: "Failed to update notes" });
+ }
+ },
+);
+
module.exports = router;
diff --git a/backend/src/server.js b/backend/src/server.js
index 832cefc..78fc180 100644
--- a/backend/src/server.js
+++ b/backend/src/server.js
@@ -26,6 +26,7 @@ const versionRoutes = require("./routes/versionRoutes");
const tfaRoutes = require("./routes/tfaRoutes");
const updateScheduler = require("./services/updateScheduler");
const { initSettings } = require("./services/settingsService");
+const { cleanup_expired_sessions } = require("./utils/session_manager");
// Initialize Prisma client with optimized connection pooling for multiple instances
const prisma = createPrismaClient();
@@ -399,6 +400,9 @@ process.on("SIGINT", async () => {
if (process.env.ENABLE_LOGGING === "true") {
logger.info("SIGINT received, shutting down gracefully");
}
+ if (app.locals.session_cleanup_interval) {
+ clearInterval(app.locals.session_cleanup_interval);
+ }
updateScheduler.stop();
await disconnectPrisma(prisma);
process.exit(0);
@@ -408,6 +412,9 @@ process.on("SIGTERM", async () => {
if (process.env.ENABLE_LOGGING === "true") {
logger.info("SIGTERM received, shutting down gracefully");
}
+ if (app.locals.session_cleanup_interval) {
+ clearInterval(app.locals.session_cleanup_interval);
+ }
updateScheduler.stop();
await disconnectPrisma(prisma);
process.exit(0);
@@ -671,15 +678,35 @@ async function startServer() {
// Initialize dashboard preferences for all users
await initializeDashboardPreferences();
+
+ // Initial session cleanup
+ await cleanup_expired_sessions();
+
+ // Schedule session cleanup every hour
+ const session_cleanup_interval = setInterval(
+ async () => {
+ try {
+ await cleanup_expired_sessions();
+ } catch (error) {
+ console.error("Session cleanup error:", error);
+ }
+ },
+ 60 * 60 * 1000,
+ ); // Every hour
+
app.listen(PORT, () => {
if (process.env.ENABLE_LOGGING === "true") {
logger.info(`Server running on port ${PORT}`);
logger.info(`Environment: ${process.env.NODE_ENV}`);
+ logger.info("✅ Session cleanup scheduled (every hour)");
}
// Start update scheduler
updateScheduler.start();
});
+
+ // Store interval for cleanup on shutdown
+ app.locals.session_cleanup_interval = session_cleanup_interval;
} catch (error) {
console.error("❌ Failed to start server:", error.message);
process.exit(1);
diff --git a/backend/src/utils/session_manager.js b/backend/src/utils/session_manager.js
new file mode 100644
index 0000000..b941ec5
--- /dev/null
+++ b/backend/src/utils/session_manager.js
@@ -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,
+};
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index af5b350..a9642ca 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -17,7 +17,9 @@ import Profile from "./pages/Profile";
import Repositories from "./pages/Repositories";
import RepositoryDetail from "./pages/RepositoryDetail";
import AlertChannels from "./pages/settings/AlertChannels";
+import Integrations from "./pages/settings/Integrations";
import Notifications from "./pages/settings/Notifications";
+import PatchManagement from "./pages/settings/PatchManagement";
import SettingsAgentConfig from "./pages/settings/SettingsAgentConfig";
import SettingsHostGroups from "./pages/settings/SettingsHostGroups";
import SettingsServerConfig from "./pages/settings/SettingsServerConfig";
@@ -248,6 +250,26 @@ function AppRoutes() {
}
/>
+
- Architecture
-
- {host.architecture}
-
+ Architecture
+
+ {host.architecture}
+
- Kernel Version
-
- {host.kernel_version}
-
+ Kernel Version
+
+ {host.kernel_version}
+
- SELinux Status
-
+ SELinux Status
+
+ System Uptime
+
+ {host.system_uptime}
+
+ CPU Model
+
+ {host.cpu_model}
+
+ CPU Cores
+
+ {host.cpu_cores}
+
+ RAM Installed
+
+ {host.ram_installed} GB
+
+ Swap Size
+
+ {host.swap_size} GB
+
+ Load Average
+
+ {host.load_average
+ .filter((load) => load != null)
+ .map((load, index) => (
+
+ {typeof load === "number"
+ ? load.toFixed(2)
+ : String(load)}
+ {index <
+ host.load_average.filter(
+ (load) => load != null,
+ ).length -
+ 1 && ", "}
+
+ ))}
+
+ Size: {disk.size}
+
+ Mount: {disk.mountpoint}
+
+ No system information available
+
+ System information will appear once the agent collects
+ data from this host
+
- No system information available
-
- System Uptime
-
- {host.system_uptime}
-
- CPU Model
-
- {host.cpu_model}
-
- CPU Cores
-
- {host.cpu_cores}
-
- RAM Installed
-
- {host.ram_installed} GB
-
- Swap Size
-
- {host.swap_size} GB
-
- Load Average
-
- {host.load_average
- .filter((load) => load != null)
- .map((load, index) => (
-
- {typeof load === "number"
- ? load.toFixed(2)
- : String(load)}
- {index <
- host.load_average.filter(
- (load) => load != null,
- ).length -
- 1 && ", "}
-
- ))}
-
- Size: {disk.size}
-
- Mount: {disk.mountpoint}
-
- No monitoring data available
-
- Monitoring data will appear once the agent collects
- system information
-
+
+
+
+
+ {/* System Overview */}
+
+
+
-
-
+ Host Notes
+
+
Get started by adding your first alert channel.
-+ Connect PatchMon to third-party services +
++ We are building integrations for Slack, Discord, email, and + webhooks to streamline alerts and workflows. +
+- Monitor system health and performance events -
-Get started by creating your first notification rule.
-+ Define and enforce policies for applying and monitoring patches +
++ We are designing rule sets, approval workflows, and automated + patch policies to give you granular control over updates. +
++ Configure rule sets for patch management and monitoring +
+