mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-03 21:43:33 +00:00
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:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "settings" ADD COLUMN "signup_enabled" BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user