Added more dashboard cards

Fixed permissions roles creation bug
On initial deployment, made it so the agent being populated will be set as default and current
Fixed host detail to include package numbers
Added ability to add full name
- fixed loads of other bugs caused by camelcase to snake_Case migration
This commit is contained in:
Muhammad Ibrahim
2025-09-22 21:31:14 +01:00
parent 9b76d9f81a
commit f23f075e41
19 changed files with 619 additions and 141 deletions

View File

@@ -151,8 +151,13 @@ router.post('/admin/users', authenticateToken, requireManageUsers, [
body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
body('email').isEmail().withMessage('Valid email is required'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters'),
body('first_name').optional().isLength({ min: 1 }).withMessage('First name must be at least 1 character'),
body('last_name').optional().isLength({ min: 1 }).withMessage('Last name must be at least 1 character'),
body('role').optional().custom(async (value) => {
if (!value) return true; // Optional field
// Allow built-in roles even if not in role_permissions table yet
const builtInRoles = ['admin', 'user'];
if (builtInRoles.includes(value)) return true;
const rolePermissions = await prisma.role_permissions.findUnique({
where: { role: value }
});
@@ -168,7 +173,7 @@ router.post('/admin/users', authenticateToken, requireManageUsers, [
return res.status(400).json({ errors: errors.array() });
}
const { username, email, password, role = 'user' } = req.body;
const { username, email, password, first_name, last_name, role = 'user' } = req.body;
// Check if user already exists
const existingUser = await prisma.users.findFirst({
@@ -190,15 +195,21 @@ router.post('/admin/users', authenticateToken, requireManageUsers, [
// Create user
const user = await prisma.users.create({
data: {
id: uuidv4(),
username,
email,
password_hash: passwordHash,
role
first_name: first_name || null,
last_name: last_name || null,
role,
updated_at: new Date()
},
select: {
id: true,
username: true,
email: true,
first_name: true,
last_name: true,
role: true,
is_active: true,
created_at: true
@@ -542,6 +553,8 @@ router.post('/login', [
id: true,
username: true,
email: true,
first_name: true,
last_name: true,
password_hash: true,
role: true,
is_active: true,
@@ -690,6 +703,8 @@ router.post('/verify-tfa', [
id: user.id,
username: user.username,
email: user.email,
first_name: user.first_name,
last_name: user.last_name,
role: user.role
}
});
@@ -714,7 +729,9 @@ router.get('/profile', authenticateToken, async (req, res) => {
// Update user profile
router.put('/profile', authenticateToken, [
body('username').optional().isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
body('email').optional().isEmail().withMessage('Valid email is required')
body('email').optional().isEmail().withMessage('Valid email is required'),
body('first_name').optional().isLength({ min: 1 }).withMessage('First name must be at least 1 character'),
body('last_name').optional().isLength({ min: 1 }).withMessage('Last name must be at least 1 character')
], async (req, res) => {
try {
const errors = validationResult(req);
@@ -722,11 +739,13 @@ router.put('/profile', authenticateToken, [
return res.status(400).json({ errors: errors.array() });
}
const { username, email } = req.body;
const { username, email, first_name, last_name } = req.body;
const updateData = {};
if (username) updateData.username = username;
if (email) updateData.email = email;
if (first_name !== undefined) updateData.first_name = first_name || null;
if (last_name !== undefined) updateData.last_name = last_name || null;
// Check if username/email already exists (excluding current user)
if (username || email) {
@@ -756,6 +775,8 @@ router.put('/profile', authenticateToken, [
id: true,
username: true,
email: true,
first_name: true,
last_name: true,
role: true,
is_active: true,
last_login: true,

View File

@@ -74,13 +74,17 @@ router.get('/defaults', authenticateToken, async (req, res) => {
{ cardId: 'hostsNeedingUpdates', title: 'Needs Updating', icon: 'AlertTriangle', enabled: true, order: 1 },
{ cardId: 'totalOutdatedPackages', title: 'Outdated Packages', icon: 'Package', enabled: true, order: 2 },
{ cardId: 'securityUpdates', title: 'Security Updates', icon: 'Shield', enabled: true, order: 3 },
{ cardId: 'erroredHosts', title: 'Errored Hosts', icon: 'AlertTriangle', enabled: true, order: 4 },
{ cardId: 'offlineHosts', title: 'Offline/Stale Hosts', icon: 'WifiOff', enabled: false, order: 5 },
{ cardId: 'osDistribution', title: 'OS Distribution', icon: 'BarChart3', enabled: true, order: 6 },
{ cardId: 'osDistributionBar', title: 'OS Distribution (Bar)', icon: 'BarChart3', enabled: false, order: 7 },
{ cardId: 'updateStatus', title: 'Update Status', icon: 'BarChart3', enabled: true, order: 8 },
{ cardId: 'packagePriority', title: 'Package Priority', icon: 'BarChart3', enabled: true, order: 9 },
{ cardId: 'quickStats', title: 'Quick Stats', icon: 'TrendingUp', enabled: true, order: 10 }
{ cardId: 'upToDateHosts', title: 'Up to date', icon: 'CheckCircle', enabled: true, order: 4 },
{ cardId: 'totalHostGroups', title: 'Host Groups', icon: 'Folder', enabled: false, order: 5 },
{ cardId: 'totalUsers', title: 'Users', icon: 'Users', enabled: false, order: 6 },
{ cardId: 'totalRepos', title: 'Repositories', icon: 'GitBranch', enabled: false, order: 7 },
{ cardId: 'osDistribution', title: 'OS Distribution', icon: 'BarChart3', enabled: true, order: 8 },
{ cardId: 'osDistributionBar', title: 'OS Distribution (Bar)', icon: 'BarChart3', enabled: false, order: 9 },
{ cardId: 'updateStatus', title: 'Update Status', icon: 'BarChart3', enabled: true, order: 10 },
{ cardId: 'packagePriority', title: 'Package Priority', icon: 'BarChart3', enabled: true, order: 11 },
{ cardId: 'quickStats', title: 'Quick Stats', icon: 'TrendingUp', enabled: true, order: 12 },
{ cardId: 'recentUsers', title: 'Recent Users Logged in', icon: 'Users', enabled: true, order: 13 },
{ cardId: 'recentCollection', title: 'Recent Collection', icon: 'Server', enabled: true, order: 14 }
];
res.json(defaultCards);

View File

@@ -5,7 +5,8 @@ const { authenticateToken } = require('../middleware/auth');
const {
requireViewDashboard,
requireViewHosts,
requireViewPackages
requireViewPackages,
requireViewUsers
} = require('../middleware/permissions');
const router = express.Router();
@@ -33,6 +34,9 @@ router.get('/stats', authenticateToken, requireViewDashboard, async (req, res) =
erroredHosts,
securityUpdates,
offlineHosts,
totalHostGroups,
totalUsers,
totalRepos,
osDistribution,
updateTrends
] = await Promise.all([
@@ -83,6 +87,15 @@ router.get('/stats', authenticateToken, requireViewDashboard, async (req, res) =
}
}),
// Total host groups count
prisma.host_groups.count(),
// Total users count
prisma.users.count(),
// Total repositories count
prisma.repositories.count(),
// OS distribution for pie chart
prisma.hosts.groupBy({
by: ['os_type'],
@@ -133,10 +146,14 @@ router.get('/stats', authenticateToken, requireViewDashboard, async (req, res) =
cards: {
totalHosts,
hostsNeedingUpdates,
upToDateHosts: Math.max(totalHosts - hostsNeedingUpdates, 0),
totalOutdatedPackages,
erroredHosts,
securityUpdates,
offlineHosts
offlineHosts,
totalHostGroups,
totalUsers,
totalRepos
},
charts: {
osDistribution: osDistributionFormatted,
@@ -338,9 +355,9 @@ router.get('/hosts/:hostId', authenticateToken, requireViewHosts, async (req, re
const hostWithStats = {
...host,
stats: {
totalPackages: host.host_packages.length,
outdatedPackages: host.host_packages.filter(hp => hp.needs_update).length,
securityUpdates: host.host_packages.filter(hp => hp.needs_update && hp.is_security_update).length
total_packages: host.host_packages.length,
outdated_packages: host.host_packages.filter(hp => hp.needs_update).length,
security_updates: host.host_packages.filter(hp => hp.needs_update && hp.is_security_update).length
}
};
@@ -351,4 +368,59 @@ router.get('/hosts/:hostId', authenticateToken, requireViewHosts, async (req, re
}
});
// Get recent users ordered by last_login desc
router.get('/recent-users', authenticateToken, requireViewUsers, async (req, res) => {
try {
const users = await prisma.users.findMany({
where: {
last_login: {
not: null
}
},
select: {
id: true,
username: true,
email: true,
role: true,
last_login: true,
created_at: true
},
orderBy: [
{ last_login: 'desc' },
{ created_at: 'desc' }
],
take: 5
});
res.json(users);
} catch (error) {
console.error('Error fetching recent users:', error);
res.status(500).json({ error: 'Failed to fetch recent users' });
}
});
// Get recent hosts that have sent data (ordered by last_update desc)
router.get('/recent-collection', authenticateToken, requireViewHosts, async (req, res) => {
try {
const hosts = await prisma.hosts.findMany({
select: {
id: true,
friendly_name: true,
hostname: true,
last_update: true,
status: true
},
orderBy: {
last_update: 'desc'
},
take: 5
});
res.json(hosts);
} catch (error) {
console.error('Error fetching recent collection:', error);
res.status(500).json({ error: 'Failed to fetch recent collection' });
}
});
module.exports = router;

View File

@@ -1,6 +1,7 @@
const express = require('express');
const { body, validationResult } = require('express-validator');
const { PrismaClient } = require('@prisma/client');
const { randomUUID } = require('crypto');
const { authenticateToken } = require('../middleware/auth');
const { requireManageHosts } = require('../middleware/permissions');
@@ -41,13 +42,13 @@ router.get('/:id', authenticateToken, async (req, res) => {
hosts: {
select: {
id: true,
friendlyName: true,
friendly_name: true,
hostname: true,
ip: true,
osType: true,
osVersion: true,
os_type: true,
os_version: true,
status: true,
lastUpdate: true
last_update: true
}
}
}
@@ -89,9 +90,11 @@ router.post('/', authenticateToken, requireManageHosts, [
const hostGroup = await prisma.host_groups.create({
data: {
id: randomUUID(),
name,
description: description || null,
color: color || '#3B82F6'
color: color || '#3B82F6',
updated_at: new Date()
}
});
@@ -143,7 +146,8 @@ router.put('/:id', authenticateToken, requireManageHosts, [
data: {
name,
description: description || null,
color: color || '#3B82F6'
color: color || '#3B82F6',
updated_at: new Date()
}
});
@@ -199,20 +203,20 @@ router.get('/:id/hosts', authenticateToken, async (req, res) => {
const { id } = req.params;
const hosts = await prisma.hosts.findMany({
where: { hostGroupId: id },
where: { host_group_id: id },
select: {
id: true,
friendlyName: true,
friendly_name: true,
ip: true,
osType: true,
osVersion: true,
os_type: true,
os_version: true,
architecture: true,
status: true,
lastUpdate: true,
createdAt: true
last_update: true,
created_at: true
},
orderBy: {
friendlyName: 'asc'
friendly_name: 'asc'
}
});

View File

@@ -1,13 +1,13 @@
const express = require('express');
const { PrismaClient } = require('@prisma/client');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const { requireManageSettings } = require('../middleware/permissions');
const { requireManageSettings, requireManageUsers } = require('../middleware/permissions');
const router = express.Router();
const prisma = new PrismaClient();
// Get all role permissions
router.get('/roles', authenticateToken, requireManageSettings, async (req, res) => {
// Get all role permissions (allow users who can manage users to view roles)
router.get('/roles', authenticateToken, requireManageUsers, async (req, res) => {
try {
const permissions = await prisma.role_permissions.findMany({
orderBy: {

View File

@@ -133,6 +133,18 @@ async function checkAndImportAgentVersion() {
version: localVersion,
release_notes: `Auto-imported on startup (${new Date().toISOString()})`,
script_content: scriptContent,
is_default: true,
is_current: true,
updated_at: new Date()
}
});
// Update all other versions to not be default or current
await prisma.agent_versions.updateMany({
where: {
version: { not: localVersion }
},
data: {
is_default: false,
is_current: false,
updated_at: new Date()