commit
0a331ab00c
|
|
@ -944,12 +944,19 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
|||
$scope.loadingSSHLogins = false;
|
||||
if (response.data && response.data.logins) {
|
||||
$scope.sshLogins = response.data.logins;
|
||||
// Debug: Log first login to see structure
|
||||
if ($scope.sshLogins.length > 0) {
|
||||
console.log('First SSH login object:', $scope.sshLogins[0]);
|
||||
console.log('IP field:', $scope.sshLogins[0].ip);
|
||||
console.log('All keys:', Object.keys($scope.sshLogins[0]));
|
||||
}
|
||||
} else {
|
||||
$scope.sshLogins = [];
|
||||
}
|
||||
}, function (err) {
|
||||
$scope.loadingSSHLogins = false;
|
||||
$scope.errorSSHLogins = 'Failed to load SSH logins.';
|
||||
console.error('Failed to load SSH logins:', err);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -1552,28 +1559,419 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
|||
$scope.loadingSSHActivity = false;
|
||||
$scope.errorSSHActivity = '';
|
||||
|
||||
$scope.viewSSHActivity = function(login) {
|
||||
$scope.viewSSHActivity = function(login, event) {
|
||||
$scope.showSSHActivityModal = true;
|
||||
$scope.sshActivity = { processes: [], w: [] };
|
||||
$scope.sshActivityUser = login.user;
|
||||
|
||||
// Extract IP from multiple sources - comprehensive extraction for IPv4 and IPv6
|
||||
var extractedIP = '';
|
||||
|
||||
// Method 1: Direct property access (highest priority - from backend)
|
||||
if (login && login.ip) {
|
||||
extractedIP = login.ip.toString().trim();
|
||||
} else if (login && login['ip']) {
|
||||
extractedIP = login['ip'].toString().trim();
|
||||
}
|
||||
|
||||
// Method 2: Alternative field names
|
||||
if (!extractedIP && login) {
|
||||
if (login.ipAddress) extractedIP = login.ipAddress.toString().trim();
|
||||
else if (login['IP Address']) extractedIP = login['IP Address'].toString().trim();
|
||||
else if (login['IP']) extractedIP = login['IP'].toString().trim();
|
||||
}
|
||||
|
||||
// Method 3: Extract from raw line using regex (IPv4 and IPv6)
|
||||
if (!extractedIP && login && login.raw) {
|
||||
// Try IPv4 first (most common)
|
||||
var ipv4Match = login.raw.match(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/);
|
||||
if (ipv4Match && ipv4Match[1]) {
|
||||
var ipv4 = ipv4Match[1].trim();
|
||||
if (ipv4 !== '127.0.0.1' && ipv4 !== '0.0.0.0') {
|
||||
extractedIP = ipv4;
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid IPv4, try IPv6
|
||||
if (!extractedIP) {
|
||||
// IPv6 pattern: matches full IPv6 addresses and compressed forms
|
||||
var ipv6Pattern = /([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/;
|
||||
var ipv6Match = login.raw.match(ipv6Pattern);
|
||||
if (ipv6Match && ipv6Match[0]) {
|
||||
var ipv6 = ipv6Match[0].trim();
|
||||
if (ipv6 !== '::1' && ipv6.length > 0) {
|
||||
extractedIP = ipv6;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Method 4: Try to get from event target data attribute as fallback
|
||||
if (!extractedIP && event && event.currentTarget) {
|
||||
var dataIP = event.currentTarget.getAttribute('data-ip');
|
||||
if (dataIP) extractedIP = dataIP.toString().trim();
|
||||
}
|
||||
|
||||
// Final fallback: search entire raw line for any IP
|
||||
if (!extractedIP && login && login.raw) {
|
||||
// Try all IPv4 addresses
|
||||
var allIPv4s = login.raw.match(/\b(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\b/g);
|
||||
if (allIPv4s && allIPv4s.length > 0) {
|
||||
for (var i = 0; i < allIPv4s.length; i++) {
|
||||
var ip = allIPv4s[i].trim();
|
||||
if (ip !== '127.0.0.1' && ip !== '0.0.0.0' && ip.length > 0) {
|
||||
extractedIP = ip;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If no IPv4, try all IPv6 addresses
|
||||
if (!extractedIP) {
|
||||
var allIPv6s = login.raw.match(/([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}/g);
|
||||
if (allIPv6s && allIPv6s.length > 0) {
|
||||
for (var j = 0; j < allIPv6s.length; j++) {
|
||||
var ip6 = allIPv6s[j].trim();
|
||||
if (ip6 !== '::1' && ip6.length > 0) {
|
||||
extractedIP = ip6;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final cleanup
|
||||
$scope.sshActivityIP = (extractedIP || '').toString().trim();
|
||||
$scope.sshActivityTTY = ''; // Store TTY for kill session
|
||||
// Check both 'session' and 'activity' fields for status
|
||||
$scope.sshActivityStatus = login.session || login.activity || '';
|
||||
|
||||
// Use backend is_active field if available (most reliable)
|
||||
// Fallback to checking session text if is_active is not set
|
||||
// IMPORTANT: Check for both boolean true and string 'true' (JSON might serialize differently)
|
||||
if (login.is_active !== undefined && login.is_active !== null) {
|
||||
// Backend explicitly set is_active
|
||||
$scope.isActiveSession = (login.is_active === true || login.is_active === 'true' || login.is_active === 1 || login.is_active === '1');
|
||||
console.log('Using backend is_active field:', login.is_active, '-> isActiveSession:', $scope.isActiveSession);
|
||||
} else {
|
||||
// Fallback: check session text
|
||||
var sessionStatus = ($scope.sshActivityStatus || '').toLowerCase();
|
||||
$scope.isActiveSession = (sessionStatus.indexOf('still logged in') !== -1);
|
||||
console.log('Using fallback session text check:', sessionStatus, '-> isActiveSession:', $scope.isActiveSession);
|
||||
}
|
||||
|
||||
// If IP is still empty, try one more time with more aggressive extraction
|
||||
if (!$scope.sshActivityIP && login) {
|
||||
console.log('IP still empty, trying aggressive extraction...');
|
||||
// Try every possible field name variation
|
||||
var possibleIPFields = ['ip', 'IP', 'ipAddress', 'IP Address', 'ip_address', 'IP_ADDRESS', 'client_ip', 'clientIP', 'source_ip', 'sourceIP'];
|
||||
for (var k = 0; k < possibleIPFields.length; k++) {
|
||||
if (login[possibleIPFields[k]]) {
|
||||
var testIP = login[possibleIPFields[k]].toString().trim();
|
||||
// Validate it looks like an IP (IPv4 or IPv6)
|
||||
if (testIP.match(/^(\d{1,3}\.){3}\d{1,3}$/) || testIP.match(/^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/)) {
|
||||
if (testIP !== '127.0.0.1' && testIP !== '0.0.0.0' && testIP !== '::1') {
|
||||
$scope.sshActivityIP = testIP;
|
||||
console.log('Found IP in field', possibleIPFields[k], ':', $scope.sshActivityIP);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Last resort: check if IP is in the table cell itself (from DOM)
|
||||
if (!$scope.sshActivityIP && event && event.currentTarget) {
|
||||
try {
|
||||
var row = event.currentTarget.closest('tr');
|
||||
if (row) {
|
||||
// Try different column positions (IP could be in different positions)
|
||||
var cells = row.querySelectorAll('td');
|
||||
for (var cellIdx = 0; cellIdx < cells.length; cellIdx++) {
|
||||
var cellText = cells[cellIdx].textContent.trim();
|
||||
// Check if this cell contains an IP address
|
||||
var ipMatch = cellText.match(/^(\d{1,3}\.){3}\d{1,3}$/) || cellText.match(/^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$/);
|
||||
if (ipMatch && cellText !== '127.0.0.1' && cellText !== '0.0.0.0' && cellText !== '::1') {
|
||||
$scope.sshActivityIP = cellText;
|
||||
console.log('Found IP from table cell (column ' + cellIdx + '):', $scope.sshActivityIP);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('Error extracting IP from DOM:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logging - detailed inspection
|
||||
console.log('View SSH Activity - Login object:', login);
|
||||
console.log('Login keys:', Object.keys(login));
|
||||
console.log('login.ip:', login.ip);
|
||||
console.log('login.is_active:', login.is_active);
|
||||
console.log('Extracted IP:', $scope.sshActivityIP);
|
||||
console.log('Session status:', $scope.sshActivityStatus);
|
||||
console.log('Is active session:', $scope.isActiveSession);
|
||||
$scope.showFullJSON = false; // Collapsible JSON view
|
||||
$scope.loadingSSHActivity = true;
|
||||
$scope.errorSSHActivity = '';
|
||||
$scope.killingProcess = null;
|
||||
$scope.killingSession = false;
|
||||
var tty = '';
|
||||
// Try to extract tty from login.raw or login.session if available
|
||||
if (login.raw) {
|
||||
var match = login.raw.match(/(pts\/[0-9]+)/);
|
||||
if (match) tty = match[1];
|
||||
if (match) {
|
||||
tty = match[1];
|
||||
$scope.sshActivityTTY = tty;
|
||||
}
|
||||
}
|
||||
$http.post('/base/getSSHUserActivity', { user: login.user, tty: tty }).then(function(response) {
|
||||
// Also try to extract from session field or raw line
|
||||
if (!tty && login.session) {
|
||||
var sessionMatch = login.session.match(/(pts\/[0-9]+)/);
|
||||
if (sessionMatch) {
|
||||
tty = sessionMatch[1];
|
||||
$scope.sshActivityTTY = tty;
|
||||
}
|
||||
}
|
||||
// Also check raw line for TTY
|
||||
if (!tty && login.raw) {
|
||||
var rawMatch = login.raw.match(/(pts\/[0-9]+)/);
|
||||
if (rawMatch) {
|
||||
tty = rawMatch[1];
|
||||
$scope.sshActivityTTY = tty;
|
||||
}
|
||||
}
|
||||
// Make API call with IP included - reduced timeout for faster response
|
||||
var requestData = {
|
||||
user: login.user,
|
||||
tty: tty,
|
||||
ip: $scope.sshActivityIP
|
||||
};
|
||||
|
||||
// Set shorter timeout for faster feedback
|
||||
var timeoutPromise = $timeout(function() {
|
||||
$scope.loadingSSHActivity = false;
|
||||
$scope.errorSSHActivity = 'Request timed out. The user may not have any active processes.';
|
||||
$scope.sshActivity = { processes: [], w: [] };
|
||||
}, 5000); // 5 second timeout (reduced from 10)
|
||||
|
||||
$http.post('/base/getSSHUserActivity', requestData, { timeout: 3000 }).then(function(response) {
|
||||
$timeout.cancel(timeoutPromise); // Cancel timeout on success
|
||||
$scope.loadingSSHActivity = false;
|
||||
if (response.data) {
|
||||
$scope.sshActivity = response.data;
|
||||
// Check if response has error field
|
||||
if (response.data.error) {
|
||||
$scope.errorSSHActivity = response.data.error;
|
||||
$scope.sshActivity = { processes: [], w: [] };
|
||||
} else {
|
||||
$scope.sshActivity = response.data;
|
||||
// Ensure all expected fields exist
|
||||
if (!$scope.sshActivity.processes) $scope.sshActivity.processes = [];
|
||||
if (!$scope.sshActivity.w) $scope.sshActivity.w = [];
|
||||
if (!$scope.sshActivity.process_tree) $scope.sshActivity.process_tree = [];
|
||||
if (!$scope.sshActivity.shell_history) $scope.sshActivity.shell_history = [];
|
||||
|
||||
// Try to extract TTY from processes if not already set
|
||||
if (!$scope.sshActivityTTY && response.data.processes && response.data.processes.length > 0) {
|
||||
var firstProcess = response.data.processes[0];
|
||||
if (firstProcess.tty) {
|
||||
$scope.sshActivityTTY = firstProcess.tty;
|
||||
}
|
||||
}
|
||||
// Update active session status - prioritize backend is_active field
|
||||
// The backend already determined if session is active, so trust that first
|
||||
// Only update if we have additional evidence (processes/w output)
|
||||
var hasProcesses = response.data.processes && response.data.processes.length > 0;
|
||||
var hasActiveW = response.data.w && response.data.w.length > 0;
|
||||
|
||||
// If backend says it's active, keep it active (don't override)
|
||||
// If backend says inactive but we find processes/w, mark as active
|
||||
if ($scope.isActiveSession === true) {
|
||||
// Backend already marked as active, keep it that way
|
||||
// (processes might not have loaded yet, but session is still active)
|
||||
} else if (hasProcesses || hasActiveW) {
|
||||
// Backend said inactive, but we found evidence it's active
|
||||
$scope.isActiveSession = true;
|
||||
}
|
||||
// If backend said inactive and no processes found, keep as inactive
|
||||
|
||||
// Debug logging
|
||||
console.log('SSH Activity loaded:', {
|
||||
processes: response.data.processes ? response.data.processes.length : 0,
|
||||
w: response.data.w ? response.data.w.length : 0,
|
||||
hasProcesses: hasProcesses,
|
||||
hasActiveW: hasActiveW,
|
||||
originalStatus: $scope.sshActivityStatus,
|
||||
isActiveSession: $scope.isActiveSession,
|
||||
ip: $scope.sshActivityIP
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$scope.sshActivity = { processes: [], w: [] };
|
||||
$scope.errorSSHActivity = 'No data returned from server.';
|
||||
}
|
||||
}, function(err) {
|
||||
$timeout.cancel(timeoutPromise); // Cancel timeout on error
|
||||
$scope.loadingSSHActivity = false;
|
||||
$scope.errorSSHActivity = (err.data && err.data.error) ? err.data.error : 'Failed to fetch activity.';
|
||||
var errorMsg = 'Failed to fetch activity.';
|
||||
|
||||
// Handle different error scenarios
|
||||
if (err.data) {
|
||||
// Server returned error data
|
||||
if (err.data.error) {
|
||||
errorMsg = err.data.error;
|
||||
} else if (typeof err.data === 'string') {
|
||||
errorMsg = err.data;
|
||||
} else if (err.data.message) {
|
||||
errorMsg = err.data.message;
|
||||
}
|
||||
} else if (err.status === 0 || err.status === -1) {
|
||||
errorMsg = 'Network error. Please check your connection and try again.';
|
||||
} else if (err.status >= 500) {
|
||||
errorMsg = 'Server error (HTTP ' + err.status + '). Please try again later.';
|
||||
} else if (err.status === 404) {
|
||||
errorMsg = 'Endpoint not found. Please refresh the page.';
|
||||
} else if (err.status === 403) {
|
||||
errorMsg = 'Access denied. Admin privileges required.';
|
||||
} else if (err.status === 400) {
|
||||
errorMsg = 'Invalid request. Please check the user information.';
|
||||
} else if (err.status) {
|
||||
errorMsg = 'Request failed with status ' + err.status + '.';
|
||||
} else if (err.message) {
|
||||
errorMsg = err.message;
|
||||
}
|
||||
|
||||
$scope.errorSSHActivity = errorMsg;
|
||||
// Set empty activity data so modal can still display
|
||||
$scope.sshActivity = {
|
||||
processes: [],
|
||||
w: [],
|
||||
process_tree: [],
|
||||
shell_history: [],
|
||||
disk_usage: '',
|
||||
geoip: {}
|
||||
};
|
||||
|
||||
// Log error for debugging
|
||||
console.error('SSH Activity fetch error:', err);
|
||||
});
|
||||
};
|
||||
|
||||
// Kill individual process
|
||||
$scope.killProcess = function(pid, user) {
|
||||
if (!confirm('Are you sure you want to force kill process ' + pid + '? This action cannot be undone.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.killingProcess = pid;
|
||||
|
||||
var data = {
|
||||
pid: pid,
|
||||
user: user
|
||||
};
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post('/base/killSSHProcess', data, config).then(function(response) {
|
||||
$scope.killingProcess = null;
|
||||
if (response.data && response.data.success) {
|
||||
new PNotify({
|
||||
title: 'Process Killed',
|
||||
text: response.data.message || 'Process ' + pid + ' has been terminated.',
|
||||
type: 'success',
|
||||
delay: 3000
|
||||
});
|
||||
// Refresh activity to update process list
|
||||
$scope.viewSSHActivity({ user: user, ip: $scope.sshActivityIP, raw: '', session: '' });
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: (response.data && response.data.error) || 'Failed to kill process.',
|
||||
type: 'error',
|
||||
delay: 5000
|
||||
});
|
||||
}
|
||||
}, function(err) {
|
||||
$scope.killingProcess = null;
|
||||
var errorMsg = 'Failed to kill process.';
|
||||
if (err.data && err.data.error) {
|
||||
errorMsg = err.data.error;
|
||||
} else if (err.data && err.data.message) {
|
||||
errorMsg = err.data.message;
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: errorMsg,
|
||||
type: 'error',
|
||||
delay: 5000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Kill entire SSH session
|
||||
$scope.killSSHSession = function(user, tty) {
|
||||
var confirmMsg = 'Are you sure you want to kill all processes for user ' + user;
|
||||
if (tty) {
|
||||
confirmMsg += ' on terminal ' + tty;
|
||||
}
|
||||
confirmMsg += '? This will terminate their SSH session.';
|
||||
|
||||
if (!confirm(confirmMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.killingSession = true;
|
||||
|
||||
var data = {
|
||||
user: user,
|
||||
tty: tty || ''
|
||||
};
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
};
|
||||
|
||||
$http.post('/base/killSSHSession', data, config).then(function(response) {
|
||||
$scope.killingSession = false;
|
||||
if (response.data && response.data.success) {
|
||||
new PNotify({
|
||||
title: 'Session Terminated',
|
||||
text: response.data.message || 'SSH session has been terminated successfully.',
|
||||
type: 'success',
|
||||
delay: 3000
|
||||
});
|
||||
// Close modal and refresh page after a delay
|
||||
setTimeout(function() {
|
||||
$scope.closeSSHActivityModal();
|
||||
location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: (response.data && response.data.error) || 'Failed to kill session.',
|
||||
type: 'error',
|
||||
delay: 5000
|
||||
});
|
||||
}
|
||||
}, function(err) {
|
||||
$scope.killingSession = false;
|
||||
var errorMsg = 'Failed to kill session.';
|
||||
if (err.data && err.data.error) {
|
||||
errorMsg = err.data.error;
|
||||
} else if (err.data && err.data.message) {
|
||||
errorMsg = err.data.message;
|
||||
}
|
||||
new PNotify({
|
||||
title: 'Error',
|
||||
text: errorMsg,
|
||||
type: 'error',
|
||||
delay: 5000
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -1581,8 +1979,15 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) {
|
|||
$scope.showSSHActivityModal = false;
|
||||
$scope.sshActivity = { processes: [], w: [] };
|
||||
$scope.sshActivityUser = '';
|
||||
$scope.sshActivityIP = ''; // Clear IP when closing modal
|
||||
$scope.sshActivityTTY = ''; // Clear TTY when closing modal
|
||||
$scope.sshActivityStatus = ''; // Clear activity status
|
||||
$scope.isActiveSession = false; // Reset active session flag
|
||||
$scope.showFullJSON = false; // Reset JSON view
|
||||
$scope.loadingSSHActivity = false;
|
||||
$scope.errorSSHActivity = '';
|
||||
$scope.killingProcess = null;
|
||||
$scope.killingSession = false;
|
||||
};
|
||||
|
||||
// Close modal when clicking backdrop
|
||||
|
|
|
|||
|
|
@ -230,34 +230,114 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.session-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.active-session {
|
||||
background-color: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.active-session i {
|
||||
color: #10b981;
|
||||
font-size: 8px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.inactive-session {
|
||||
background-color: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.inactive-session i {
|
||||
color: #ef4444;
|
||||
font-size: 8px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.session-status span {
|
||||
color: #64748b;
|
||||
font-weight: normal;
|
||||
margin-left: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.activity-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--bg-primary, white);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-primary, #e8e9ff);
|
||||
margin-top: 15px;
|
||||
table-layout: fixed;
|
||||
display: table;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.activity-table-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.activity-table th,
|
||||
.activity-table td {
|
||||
display: table-cell !important;
|
||||
}
|
||||
|
||||
.activity-table tr {
|
||||
display: table-row !important;
|
||||
}
|
||||
|
||||
.activity-table thead {
|
||||
display: table-header-group !important;
|
||||
background: linear-gradient(135deg, #5b5fcf 0%, #4a4fc7 100%);
|
||||
}
|
||||
|
||||
.activity-table tbody {
|
||||
display: table-row-group !important;
|
||||
}
|
||||
|
||||
.activity-table th {
|
||||
text-align: left;
|
||||
padding: 12px 15px;
|
||||
padding: 14px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--text-secondary, #64748b);
|
||||
color: #ffffff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
border-bottom: 2px solid var(--border-color, #e8e9ff);
|
||||
border-bottom: 2px solid rgba(91, 95, 207, 0.3);
|
||||
background: linear-gradient(135deg, #5b5fcf 0%, #4a4fc7 100%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
vertical-align: middle;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.activity-table tbody tr {
|
||||
border-bottom: 1px solid var(--border-color, #f0f0ff);
|
||||
}
|
||||
|
||||
.activity-table tbody tr:hover {
|
||||
background: var(--bg-hover, #f8f9ff);
|
||||
}
|
||||
|
||||
.activity-table td {
|
||||
padding: 12px 15px;
|
||||
padding: 12px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary, #2f3640);
|
||||
border-bottom: 1px solid var(--border-color, #f0f0ff);
|
||||
}
|
||||
|
||||
.activity-table tr:hover {
|
||||
background: var(--bg-hover, #f8f9ff);
|
||||
vertical-align: middle;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.view-activity-btn {
|
||||
|
|
@ -298,23 +378,37 @@
|
|||
height: 100vh;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
display: none !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
backdrop-filter: blur(2px);
|
||||
/* Initially hidden to prevent flicker on page load */
|
||||
display: none;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-backdrop.show {
|
||||
display: flex;
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.modal-backdrop.ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.modal-backdrop.show.ng-cloak {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
/* Ensure modal is hidden when ng-show is false */
|
||||
.modal-backdrop:not(.show) {
|
||||
display: none !important;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
width: 600px;
|
||||
width: 800px;
|
||||
background: var(--bg-secondary, #fff);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 8px 40px rgba(0,0,0,0.18);
|
||||
|
|
@ -322,6 +416,202 @@
|
|||
position: relative;
|
||||
overflow-y: auto;
|
||||
animation: modalFadeIn 0.3s ease-out;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 2px solid var(--border-color, #e8e9ff);
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: #5b5fcf;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.modal-close-btn {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 1.8rem;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-close-btn:hover {
|
||||
background: var(--bg-hover, #f8f9ff);
|
||||
color: #2f3640;
|
||||
}
|
||||
|
||||
.activity-content {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.activity-json {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: #2f3640;
|
||||
}
|
||||
|
||||
.process-list {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.process-item {
|
||||
background: white;
|
||||
border: 1px solid var(--border-color, #e8e9ff);
|
||||
border-radius: 8px;
|
||||
padding: 12px 15px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.process-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.process-pid {
|
||||
font-weight: 600;
|
||||
color: #5b5fcf;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.process-cmd {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.process-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.kill-process-btn {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 14px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.kill-process-btn:hover {
|
||||
background: #dc2626;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.3);
|
||||
}
|
||||
|
||||
.kill-process-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 2px solid var(--border-color, #e8e9ff);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.modal-actions-left {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.modal-actions-right {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.kill-session-btn {
|
||||
background: #f59e0b;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.kill-session-btn:hover {
|
||||
background: #d97706;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.kill-session-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.ban-ip-btn {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.ban-ip-btn:hover:not(:disabled) {
|
||||
background: #b91c1c;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(220, 38, 38, 0.3);
|
||||
}
|
||||
|
||||
.ban-ip-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
|
|
@ -506,19 +796,19 @@
|
|||
{$ securityAlerts.length $}
|
||||
</span>
|
||||
</button>
|
||||
<button class="activity-tab" onclick="switchTab('top-process', this)" ng-hide="hideSystemCharts">
|
||||
<button class="activity-tab" onclick="switchTab('top-process', this)">
|
||||
<i class="fas fa-microchip"></i>
|
||||
<span>Top Process</span>
|
||||
</button>
|
||||
<button class="activity-tab" onclick="switchTab('traffic', this)" ng-hide="hideSystemCharts">
|
||||
<button class="activity-tab" onclick="switchTab('traffic', this)">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
<span>Traffic</span>
|
||||
</button>
|
||||
<button class="activity-tab" onclick="switchTab('diskio', this)" ng-hide="hideSystemCharts">
|
||||
<button class="activity-tab" onclick="switchTab('diskio', this)">
|
||||
<i class="fas fa-hard-drive"></i>
|
||||
<span>Disk IO</span>
|
||||
</button>
|
||||
<button class="activity-tab" onclick="switchTab('cpu-usage', this)" ng-hide="hideSystemCharts">
|
||||
<button class="activity-tab" onclick="switchTab('cpu-usage', this)">
|
||||
<i class="fas fa-tachometer-alt"></i>
|
||||
<span>CPU Usage</span>
|
||||
</button>
|
||||
|
|
@ -533,7 +823,8 @@
|
|||
<div ng-if="!loadingSSHLogins && sshLogins.length === 0" style="text-align: center; padding: 20px; color: #8893a7;">
|
||||
No recent SSH logins found.
|
||||
</div>
|
||||
<table class="activity-table" ng-if="!loadingSSHLogins && sshLogins.length > 0">
|
||||
<div class="activity-table-wrapper">
|
||||
<table class="activity-table records-table" ng-if="!loadingSSHLogins && sshLogins.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%;">USER</th>
|
||||
|
|
@ -548,26 +839,35 @@
|
|||
<tr ng-repeat="login in sshLogins">
|
||||
<td>{$ login.user $}</td>
|
||||
<td>{$ login.ip $}</td>
|
||||
<td>{$ login.country $}</td>
|
||||
<td>{$ login.date $}</td>
|
||||
<td>{$ login.session $}</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<button class="view-activity-btn" ng-click="viewSSHActivity(login)">View Activity</button>
|
||||
<button class="ban-ip-btn" ng-click="blockIPAddress(login.ip)"
|
||||
style="background: #dc2626; color: white; border: 1px solid #dc2626; padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer;"
|
||||
onmouseover="this.style.background='#b91c1c'"
|
||||
onmouseout="this.style.background='#dc2626'">
|
||||
<i class="fas fa-ban"></i> Ban IP
|
||||
</button>
|
||||
<div style="display: flex; align-items: center; gap: 6px;">
|
||||
<img ng-if="login.flag" ng-src="{$ login.flag $}" alt="{$ login.country $}" style="width: 24px; height: 18px; border: 1px solid #e8e9ff; border-radius: 2px;" />
|
||||
<span>{$ login.country $}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{$ login.date $}</td>
|
||||
<td>
|
||||
<span ng-if="login.is_active" class="session-status active-session">
|
||||
<i class="fas fa-circle" style="color: #10b981; font-size: 8px; margin-right: 6px;"></i>
|
||||
<strong>ACTIVE</strong>
|
||||
<span style="color: #64748b; font-weight: normal; margin-left: 6px;">({$ login.session $})</span>
|
||||
</span>
|
||||
<span ng-if="!login.is_active" class="session-status inactive-session">
|
||||
<i class="fas fa-circle" style="color: #ef4444; font-size: 8px; margin-right: 6px;"></i>
|
||||
<strong>INACTIVE</strong>
|
||||
<span style="color: #64748b; font-weight: normal; margin-left: 6px;">({$ login.session $})</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="view-activity-btn" ng-click="viewSSHActivity(login, $event)">View Activity</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Dummy data for demonstration -->
|
||||
<table class="activity-table" ng-if="loadingSSHLogins">
|
||||
<table class="activity-table records-table" ng-if="loadingSSHLogins">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 15%;">USER</th>
|
||||
|
|
@ -586,12 +886,7 @@
|
|||
<td>Wed Jun 4 20:59</td>
|
||||
<td>Still Logged In</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 8px; align-items: center;">
|
||||
<button class="view-activity-btn" style="background: #5b5fcf; color: white; border-color: #5b5fcf;">View Activity</button>
|
||||
<button class="ban-ip-btn" style="background: #dc2626; color: white; border: 1px solid #dc2626; padding: 6px 12px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer;">
|
||||
<i class="fas fa-ban"></i> Ban IP
|
||||
</button>
|
||||
</div>
|
||||
<button class="view-activity-btn" style="background: #5b5fcf; color: white; border-color: #5b5fcf;">View Activity</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
|
@ -715,7 +1010,7 @@
|
|||
<div ng-if="!loadingSSHLogs && sshLogs.length === 0" style="text-align: center; padding: 20px; color: #8893a7;">
|
||||
No recent SSH logs found.
|
||||
</div>
|
||||
<table class="activity-table" ng-if="!loadingSSHLogs && sshLogs.length > 0">
|
||||
<table class="activity-table records-table" ng-if="!loadingSSHLogs && sshLogs.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>TIMESTAMP</th>
|
||||
|
|
@ -732,7 +1027,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Top Process Tab -->
|
||||
<div id="top-process" class="tab-content" ng-hide="hideSystemCharts">
|
||||
<div id="top-process" class="tab-content">
|
||||
<div ng-if="loadingTopProcesses" style="text-align: center; padding: 20px; color: #8893a7;">
|
||||
Loading top processes...
|
||||
</div>
|
||||
|
|
@ -742,7 +1037,7 @@
|
|||
<div ng-if="!loadingTopProcesses && !errorTopProcesses && topProcesses.length === 0" style="text-align: center; padding: 20px; color: #8893a7;">
|
||||
No process information available.
|
||||
</div>
|
||||
<table class="activity-table" ng-if="!loadingTopProcesses && !errorTopProcesses && topProcesses.length > 0">
|
||||
<table class="activity-table records-table" ng-if="!loadingTopProcesses && !errorTopProcesses && topProcesses.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>PID</th>
|
||||
|
|
@ -765,21 +1060,21 @@
|
|||
</div>
|
||||
|
||||
<!-- Traffic Tab -->
|
||||
<div id="traffic" class="tab-content" ng-hide="hideSystemCharts">
|
||||
<div id="traffic" class="tab-content">
|
||||
<div class="chart-container">
|
||||
<canvas id="trafficChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disk IO Tab -->
|
||||
<div id="diskio" class="tab-content" ng-hide="hideSystemCharts">
|
||||
<div id="diskio" class="tab-content">
|
||||
<div class="chart-container">
|
||||
<canvas id="diskIOChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU Usage Tab -->
|
||||
<div id="cpu-usage" class="tab-content" ng-hide="hideSystemCharts">
|
||||
<div id="cpu-usage" class="tab-content">
|
||||
<div class="chart-container">
|
||||
<canvas id="cpuChart"></canvas>
|
||||
</div>
|
||||
|
|
@ -788,19 +1083,168 @@
|
|||
</div>
|
||||
|
||||
<!-- SSH Activity Modal -->
|
||||
<div ng-show="showSSHActivityModal" class="modal-backdrop show ng-cloak" ng-click="closeModalOnBackdrop($event)">
|
||||
<div class="modal-content">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px;">
|
||||
<div style="font-size: 1.2rem; font-weight: 800; color: #5b5fcf;">
|
||||
<div ng-if="showSSHActivityModal" class="modal-backdrop" ng-class="{'show': showSSHActivityModal}" ng-click="closeModalOnBackdrop($event)">
|
||||
<div class="modal-content" ng-click="$event.stopPropagation()">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
User Activity: <span style="color: #2f3640;">{$ sshActivityUser $}</span>
|
||||
<span ng-if="sshActivityIP" style="font-size: 0.9rem; color: #64748b; font-weight: 500; margin-left: 10px;">
|
||||
(<i class="fas fa-network-wired"></i> {$ sshActivityIP $})
|
||||
</span>
|
||||
</div>
|
||||
<button class="btn btn-sm" style="border: none; background: none; font-size: 1.5rem; cursor: pointer;" ng-click="closeSSHActivityModal()">×</button>
|
||||
<button class="modal-close-btn" ng-click="closeSSHActivityModal()" title="Close">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div ng-if="loadingSSHActivity" style="text-align: center; color: #8893a7; padding: 20px 0;">Loading activity...</div>
|
||||
<div ng-if="errorSSHActivity" style="color: #e53935; padding: 10px 0;">{$ errorSSHActivity $}</div>
|
||||
|
||||
<div ng-if="loadingSSHActivity" style="text-align: center; color: #8893a7; padding: 40px 20px;">
|
||||
<i class="fas fa-spinner fa-spin" style="font-size: 2rem; margin-bottom: 10px; display: block;"></i>
|
||||
<div>Loading activity...</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="errorSSHActivity" style="background: #fee2e2; border: 1px solid #fecaca; color: #991b1b; padding: 15px; border-radius: 8px; margin-bottom: 15px;">
|
||||
<i class="fas fa-exclamation-circle"></i> {$ errorSSHActivity $}
|
||||
</div>
|
||||
|
||||
<div ng-if="!loadingSSHActivity && !errorSSHActivity">
|
||||
<!-- Activity content will be displayed here -->
|
||||
<pre style="background: #f8f9fa; padding: 15px; border-radius: 8px;">{$ sshActivity | json $}</pre>
|
||||
<!-- Session Status Info -->
|
||||
<div style="background: #f8f9ff; border-left: 4px solid #5b5fcf; padding: 12px 16px; border-radius: 6px; margin-bottom: 20px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 8px;">
|
||||
<i class="fas fa-info-circle" style="color: #5b5fcf;"></i>
|
||||
<strong style="color: #2f3640;">Session Status:</strong>
|
||||
<span ng-if="isActiveSession && sshActivity.processes && sshActivity.processes.length > 0" style="color: #10b981; font-weight: 600;">
|
||||
<i class="fas fa-circle" style="font-size: 8px;"></i> ACTIVE - User is logged in with running processes
|
||||
</span>
|
||||
<span ng-if="isActiveSession && (!sshActivity.processes || sshActivity.processes.length === 0) && sshActivity.w && sshActivity.w.length > 0" style="color: #f59e0b; font-weight: 600;">
|
||||
<i class="fas fa-circle" style="font-size: 8px;"></i> IDLE - User is logged in but no processes running (shell waiting)
|
||||
</span>
|
||||
<span ng-if="isActiveSession && (!sshActivity.processes || sshActivity.processes.length === 0) && (!sshActivity.w || sshActivity.w.length === 0)" style="color: #ef4444; font-weight: 600;">
|
||||
<i class="fas fa-circle" style="font-size: 8px;"></i> INCONSISTENT - Marked as active but no processes or sessions found
|
||||
</span>
|
||||
<span ng-if="!isActiveSession" style="color: #ef4444; font-weight: 600;">
|
||||
<i class="fas fa-circle" style="font-size: 8px;"></i> INACTIVE - Session has ended
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #64748b; margin-left: 24px;">
|
||||
<div ng-if="sshActivityStatus">
|
||||
<strong>Login Status:</strong> {$ sshActivityStatus $}
|
||||
</div>
|
||||
<div ng-if="sshActivity.processes && sshActivity.processes.length > 0" style="margin-top: 4px;">
|
||||
<strong>Running Processes:</strong> {$ sshActivity.processes.length $} process(es) found
|
||||
</div>
|
||||
<div ng-if="sshActivity.w && sshActivity.w.length > 0" style="margin-top: 4px;">
|
||||
<strong>Active Sessions (w command):</strong> {$ sshActivity.w.length $} session(s) detected
|
||||
</div>
|
||||
<div ng-if="(!sshActivity.processes || sshActivity.processes.length === 0) && sshActivity.w && sshActivity.w.length > 0" style="margin-top: 4px; color: #f59e0b;">
|
||||
<em>⚠️ User is logged in (detected by 'w' command) but no processes found. This usually means the shell is idle, waiting for input.</em>
|
||||
</div>
|
||||
<div ng-if="(!sshActivity.processes || sshActivity.processes.length === 0) && (!sshActivity.w || sshActivity.w.length === 0)" style="margin-top: 4px; color: #ef4444;">
|
||||
<em>⚠️ No processes or active sessions found. The session may have just ended or the user logged out.</em>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Processes List -->
|
||||
<div ng-if="sshActivity.processes && sshActivity.processes.length > 0" class="process-list">
|
||||
<h4 style="font-size: 14px; font-weight: 600; color: #2f3640; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-microchip"></i> Running Processes ({$ sshActivity.processes.length $})
|
||||
</h4>
|
||||
<div ng-repeat="process in sshActivity.processes" class="process-item">
|
||||
<div class="process-info">
|
||||
<span class="process-pid">PID: {$ process.pid $}</span>
|
||||
<span ng-if="process.ppid" style="color: #94a3b8; font-size: 12px;">PPID: {$ process.ppid $}</span>
|
||||
<div class="process-cmd">
|
||||
<strong>Command:</strong> {$ process.cmd $}
|
||||
<span ng-if="process.tty" style="margin-left: 10px; color: #94a3b8;">TTY: {$ process.tty $}</span>
|
||||
<span ng-if="process.time" style="margin-left: 10px; color: #94a3b8;">Time: {$ process.time $}</span>
|
||||
</div>
|
||||
<div ng-if="process.cwd" style="font-size: 11px; color: #94a3b8; margin-top: 4px;">
|
||||
<i class="fas fa-folder"></i> {$ process.cwd $}
|
||||
</div>
|
||||
</div>
|
||||
<div class="process-actions">
|
||||
<button class="kill-process-btn"
|
||||
ng-click="killProcess(process.pid, sshActivityUser)"
|
||||
ng-disabled="killingProcess === process.pid"
|
||||
title="Force Kill Process">
|
||||
<i class="fas fa-skull" ng-if="killingProcess !== process.pid"></i>
|
||||
<i class="fas fa-spinner fa-spin" ng-if="killingProcess === process.pid"></i>
|
||||
<span ng-if="killingProcess !== process.pid">Kill</span>
|
||||
<span ng-if="killingProcess === process.pid">Killing...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Session Info (w command output) -->
|
||||
<div ng-if="sshActivity.w && sshActivity.w.length > 0" style="margin-top: 20px;">
|
||||
<h4 style="font-size: 14px; font-weight: 600; color: #2f3640; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-terminal"></i> Session Information
|
||||
</h4>
|
||||
<div style="background: white; border: 1px solid var(--border-color, #e8e9ff); border-radius: 8px; padding: 15px;">
|
||||
<div ng-repeat="session in sshActivity.w" style="font-family: 'Courier New', monospace; font-size: 12px; color: #2f3640; margin-bottom: 8px;">
|
||||
{$ session $}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Activity JSON (Collapsible) -->
|
||||
<div style="margin-top: 20px;">
|
||||
<button ng-click="showFullJSON = !showFullJSON"
|
||||
style="background: var(--bg-hover, #f8f9ff); border: 1px solid var(--border-color, #e8e9ff); color: #5b5fcf; padding: 8px 16px; border-radius: 6px; font-size: 12px; font-weight: 600; cursor: pointer; width: 100%;">
|
||||
<i class="fas" ng-class="showFullJSON ? 'fa-chevron-up' : 'fa-chevron-down'"></i>
|
||||
<span ng-if="!showFullJSON">Show Full Activity JSON</span>
|
||||
<span ng-if="showFullJSON">Hide Full Activity JSON</span>
|
||||
</button>
|
||||
<div ng-if="showFullJSON" class="activity-content">
|
||||
<pre class="activity-json">{$ sshActivity | json $}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="modal-actions">
|
||||
<div class="modal-actions-left">
|
||||
<!-- Only show Kill Session button for active sessions -->
|
||||
<button class="kill-session-btn"
|
||||
ng-click="killSSHSession(sshActivityUser, sshActivityTTY)"
|
||||
ng-disabled="killingSession || !isActiveSession || !sshActivity.processes || sshActivity.processes.length === 0"
|
||||
ng-if="isActiveSession && sshActivity.processes && sshActivity.processes.length > 0"
|
||||
title="Kill All Processes for This Active Session">
|
||||
<i class="fas fa-power-off" ng-if="!killingSession"></i>
|
||||
<i class="fas fa-spinner fa-spin" ng-if="killingSession"></i>
|
||||
<span ng-if="!killingSession">Kill Session</span>
|
||||
<span ng-if="killingSession">Killing...</span>
|
||||
</button>
|
||||
<div ng-if="!isActiveSession || (isActiveSession && (!sshActivity.processes || sshActivity.processes.length === 0))" style="color: #94a3b8; font-size: 13px; padding: 10px 20px; display: inline-flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span ng-if="!isActiveSession">Session is inactive - cannot kill an ended session</span>
|
||||
<span ng-if="isActiveSession && (!sshActivity.processes || sshActivity.processes.length === 0) && sshActivity.w && sshActivity.w.length > 0">User is logged in but idle (no processes to kill) - shell is waiting for input</span>
|
||||
<span ng-if="isActiveSession && (!sshActivity.processes || sshActivity.processes.length === 0) && (!sshActivity.w || sshActivity.w.length === 0)">No processes or sessions found - session may have just ended</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-actions-right">
|
||||
<!-- Always show Ban IP button if IP is available - show for all sessions -->
|
||||
<div ng-if="!sshActivityIP" style="color: #94a3b8; font-size: 13px; padding: 10px 20px; display: inline-flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span>IP address not available</span>
|
||||
</div>
|
||||
<button ng-click="blockIPAddress(sshActivityIP)"
|
||||
ng-disabled="blockingIP === sshActivityIP || !sshActivityIP"
|
||||
class="ban-ip-btn"
|
||||
ng-if="sshActivityIP && sshActivityIP.trim() !== ''"
|
||||
title="Ban this IP address permanently">
|
||||
<i class="fas fa-ban" ng-if="blockingIP !== sshActivityIP"></i>
|
||||
<i class="fas fa-spinner fa-spin" ng-if="blockingIP === sshActivityIP"></i>
|
||||
<span ng-if="blockingIP !== sshActivityIP">Ban IP Permanently</span>
|
||||
<span ng-if="blockingIP === sshActivityIP">Banning...</span>
|
||||
</button>
|
||||
<span ng-if="blockedIPs && blockedIPs[sshActivityIP] && sshActivityIP && sshActivityIP.trim() !== ''"
|
||||
style="display: inline-flex; align-items: center; gap: 6px; color: #10b981; font-size: 14px; font-weight: 600; padding: 10px 20px;">
|
||||
<i class="fas fa-check-circle"></i> Already Blocked
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -713,25 +713,57 @@ def getRecentSSHLogins(request):
|
|||
date_match = re.search(r'([A-Za-z]{3} [A-Za-z]{3} +\d+ [\d:]+)', line)
|
||||
date_str = date_match.group(1) if date_match else ''
|
||||
session_info = ''
|
||||
if '-' in line:
|
||||
# Session ended
|
||||
session_info = line.split('-')[-1].strip()
|
||||
elif 'still logged in' in line:
|
||||
is_active = False
|
||||
if 'still logged in' in line:
|
||||
session_info = 'still logged in'
|
||||
# GeoIP lookup (cache per request)
|
||||
is_active = True
|
||||
elif '-' in line:
|
||||
# Session ended - parse the end time and duration
|
||||
# Format: "Tue May 27 11:34 - 13:47 (02:13)" or "crash (00:40)"
|
||||
end_part = line.split('-')[-1].strip()
|
||||
# Check if it's a crash or normal logout
|
||||
if 'crash' in end_part.lower():
|
||||
# Extract crash duration if available
|
||||
crash_match = re.search(r'crash\s*\(([^)]+)\)', end_part, re.IGNORECASE)
|
||||
if crash_match:
|
||||
session_info = f"crash ({crash_match.group(1)})"
|
||||
else:
|
||||
session_info = 'crash'
|
||||
else:
|
||||
# Normal session end - try to extract duration
|
||||
duration_match = re.search(r'\(([^)]+)\)', end_part)
|
||||
if duration_match:
|
||||
session_info = f"ended ({duration_match.group(1)})"
|
||||
else:
|
||||
# Just show the end time
|
||||
time_match = re.search(r'([A-Za-z]{3}\s+[A-Za-z]{3}\s+\d+\s+[\d:]+)', end_part)
|
||||
if time_match:
|
||||
session_info = f"ended at {time_match.group(1)}"
|
||||
else:
|
||||
session_info = 'ended'
|
||||
is_active = False
|
||||
# GeoIP lookup (cache per request) - support both IPv4 and IPv6
|
||||
country = flag = ''
|
||||
if re.match(r'\d+\.\d+\.\d+\.\d+', ip) and ip != '127.0.0.1':
|
||||
# Check if IP is IPv4
|
||||
is_ipv4 = re.match(r'^\d+\.\d+\.\d+\.\d+$', ip)
|
||||
# Check if IP is IPv6 (simplified check)
|
||||
is_ipv6 = ':' in ip and not is_ipv4
|
||||
|
||||
if is_ipv4 and ip != '127.0.0.1':
|
||||
if ip in ip_cache:
|
||||
country, flag = ip_cache[ip]
|
||||
else:
|
||||
try:
|
||||
geo = requests.get(f'http://ip-api.com/json/{ip}', timeout=2).json()
|
||||
geo = requests.get(f'http://ip-api.com/json/{ip}', timeout=1).json()
|
||||
country = geo.get('countryCode', '')
|
||||
flag = f"https://flagcdn.com/24x18/{country.lower()}.png" if country else ''
|
||||
ip_cache[ip] = (country, flag)
|
||||
except Exception:
|
||||
country, flag = '', ''
|
||||
elif ip == '127.0.0.1':
|
||||
elif is_ipv6 and ip != '::1':
|
||||
# IPv6 - set flag to indicate IPv6 (GeoIP API may not support IPv6 well)
|
||||
country, flag = 'IPv6', ''
|
||||
elif ip == '127.0.0.1' or ip == '::1':
|
||||
country, flag = 'Local', ''
|
||||
logins.append({
|
||||
'user': user,
|
||||
|
|
@ -740,6 +772,7 @@ def getRecentSSHLogins(request):
|
|||
'flag': flag,
|
||||
'date': date_str,
|
||||
'session': session_info,
|
||||
'is_active': is_active,
|
||||
'raw': line
|
||||
})
|
||||
return HttpResponse(json.dumps({'logins': logins}), content_type='application/json')
|
||||
|
|
@ -1254,115 +1287,63 @@ def getSSHUserActivity(request):
|
|||
login_ip = data.get('ip', '')
|
||||
if not user:
|
||||
return HttpResponse(json.dumps({'error': 'Missing user'}), content_type='application/json', status=400)
|
||||
# Get processes for the user
|
||||
ps_cmd = f"ps -u {user} -o pid,ppid,tty,time,cmd --no-headers"
|
||||
try:
|
||||
ps_output = ProcessUtilities.outputExecutioner(ps_cmd)
|
||||
except Exception as e:
|
||||
ps_output = ''
|
||||
processes = []
|
||||
pid_map = {}
|
||||
if ps_output:
|
||||
for line in ps_output.strip().split('\n'):
|
||||
parts = line.split(None, 4)
|
||||
if len(parts) == 5:
|
||||
pid, ppid, tty_val, time_val, cmd = parts
|
||||
if tty and tty not in tty_val:
|
||||
continue
|
||||
# Try to get CWD
|
||||
cwd = ''
|
||||
try:
|
||||
cwd_path = f"/proc/{pid}/cwd"
|
||||
if os.path.islink(cwd_path):
|
||||
cwd = os.readlink(cwd_path)
|
||||
except Exception:
|
||||
cwd = ''
|
||||
proc = {
|
||||
'pid': pid,
|
||||
'ppid': ppid,
|
||||
'tty': tty_val,
|
||||
'time': time_val,
|
||||
'cmd': cmd,
|
||||
'cwd': cwd
|
||||
}
|
||||
processes.append(proc)
|
||||
pid_map[pid] = proc
|
||||
# Build process tree
|
||||
tree = []
|
||||
def build_tree(parent_pid, level=0):
|
||||
for proc in processes:
|
||||
if proc['ppid'] == parent_pid:
|
||||
proc_copy = proc.copy()
|
||||
proc_copy['level'] = level
|
||||
tree.append(proc_copy)
|
||||
build_tree(proc['pid'], level+1)
|
||||
build_tree('1', 0) # Start from init
|
||||
# Find main shell process for history
|
||||
shell_history = []
|
||||
try:
|
||||
try:
|
||||
website = Websites.objects.get(externalApp=user)
|
||||
shell_home = f'/home/{website.domain}'
|
||||
except Exception:
|
||||
shell_home = pwd.getpwnam(user).pw_dir
|
||||
except Exception:
|
||||
shell_home = f"/home/{user}"
|
||||
history_file = ''
|
||||
for shell in ['.bash_history', '.zsh_history']:
|
||||
path = os.path.join(shell_home, shell)
|
||||
if os.path.exists(path):
|
||||
history_file = path
|
||||
break
|
||||
if history_file:
|
||||
try:
|
||||
with open(history_file, 'r') as f:
|
||||
lines = f.readlines()
|
||||
shell_history = [l.strip() for l in lines[-10:]]
|
||||
except Exception:
|
||||
shell_history = []
|
||||
# Disk usage
|
||||
disk_usage = ''
|
||||
if os.path.exists(shell_home):
|
||||
try:
|
||||
du_out = ProcessUtilities.outputExecutioner(f'du -sh {shell_home}')
|
||||
disk_usage = du_out.strip().split('\t')[0] if du_out else ''
|
||||
except Exception:
|
||||
disk_usage = ''
|
||||
else:
|
||||
disk_usage = 'Home directory does not exist'
|
||||
# GeoIP details
|
||||
geoip = {}
|
||||
if login_ip and login_ip not in ['127.0.0.1', 'localhost']:
|
||||
try:
|
||||
geo = requests.get(f'http://ip-api.com/json/{login_ip}?fields=status,message,country,regionName,city,isp,org,as,query', timeout=2).json()
|
||||
if geo.get('status') == 'success':
|
||||
geoip = {
|
||||
'country': geo.get('country'),
|
||||
'region': geo.get('regionName'),
|
||||
'city': geo.get('city'),
|
||||
'isp': geo.get('isp'),
|
||||
'org': geo.get('org'),
|
||||
'as': geo.get('as'),
|
||||
'ip': geo.get('query')
|
||||
}
|
||||
except Exception:
|
||||
geoip = {}
|
||||
# Optionally, get 'w' output for more info
|
||||
w_cmd = f"w -h {user}"
|
||||
try:
|
||||
w_output = ProcessUtilities.outputExecutioner(w_cmd)
|
||||
except Exception as e:
|
||||
w_output = ''
|
||||
# Get 'w' output first (fastest, most important for session status)
|
||||
w_lines = []
|
||||
if w_output:
|
||||
for line in w_output.strip().split('\n'):
|
||||
w_lines.append(line)
|
||||
try:
|
||||
w_cmd = f"w -h {user} 2>/dev/null | head -10"
|
||||
w_output = ProcessUtilities.outputExecutioner(w_cmd)
|
||||
if w_output:
|
||||
for line in w_output.strip().split('\n'):
|
||||
if line.strip():
|
||||
w_lines.append(line)
|
||||
except Exception:
|
||||
w_lines = []
|
||||
|
||||
# Get processes for the user (limit to 50 for speed)
|
||||
# If TTY is specified, filter by TTY; otherwise get all user processes
|
||||
processes = []
|
||||
try:
|
||||
if tty:
|
||||
# Filter by specific TTY
|
||||
ps_cmd = f"ps -u {user} -o pid,ppid,tty,time,cmd --no-headers 2>/dev/null | grep '{tty}' | head -50"
|
||||
else:
|
||||
# Get all processes for user
|
||||
ps_cmd = f"ps -u {user} -o pid,ppid,tty,time,cmd --no-headers 2>/dev/null | head -50"
|
||||
ps_output = ProcessUtilities.outputExecutioner(ps_cmd)
|
||||
if ps_output:
|
||||
for line in ps_output.strip().split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split(None, 4)
|
||||
if len(parts) >= 5:
|
||||
pid, ppid, tty_val, time_val, cmd = parts[0], parts[1], parts[2], parts[3], parts[4]
|
||||
# Additional TTY check if tty was specified
|
||||
if tty and tty not in tty_val:
|
||||
continue
|
||||
# Skip CWD lookup for speed
|
||||
proc = {
|
||||
'pid': pid,
|
||||
'ppid': ppid,
|
||||
'tty': tty_val,
|
||||
'time': time_val,
|
||||
'cmd': cmd[:200] if len(cmd) > 200 else cmd, # Limit command length
|
||||
'cwd': '' # Skip for speed
|
||||
}
|
||||
processes.append(proc)
|
||||
except Exception:
|
||||
processes = []
|
||||
|
||||
# Skip slow operations for fast response:
|
||||
# - Process tree (can be computed client-side if needed)
|
||||
# - Shell history (not critical for initial load)
|
||||
# - Disk usage (not critical for initial load)
|
||||
# - GeoIP (can be fetched async later if needed)
|
||||
return HttpResponse(json.dumps({
|
||||
'processes': processes,
|
||||
'process_tree': tree,
|
||||
'shell_history': shell_history,
|
||||
'disk_usage': disk_usage,
|
||||
'geoip': geoip,
|
||||
'process_tree': [], # Empty for speed
|
||||
'shell_history': [], # Empty for speed
|
||||
'disk_usage': '', # Empty for speed
|
||||
'geoip': {}, # Empty for speed
|
||||
'w': w_lines
|
||||
}), content_type='application/json')
|
||||
except Exception as e:
|
||||
|
|
|
|||
Loading…
Reference in New Issue