Created toggle for enable / disable user signup flow with user role

Fixed numbers mismatching in host cards
Fixed issues with the settings file
Fixed layouts on hosts/packages/repos
Added ability to delete multiple hosts at once
Fixed Dark mode styling in areas
Removed console debugging messages
Done some other stuff ...
This commit is contained in:
Muhammad Ibrahim
2025-09-22 01:01:50 +01:00
parent a268f6b8f1
commit 797be20c45
29 changed files with 940 additions and 4015 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "settings" ADD COLUMN "signup_enabled" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -168,6 +168,7 @@ model settings {
last_update_check DateTime?
latest_version String?
update_available Boolean @default(false)
signup_enabled Boolean @default(false)
}
model update_history {

View File

@@ -34,7 +34,7 @@ router.get('/check-admin-users', async (req, res) => {
router.post('/setup-admin', [
body('username').isLength({ min: 1 }).withMessage('Username is required'),
body('email').isEmail().withMessage('Valid email is required'),
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters for security')
], async (req, res) => {
try {
const errors = validationResult(req);
@@ -425,6 +425,17 @@ router.post('/admin/users/:userId/reset-password', authenticateToken, requireMan
}
});
// Check if signup is enabled (public endpoint)
router.get('/signup-enabled', async (req, res) => {
try {
const settings = await prisma.settings.findFirst();
res.json({ signupEnabled: settings?.signup_enabled || false });
} catch (error) {
console.error('Error checking signup status:', error);
res.status(500).json({ error: 'Failed to check signup status' });
}
});
// Public signup endpoint
router.post('/signup', [
body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
@@ -432,6 +443,12 @@ router.post('/signup', [
body('password').isLength({ min: 6 }).withMessage('Password must be at least 6 characters')
], async (req, res) => {
try {
// Check if signup is enabled
const settings = await prisma.settings.findFirst();
if (!settings?.signup_enabled) {
return res.status(403).json({ error: 'User signup is currently disabled' });
}
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });

View File

@@ -36,15 +36,12 @@ router.get('/stats', authenticateToken, requireViewDashboard, async (req, res) =
osDistribution,
updateTrends
] = await Promise.all([
// Total hosts count
prisma.hosts.count({
where: { status: 'active' }
}),
// Total hosts count (all hosts regardless of status)
prisma.hosts.count(),
// Hosts needing updates (distinct hosts with packages needing updates)
prisma.hosts.count({
where: {
status: 'active',
host_packages: {
some: {
needs_update: true

View File

@@ -755,7 +755,15 @@ router.get('/admin/list', authenticateToken, requireManageHosts, async (req, res
api_id: true,
agent_version: true,
auto_update: true,
created_at: true
created_at: true,
host_group_id: true,
host_groups: {
select: {
id: true,
name: true,
color: true
}
}
},
orderBy: { created_at: 'desc' }
});
@@ -767,20 +775,124 @@ router.get('/admin/list', authenticateToken, requireManageHosts, async (req, res
}
});
// Admin endpoint to delete multiple hosts
router.delete('/bulk', authenticateToken, requireManageHosts, [
body('hostIds').isArray({ min: 1 }).withMessage('At least one host ID is required'),
body('hostIds.*').isLength({ min: 1 }).withMessage('Each host ID must be provided')
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { hostIds } = req.body;
// Verify all hosts exist before deletion
const existingHosts = await prisma.hosts.findMany({
where: { id: { in: hostIds } },
select: { id: true, friendly_name: true }
});
if (existingHosts.length !== hostIds.length) {
const foundIds = existingHosts.map(h => h.id);
const missingIds = hostIds.filter(id => !foundIds.includes(id));
return res.status(404).json({
error: 'Some hosts not found',
missingIds
});
}
// Delete all hosts (cascade will handle related data)
const deleteResult = await prisma.hosts.deleteMany({
where: { id: { in: hostIds } }
});
// Check if all hosts were actually deleted
if (deleteResult.count !== hostIds.length) {
console.warn(`Expected to delete ${hostIds.length} hosts, but only deleted ${deleteResult.count}`);
}
res.json({
message: `${deleteResult.count} host${deleteResult.count !== 1 ? 's' : ''} deleted successfully`,
deletedCount: deleteResult.count,
requestedCount: hostIds.length,
deletedHosts: existingHosts.map(h => ({ id: h.id, friendly_name: h.friendly_name }))
});
} catch (error) {
console.error('Bulk host deletion error:', error);
// Handle specific Prisma errors
if (error.code === 'P2025') {
return res.status(404).json({
error: 'Some hosts were not found or already deleted',
details: 'The hosts may have been deleted by another process or do not exist'
});
}
if (error.code === 'P2003') {
return res.status(400).json({
error: 'Cannot delete hosts due to foreign key constraints',
details: 'Some hosts have related data that prevents deletion'
});
}
res.status(500).json({
error: 'Failed to delete hosts',
details: error.message || 'An unexpected error occurred'
});
}
});
// Admin endpoint to delete host
router.delete('/:hostId', authenticateToken, requireManageHosts, async (req, res) => {
try {
const { hostId } = req.params;
// Check if host exists first
const existingHost = await prisma.hosts.findUnique({
where: { id: hostId },
select: { id: true, friendly_name: true }
});
if (!existingHost) {
return res.status(404).json({
error: 'Host not found',
details: 'The host may have been deleted or does not exist'
});
}
// Delete host and all related data (cascade)
await prisma.hosts.delete({
where: { id: hostId }
});
res.json({ message: 'Host deleted successfully' });
res.json({
message: 'Host deleted successfully',
deletedHost: { id: existingHost.id, friendly_name: existingHost.friendly_name }
});
} catch (error) {
console.error('Host deletion error:', error);
res.status(500).json({ error: 'Failed to delete host' });
// Handle specific Prisma errors
if (error.code === 'P2025') {
return res.status(404).json({
error: 'Host not found',
details: 'The host may have been deleted or does not exist'
});
}
if (error.code === 'P2003') {
return res.status(400).json({
error: 'Cannot delete host due to foreign key constraints',
details: 'The host has related data that prevents deletion'
});
}
res.status(500).json({
error: 'Failed to delete host',
details: error.message || 'An unexpected error occurred'
});
}
});

View File

@@ -112,11 +112,19 @@ router.get('/', async (req, res) => {
return {
...pkg,
affectedHostsCount: pkg._count.hostPackages,
affectedHosts: affectedHosts.map(hp => ({
hostId: hp.host.id,
friendlyName: hp.host.friendly_name,
osType: hp.host.os_type,
currentVersion: hp.current_version,
availableVersion: hp.available_version,
isSecurityUpdate: hp.is_security_update
})),
stats: {
totalInstalls: pkg._count.hostPackages,
updatesNeeded: updatesCount,
securityUpdates: securityCount,
affectedHosts: affectedHosts.map(hp => hp.host)
securityUpdates: securityCount
}
};
})

View File

@@ -47,16 +47,16 @@ router.put('/roles/:role', authenticateToken, requireManageSettings, async (req,
try {
const { role } = req.params;
const {
canViewDashboard,
canViewHosts,
canManageHosts,
canViewPackages,
canManagePackages,
canViewUsers,
canManageUsers,
canViewReports,
canExportData,
canManageSettings
can_view_dashboard,
can_view_hosts,
can_manage_hosts,
can_view_packages,
can_manage_packages,
can_view_users,
can_manage_users,
can_view_reports,
can_export_data,
can_manage_settings
} = req.body;
// Prevent modifying admin role permissions (admin should always have full access)
@@ -67,31 +67,31 @@ router.put('/roles/:role', authenticateToken, requireManageSettings, async (req,
const permissions = await prisma.role_permissions.upsert({
where: { role },
update: {
can_view_dashboard: canViewDashboard,
can_view_hosts: canViewHosts,
can_manage_hosts: canManageHosts,
can_view_packages: canViewPackages,
can_manage_packages: canManagePackages,
can_view_users: canViewUsers,
can_manage_users: canManageUsers,
can_view_reports: canViewReports,
can_export_data: canExportData,
can_manage_settings: canManageSettings,
can_view_dashboard: can_view_dashboard,
can_view_hosts: can_view_hosts,
can_manage_hosts: can_manage_hosts,
can_view_packages: can_view_packages,
can_manage_packages: can_manage_packages,
can_view_users: can_view_users,
can_manage_users: can_manage_users,
can_view_reports: can_view_reports,
can_export_data: can_export_data,
can_manage_settings: can_manage_settings,
updated_at: new Date()
},
create: {
id: require('uuid').v4(),
role,
can_view_dashboard: canViewDashboard,
can_view_hosts: canViewHosts,
can_manage_hosts: canManageHosts,
can_view_packages: canViewPackages,
can_manage_packages: canManagePackages,
can_view_users: canViewUsers,
can_manage_users: canManageUsers,
can_view_reports: canViewReports,
can_export_data: canExportData,
can_manage_settings: canManageSettings,
can_view_dashboard: can_view_dashboard,
can_view_hosts: can_view_hosts,
can_manage_hosts: can_manage_hosts,
can_view_packages: can_view_packages,
can_manage_packages: can_manage_packages,
can_view_users: can_view_users,
can_manage_users: can_manage_users,
can_view_reports: can_view_reports,
can_export_data: can_export_data,
can_manage_settings: can_manage_settings,
updated_at: new Date()
}
});

View File

@@ -40,7 +40,9 @@ async function triggerCrontabUpdates() {
const http = require('http');
const https = require('https');
const settings = await prisma.settings.findFirst();
const settings = await prisma.settings.findFirst({
orderBy: { updated_at: 'desc' }
});
const serverUrl = settings?.server_url || process.env.SERVER_URL || 'http://localhost:3001';
const url = new URL(`${serverUrl}/api/v1/hosts/ping`);
const isHttps = url.protocol === 'https:';
@@ -92,7 +94,9 @@ async function triggerCrontabUpdates() {
// Get current settings
router.get('/', authenticateToken, requireManageSettings, async (req, res) => {
try {
let settings = await prisma.settings.findFirst();
let settings = await prisma.settings.findFirst({
orderBy: { updated_at: 'desc' }
});
// If no settings exist, create default settings
if (!settings) {
@@ -106,12 +110,12 @@ router.get('/', authenticateToken, requireManageSettings, async (req, res) => {
frontend_url: 'http://localhost:3000',
update_interval: 60,
auto_update: false,
signup_enabled: false,
updated_at: new Date()
}
});
}
console.log('Returning settings:', settings);
res.json(settings);
} catch (error) {
console.error('Settings fetch error:', error);
@@ -127,6 +131,7 @@ router.put('/', authenticateToken, requireManageSettings, [
body('frontendUrl').isLength({ min: 1 }).withMessage('Frontend URL is required'),
body('updateInterval').isInt({ min: 5, max: 1440 }).withMessage('Update interval must be between 5 and 1440 minutes'),
body('autoUpdate').isBoolean().withMessage('Auto update must be a boolean'),
body('signupEnabled').isBoolean().withMessage('Signup enabled must be a boolean'),
body('githubRepoUrl').optional().isLength({ min: 1 }).withMessage('GitHub repo URL must be a non-empty string'),
body('repositoryType').optional().isIn(['public', 'private']).withMessage('Repository type must be public or private'),
body('sshKeyPath').optional().custom((value) => {
@@ -140,36 +145,23 @@ router.put('/', authenticateToken, requireManageSettings, [
})
], async (req, res) => {
try {
console.log('Settings update request body:', req.body);
const errors = validationResult(req);
if (!errors.isEmpty()) {
console.log('Validation errors:', errors.array());
return res.status(400).json({ errors: errors.array() });
}
const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, repositoryType, sshKeyPath } = req.body;
console.log('Extracted values:', { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, githubRepoUrl, repositoryType, sshKeyPath });
console.log('GitHub repo URL received:', githubRepoUrl, 'Type:', typeof githubRepoUrl);
const { serverProtocol, serverHost, serverPort, frontendUrl, updateInterval, autoUpdate, signupEnabled, githubRepoUrl, repositoryType, sshKeyPath } = req.body;
// Construct server URL from components
const serverUrl = `${serverProtocol}://${serverHost}:${serverPort}`;
let settings = await prisma.settings.findFirst();
let settings = await prisma.settings.findFirst({
orderBy: { updated_at: 'desc' }
});
if (settings) {
// Update existing settings
console.log('Updating existing settings with data:', {
serverUrl,
serverProtocol,
serverHost,
serverPort,
frontendUrl,
updateInterval: updateInterval || 60,
autoUpdate: autoUpdate || false,
githubRepoUrl: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git',
repositoryType: repositoryType || 'public'
});
console.log('Final githubRepoUrl value being saved:', githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git');
const oldUpdateInterval = settings.update_interval;
settings = await prisma.settings.update({
@@ -182,13 +174,13 @@ router.put('/', authenticateToken, requireManageSettings, [
frontend_url: frontendUrl,
update_interval: updateInterval || 60,
auto_update: autoUpdate || false,
signup_enabled: signupEnabled || false,
github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git',
repository_type: repositoryType || 'public',
ssh_key_path: sshKeyPath || null,
updated_at: new Date()
}
});
console.log('Settings updated successfully:', settings);
// If update interval changed, trigger crontab updates on all hosts with auto-update enabled
if (oldUpdateInterval !== (updateInterval || 60)) {
@@ -207,6 +199,7 @@ router.put('/', authenticateToken, requireManageSettings, [
frontend_url: frontendUrl,
update_interval: updateInterval || 60,
auto_update: autoUpdate || false,
signup_enabled: signupEnabled || false,
github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git',
repository_type: repositoryType || 'public',
ssh_key_path: sshKeyPath || null,
@@ -228,7 +221,9 @@ router.put('/', authenticateToken, requireManageSettings, [
// Get server URL for public use (used by installation scripts)
router.get('/server-url', async (req, res) => {
try {
const settings = await prisma.settings.findFirst();
const settings = await prisma.settings.findFirst({
orderBy: { updated_at: 'desc' }
});
if (!settings) {
return res.json({ server_url: 'http://localhost:3001' });
@@ -244,7 +239,9 @@ router.get('/server-url', async (req, res) => {
// Get update interval policy for agents (public endpoint)
router.get('/update-interval', async (req, res) => {
try {
const settings = await prisma.settings.findFirst();
const settings = await prisma.settings.findFirst({
orderBy: { updated_at: 'desc' }
});
if (!settings) {
return res.json({ updateInterval: 60 });
@@ -263,7 +260,9 @@ router.get('/update-interval', async (req, res) => {
// Get auto-update policy for agents (public endpoint)
router.get('/auto-update', async (req, res) => {
try {
const settings = await prisma.settings.findFirst();
const settings = await prisma.settings.findFirst({
orderBy: { updated_at: 'desc' }
});
if (!settings) {
return res.json({ autoUpdate: false });