Fixed permissions issues

Created default user role
modified server.js to check if roles of admin/user is present
modified server.js to check dashboard cards
set up default dashboard cards to show
This commit is contained in:
Muhammad Ibrahim
2025-09-24 01:56:02 +01:00
parent db0ba201a4
commit 3a0b564a6f
16 changed files with 797 additions and 77 deletions

View File

@@ -1,3 +0,0 @@
Join my discord for Instructions, support and feedback :
https://discord.gg/S7RXUHwg

View File

@@ -20,3 +20,6 @@ AGENT_RATE_LIMIT_MAX=1000
# Logging # Logging
LOG_LEVEL=info LOG_LEVEL=info
ENABLE_LOGGING=true ENABLE_LOGGING=true
# User Registration
DEFAULT_USER_ROLE=user

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "settings" ADD COLUMN "default_user_role" TEXT NOT NULL DEFAULT 'user';

View File

@@ -0,0 +1,65 @@
-- Initialize default dashboard preferences for all existing users
-- This migration ensures that all users have proper role-based dashboard preferences
-- Function to create default dashboard preferences for a user
CREATE OR REPLACE FUNCTION init_user_dashboard_preferences(user_id TEXT, user_role TEXT)
RETURNS VOID AS $$
DECLARE
pref_record RECORD;
BEGIN
-- Delete any existing preferences for this user
DELETE FROM dashboard_preferences WHERE dashboard_preferences.user_id = init_user_dashboard_preferences.user_id;
-- Insert role-based preferences
IF user_role = 'admin' THEN
-- Admin gets full access to all cards (iby's preferred layout)
INSERT INTO dashboard_preferences (id, user_id, card_id, enabled, "order", created_at, updated_at)
VALUES
(gen_random_uuid(), user_id, 'totalHosts', true, 0, NOW(), NOW()),
(gen_random_uuid(), user_id, 'hostsNeedingUpdates', true, 1, NOW(), NOW()),
(gen_random_uuid(), user_id, 'totalOutdatedPackages', true, 2, NOW(), NOW()),
(gen_random_uuid(), user_id, 'securityUpdates', true, 3, NOW(), NOW()),
(gen_random_uuid(), user_id, 'totalHostGroups', true, 4, NOW(), NOW()),
(gen_random_uuid(), user_id, 'upToDateHosts', true, 5, NOW(), NOW()),
(gen_random_uuid(), user_id, 'totalRepos', true, 6, NOW(), NOW()),
(gen_random_uuid(), user_id, 'totalUsers', true, 7, NOW(), NOW()),
(gen_random_uuid(), user_id, 'osDistribution', true, 8, NOW(), NOW()),
(gen_random_uuid(), user_id, 'osDistributionBar', true, 9, NOW(), NOW()),
(gen_random_uuid(), user_id, 'recentCollection', true, 10, NOW(), NOW()),
(gen_random_uuid(), user_id, 'updateStatus', true, 11, NOW(), NOW()),
(gen_random_uuid(), user_id, 'packagePriority', true, 12, NOW(), NOW()),
(gen_random_uuid(), user_id, 'recentUsers', true, 13, NOW(), NOW()),
(gen_random_uuid(), user_id, 'quickStats', true, 14, NOW(), NOW());
ELSE
-- Regular users get comprehensive layout but without user management cards
INSERT INTO dashboard_preferences (id, user_id, card_id, enabled, "order", created_at, updated_at)
VALUES
(gen_random_uuid(), user_id, 'totalHosts', true, 0, NOW(), NOW()),
(gen_random_uuid(), user_id, 'hostsNeedingUpdates', true, 1, NOW(), NOW()),
(gen_random_uuid(), user_id, 'totalOutdatedPackages', true, 2, NOW(), NOW()),
(gen_random_uuid(), user_id, 'securityUpdates', true, 3, NOW(), NOW()),
(gen_random_uuid(), user_id, 'totalHostGroups', true, 4, NOW(), NOW()),
(gen_random_uuid(), user_id, 'upToDateHosts', true, 5, NOW(), NOW()),
(gen_random_uuid(), user_id, 'totalRepos', true, 6, NOW(), NOW()),
(gen_random_uuid(), user_id, 'osDistribution', true, 7, NOW(), NOW()),
(gen_random_uuid(), user_id, 'osDistributionBar', true, 8, NOW(), NOW()),
(gen_random_uuid(), user_id, 'recentCollection', true, 9, NOW(), NOW()),
(gen_random_uuid(), user_id, 'updateStatus', true, 10, NOW(), NOW()),
(gen_random_uuid(), user_id, 'packagePriority', true, 11, NOW(), NOW()),
(gen_random_uuid(), user_id, 'quickStats', true, 12, NOW(), NOW());
END IF;
END;
$$ LANGUAGE plpgsql;
-- Apply default preferences to all existing users
DO $$
DECLARE
user_record RECORD;
BEGIN
FOR user_record IN SELECT id, role FROM users LOOP
PERFORM init_user_dashboard_preferences(user_record.id, user_record.role);
END LOOP;
END $$;
-- Drop the temporary function
DROP FUNCTION init_user_dashboard_preferences(TEXT, TEXT);

View File

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

View File

@@ -6,6 +6,7 @@ const { body, validationResult } = require('express-validator');
const { authenticateToken, requireAdmin } = require('../middleware/auth'); const { authenticateToken, requireAdmin } = require('../middleware/auth');
const { requireViewUsers, requireManageUsers } = require('../middleware/permissions'); const { requireViewUsers, requireManageUsers } = require('../middleware/permissions');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const { createDefaultDashboardPreferences } = require('./dashboardPreferencesRoutes');
const router = express.Router(); const router = express.Router();
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@@ -32,6 +33,8 @@ router.get('/check-admin-users', async (req, res) => {
// Create first admin user (for first-time setup) // Create first admin user (for first-time setup)
router.post('/setup-admin', [ router.post('/setup-admin', [
body('firstName').isLength({ min: 1 }).withMessage('First name is required'),
body('lastName').isLength({ min: 1 }).withMessage('Last name is required'),
body('username').isLength({ min: 1 }).withMessage('Username is required'), body('username').isLength({ min: 1 }).withMessage('Username is required'),
body('email').isEmail().withMessage('Valid email is required'), body('email').isEmail().withMessage('Valid email is required'),
body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters for security') body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters for security')
@@ -45,7 +48,7 @@ router.post('/setup-admin', [
}); });
} }
const { username, email, password } = req.body; const { firstName, lastName, username, email, password } = req.body;
// Check if any admin users already exist // Check if any admin users already exist
const adminCount = await prisma.users.count({ const adminCount = await prisma.users.count({
@@ -84,6 +87,8 @@ router.post('/setup-admin', [
username: username.trim(), username: username.trim(),
email: email.trim(), email: email.trim(),
password_hash: passwordHash, password_hash: passwordHash,
first_name: firstName.trim(),
last_name: lastName.trim(),
role: 'admin', role: 'admin',
is_active: true, is_active: true,
created_at: new Date(), created_at: new Date(),
@@ -98,6 +103,9 @@ router.post('/setup-admin', [
} }
}); });
// Create default dashboard preferences for the new admin user
await createDefaultDashboardPreferences(user.id, 'admin');
res.status(201).json({ res.status(201).json({
message: 'Admin user created successfully', message: 'Admin user created successfully',
user: user user: user
@@ -173,7 +181,14 @@ router.post('/admin/users', authenticateToken, requireManageUsers, [
return res.status(400).json({ errors: errors.array() }); return res.status(400).json({ errors: errors.array() });
} }
const { username, email, password, first_name, last_name, role = 'user' } = req.body; const { username, email, password, first_name, last_name, role } = req.body;
// Get default user role from settings if no role specified
let userRole = role;
if (!userRole) {
const settings = await prisma.settings.findFirst();
userRole = settings?.default_user_role || 'user';
}
// Check if user already exists // Check if user already exists
const existingUser = await prisma.users.findFirst({ const existingUser = await prisma.users.findFirst({
@@ -201,7 +216,7 @@ router.post('/admin/users', authenticateToken, requireManageUsers, [
password_hash: passwordHash, password_hash: passwordHash,
first_name: first_name || null, first_name: first_name || null,
last_name: last_name || null, last_name: last_name || null,
role, role: userRole,
updated_at: new Date() updated_at: new Date()
}, },
select: { select: {
@@ -216,6 +231,9 @@ router.post('/admin/users', authenticateToken, requireManageUsers, [
} }
}); });
// Create default dashboard preferences for the new user
await createDefaultDashboardPreferences(user.id, userRole);
res.status(201).json({ res.status(201).json({
message: 'User created successfully', message: 'User created successfully',
user user
@@ -449,6 +467,8 @@ router.get('/signup-enabled', async (req, res) => {
// Public signup endpoint // Public signup endpoint
router.post('/signup', [ router.post('/signup', [
body('firstName').isLength({ min: 1 }).withMessage('First name is required'),
body('lastName').isLength({ min: 1 }).withMessage('Last name is required'),
body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'), body('username').isLength({ min: 3 }).withMessage('Username must be at least 3 characters'),
body('email').isEmail().withMessage('Valid email 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: 6 }).withMessage('Password must be at least 6 characters')
@@ -465,7 +485,7 @@ router.post('/signup', [
return res.status(400).json({ errors: errors.array() }); return res.status(400).json({ errors: errors.array() });
} }
const { username, email, password } = req.body; const { firstName, lastName, username, email, password } = req.body;
// Check if user already exists // Check if user already exists
const existingUser = await prisma.users.findFirst({ const existingUser = await prisma.users.findFirst({
@@ -484,14 +504,19 @@ router.post('/signup', [
// Hash password // Hash password
const passwordHash = await bcrypt.hash(password, 12); const passwordHash = await bcrypt.hash(password, 12);
// Create user with default 'user' role // Get default user role from settings or environment variable
const defaultRole = settings?.default_user_role || process.env.DEFAULT_USER_ROLE || 'user';
// Create user with default role from settings
const user = await prisma.users.create({ const user = await prisma.users.create({
data: { data: {
id: uuidv4(), id: uuidv4(),
username, username,
email, email,
password_hash: passwordHash, password_hash: passwordHash,
role: 'user', first_name: firstName.trim(),
last_name: lastName.trim(),
role: defaultRole,
updated_at: new Date() updated_at: new Date()
}, },
select: { select: {
@@ -504,6 +529,9 @@ router.post('/signup', [
} }
}); });
// Create default dashboard preferences for the new user
await createDefaultDashboardPreferences(user.id, defaultRole);
console.log(`New user registered: ${user.username} (${user.email})`); console.log(`New user registered: ${user.username} (${user.email})`);
// Generate token for immediate login // Generate token for immediate login

View File

@@ -2,10 +2,115 @@ const express = require('express');
const { body, validationResult } = require('express-validator'); const { body, validationResult } = require('express-validator');
const { PrismaClient } = require('@prisma/client'); const { PrismaClient } = require('@prisma/client');
const { authenticateToken } = require('../middleware/auth'); const { authenticateToken } = require('../middleware/auth');
const { v4: uuidv4 } = require('uuid');
const router = express.Router(); const router = express.Router();
const prisma = new PrismaClient(); const prisma = new PrismaClient();
// Helper function to get user permissions based on role
async function getUserPermissions(userRole) {
try {
const permissions = await prisma.role_permissions.findUnique({
where: { role: userRole }
});
// If no specific permissions found, return default admin permissions (for backward compatibility)
if (!permissions) {
console.warn(`No permissions found for role: ${userRole}, defaulting to admin access`);
return {
can_view_dashboard: true,
can_view_hosts: true,
can_manage_hosts: true,
can_view_packages: true,
can_manage_packages: true,
can_view_users: true,
can_manage_users: true,
can_view_reports: true,
can_export_data: true,
can_manage_settings: true
};
}
return permissions;
} catch (error) {
console.error('Error fetching user permissions:', error);
// Return admin permissions as fallback
return {
can_view_dashboard: true,
can_view_hosts: true,
can_manage_hosts: true,
can_view_packages: true,
can_manage_packages: true,
can_view_users: true,
can_manage_users: true,
can_view_reports: true,
can_export_data: true,
can_manage_settings: true
};
}
}
// Helper function to create permission-based dashboard preferences for a new user
async function createDefaultDashboardPreferences(userId, userRole = 'user') {
try {
// Get user's actual permissions
const permissions = await getUserPermissions(userRole);
// Define all possible dashboard cards with their required permissions
const allCards = [
// Host-related cards
{ cardId: 'totalHosts', requiredPermission: 'can_view_hosts', order: 0 },
{ cardId: 'hostsNeedingUpdates', requiredPermission: 'can_view_hosts', order: 1 },
{ cardId: 'upToDateHosts', requiredPermission: 'can_view_hosts', order: 2 },
{ cardId: 'totalHostGroups', requiredPermission: 'can_view_hosts', order: 3 },
// Package-related cards
{ cardId: 'totalOutdatedPackages', requiredPermission: 'can_view_packages', order: 4 },
{ cardId: 'securityUpdates', requiredPermission: 'can_view_packages', order: 5 },
{ cardId: 'packagePriority', requiredPermission: 'can_view_packages', order: 6 },
// Repository-related cards
{ cardId: 'totalRepos', requiredPermission: 'can_view_hosts', order: 7 }, // Repos are host-related
// User management cards (admin only)
{ cardId: 'totalUsers', requiredPermission: 'can_view_users', order: 8 },
{ cardId: 'recentUsers', requiredPermission: 'can_view_users', order: 9 },
// System/Report cards
{ cardId: 'osDistribution', requiredPermission: 'can_view_reports', order: 10 },
{ cardId: 'osDistributionBar', requiredPermission: 'can_view_reports', order: 11 },
{ cardId: 'updateStatus', requiredPermission: 'can_view_reports', order: 12 },
{ cardId: 'recentCollection', requiredPermission: 'can_view_hosts', order: 13 }, // Collection is host-related
{ cardId: 'quickStats', requiredPermission: 'can_view_dashboard', order: 14 }
];
// Filter cards based on user's permissions
const allowedCards = allCards.filter(card => {
return permissions[card.requiredPermission] === true;
});
// Create preferences data
const preferencesData = allowedCards.map((card) => ({
id: uuidv4(),
user_id: userId,
card_id: card.cardId,
enabled: true,
order: card.order, // Preserve original order from allCards
created_at: new Date(),
updated_at: new Date()
}));
await prisma.dashboard_preferences.createMany({
data: preferencesData
});
console.log(`Permission-based dashboard preferences created for user ${userId} with role ${userRole}: ${allowedCards.length} cards`);
} catch (error) {
console.error('Error creating default dashboard preferences:', error);
// Don't throw error - this shouldn't break user creation
}
}
// Get user's dashboard preferences // Get user's dashboard preferences
router.get('/', authenticateToken, async (req, res) => { router.get('/', authenticateToken, async (req, res) => {
try { try {
@@ -69,22 +174,24 @@ router.put('/', authenticateToken, [
// Get default dashboard card configuration // Get default dashboard card configuration
router.get('/defaults', authenticateToken, async (req, res) => { router.get('/defaults', authenticateToken, async (req, res) => {
try { try {
// Default configuration based on iby's (Muhammad Ibrahim) preferred layout
// This provides a comprehensive dashboard view for all new users
const defaultCards = [ const defaultCards = [
{ cardId: 'totalHosts', title: 'Total Hosts', icon: 'Server', enabled: true, order: 0 }, { cardId: 'totalHosts', title: 'Total Hosts', icon: 'Server', enabled: true, order: 0 },
{ cardId: 'hostsNeedingUpdates', title: 'Needs Updating', icon: 'AlertTriangle', enabled: true, order: 1 }, { cardId: 'hostsNeedingUpdates', title: 'Needs Updating', icon: 'AlertTriangle', enabled: true, order: 1 },
{ cardId: 'totalOutdatedPackages', title: 'Outdated Packages', icon: 'Package', enabled: true, order: 2 }, { cardId: 'totalOutdatedPackages', title: 'Outdated Packages', icon: 'Package', enabled: true, order: 2 },
{ cardId: 'securityUpdates', title: 'Security Updates', icon: 'Shield', enabled: true, order: 3 }, { cardId: 'securityUpdates', title: 'Security Updates', icon: 'Shield', enabled: true, order: 3 },
{ cardId: 'upToDateHosts', title: 'Up to date', icon: 'CheckCircle', enabled: true, order: 4 }, { cardId: 'totalHostGroups', title: 'Host Groups', icon: 'Folder', enabled: true, order: 4 },
{ cardId: 'totalHostGroups', title: 'Host Groups', icon: 'Folder', enabled: false, order: 5 }, { cardId: 'upToDateHosts', title: 'Up to date', icon: 'CheckCircle', enabled: true, order: 5 },
{ cardId: 'totalUsers', title: 'Users', icon: 'Users', enabled: false, order: 6 }, { cardId: 'totalRepos', title: 'Repositories', icon: 'GitBranch', enabled: true, order: 6 },
{ cardId: 'totalRepos', title: 'Repositories', icon: 'GitBranch', enabled: false, order: 7 }, { cardId: 'totalUsers', title: 'Users', icon: 'Users', enabled: true, order: 7 },
{ cardId: 'osDistribution', title: 'OS Distribution', icon: 'BarChart3', enabled: true, order: 8 }, { cardId: 'osDistribution', title: 'OS Distribution', icon: 'BarChart3', enabled: true, order: 8 },
{ cardId: 'osDistributionBar', title: 'OS Distribution (Bar)', icon: 'BarChart3', enabled: false, order: 9 }, { cardId: 'osDistributionBar', title: 'OS Distribution (Bar)', icon: 'BarChart3', enabled: true, order: 9 },
{ cardId: 'updateStatus', title: 'Update Status', icon: 'BarChart3', enabled: true, order: 10 }, { cardId: 'recentCollection', title: 'Recent Collection', icon: 'Server', enabled: true, order: 10 },
{ cardId: 'packagePriority', title: 'Package Priority', icon: 'BarChart3', enabled: true, order: 11 }, { cardId: 'updateStatus', title: 'Update Status', icon: 'BarChart3', enabled: true, order: 11 },
{ cardId: 'quickStats', title: 'Quick Stats', icon: 'TrendingUp', enabled: true, order: 12 }, { cardId: 'packagePriority', title: 'Package Priority', icon: 'BarChart3', enabled: true, order: 12 },
{ cardId: 'recentUsers', title: 'Recent Users Logged in', icon: 'Users', enabled: true, order: 13 }, { cardId: 'recentUsers', title: 'Recent Users Logged in', icon: 'Users', enabled: true, order: 13 },
{ cardId: 'recentCollection', title: 'Recent Collection', icon: 'Server', enabled: true, order: 14 } { cardId: 'quickStats', title: 'Quick Stats', icon: 'TrendingUp', enabled: true, order: 14 }
]; ];
res.json(defaultCards); res.json(defaultCards);
@@ -94,4 +201,4 @@ router.get('/defaults', authenticateToken, async (req, res) => {
} }
}); });
module.exports = router; module.exports = { router, createDefaultDashboardPreferences };

View File

@@ -59,9 +59,9 @@ router.put('/roles/:role', authenticateToken, requireManageSettings, async (req,
can_manage_settings can_manage_settings
} = req.body; } = req.body;
// Prevent modifying admin role permissions (admin should always have full access) // Prevent modifying admin and user role permissions (built-in roles)
if (role === 'admin') { if (role === 'admin' || role === 'user') {
return res.status(400).json({ error: 'Cannot modify admin role permissions' }); return res.status(400).json({ error: `Cannot modify ${role} role permissions - this is a built-in role` });
} }
const permissions = await prisma.role_permissions.upsert({ const permissions = await prisma.role_permissions.upsert({
@@ -111,9 +111,9 @@ router.delete('/roles/:role', authenticateToken, requireManageSettings, async (r
try { try {
const { role } = req.params; const { role } = req.params;
// Prevent deleting admin role // Prevent deleting admin and user roles (built-in roles)
if (role === 'admin') { if (role === 'admin' || role === 'user') {
return res.status(400).json({ error: 'Cannot delete admin role' }); return res.status(400).json({ error: `Cannot delete ${role} role - this is a built-in role` });
} }
// Check if any users are using this role // Check if any users are using this role

View File

@@ -111,6 +111,7 @@ router.put('/', authenticateToken, requireManageSettings, [
body('updateInterval').isInt({ min: 5, max: 1440 }).withMessage('Update interval must be between 5 and 1440 minutes'), 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('autoUpdate').isBoolean().withMessage('Auto update must be a boolean'),
body('signupEnabled').isBoolean().withMessage('Signup enabled must be a boolean'), body('signupEnabled').isBoolean().withMessage('Signup enabled must be a boolean'),
body('defaultUserRole').optional().isLength({ min: 1 }).withMessage('Default user role must be a non-empty string'),
body('githubRepoUrl').optional().isLength({ min: 1 }).withMessage('GitHub repo URL must be a non-empty string'), 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('repositoryType').optional().isIn(['public', 'private']).withMessage('Repository type must be public or private'),
body('sshKeyPath').optional().custom((value) => { body('sshKeyPath').optional().custom((value) => {
@@ -130,7 +131,7 @@ router.put('/', authenticateToken, requireManageSettings, [
return res.status(400).json({ errors: errors.array() }); return res.status(400).json({ errors: errors.array() });
} }
const { serverProtocol, serverHost, serverPort, updateInterval, autoUpdate, signupEnabled, githubRepoUrl, repositoryType, sshKeyPath } = req.body; const { serverProtocol, serverHost, serverPort, updateInterval, autoUpdate, signupEnabled, defaultUserRole, githubRepoUrl, repositoryType, sshKeyPath } = req.body;
// Get current settings to check for update interval changes // Get current settings to check for update interval changes
const currentSettings = await getSettings(); const currentSettings = await getSettings();
@@ -144,6 +145,7 @@ router.put('/', authenticateToken, requireManageSettings, [
update_interval: updateInterval || 60, update_interval: updateInterval || 60,
auto_update: autoUpdate || false, auto_update: autoUpdate || false,
signup_enabled: signupEnabled || false, signup_enabled: signupEnabled || false,
default_user_role: defaultUserRole || process.env.DEFAULT_USER_ROLE || 'user',
github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git', github_repo_url: githubRepoUrl !== undefined ? githubRepoUrl : 'git@github.com:9technologygroup/patchmon.net.git',
repository_type: repositoryType || 'public', repository_type: repositoryType || 'public',
ssh_key_path: sshKeyPath || null, ssh_key_path: sshKeyPath || null,

View File

@@ -14,7 +14,7 @@ const packageRoutes = require('./routes/packageRoutes');
const dashboardRoutes = require('./routes/dashboardRoutes'); const dashboardRoutes = require('./routes/dashboardRoutes');
const permissionsRoutes = require('./routes/permissionsRoutes'); const permissionsRoutes = require('./routes/permissionsRoutes');
const settingsRoutes = require('./routes/settingsRoutes'); const settingsRoutes = require('./routes/settingsRoutes');
const dashboardPreferencesRoutes = require('./routes/dashboardPreferencesRoutes'); const { router: dashboardPreferencesRoutes, createDefaultDashboardPreferences } = require('./routes/dashboardPreferencesRoutes');
const repositoryRoutes = require('./routes/repositoryRoutes'); const repositoryRoutes = require('./routes/repositoryRoutes');
const versionRoutes = require('./routes/versionRoutes'); const versionRoutes = require('./routes/versionRoutes');
const tfaRoutes = require('./routes/tfaRoutes'); const tfaRoutes = require('./routes/tfaRoutes');
@@ -188,6 +188,111 @@ async function checkAndImportAgentVersion() {
} }
} }
// Function to check and create default role permissions on startup
async function checkAndCreateRolePermissions() {
console.log('🔐 Starting role permissions auto-creation check...');
// Skip if auto-creation is disabled
if (process.env.AUTO_CREATE_ROLE_PERMISSIONS === 'false') {
console.log('❌ Auto-creation of role permissions is disabled');
if (process.env.ENABLE_LOGGING === 'true') {
logger.info('Auto-creation of role permissions is disabled');
}
return;
}
try {
const crypto = require('crypto');
// Define default roles and permissions
const defaultRoles = [
{
id: crypto.randomUUID(),
role: 'admin',
can_view_dashboard: true,
can_view_hosts: true,
can_manage_hosts: true,
can_view_packages: true,
can_manage_packages: true,
can_view_users: true,
can_manage_users: true,
can_view_reports: true,
can_export_data: true,
can_manage_settings: true,
created_at: new Date(),
updated_at: new Date()
},
{
id: crypto.randomUUID(),
role: 'user',
can_view_dashboard: true,
can_view_hosts: true,
can_manage_hosts: false,
can_view_packages: true,
can_manage_packages: false,
can_view_users: false,
can_manage_users: false,
can_view_reports: true,
can_export_data: false,
can_manage_settings: false,
created_at: new Date(),
updated_at: new Date()
}
];
const createdRoles = [];
const existingRoles = [];
for (const roleData of defaultRoles) {
// Check if role already exists
const existingRole = await prisma.role_permissions.findUnique({
where: { role: roleData.role }
});
if (existingRole) {
console.log(`✅ Role '${roleData.role}' already exists in database`);
existingRoles.push(existingRole);
if (process.env.ENABLE_LOGGING === 'true') {
logger.info(`Role '${roleData.role}' already exists in database`);
}
} else {
// Create new role permission
const permission = await prisma.role_permissions.create({
data: roleData
});
createdRoles.push(permission);
console.log(`🆕 Created role '${roleData.role}' with permissions`);
if (process.env.ENABLE_LOGGING === 'true') {
logger.info(`Created role '${roleData.role}' with permissions`);
}
}
}
if (createdRoles.length > 0) {
console.log(`🎉 Successfully auto-created ${createdRoles.length} role permissions on startup`);
console.log('📋 Created roles:');
createdRoles.forEach(role => {
console.log(`${role.role}: dashboard=${role.can_view_dashboard}, hosts=${role.can_manage_hosts}, packages=${role.can_manage_packages}, users=${role.can_manage_users}, settings=${role.can_manage_settings}`);
});
if (process.env.ENABLE_LOGGING === 'true') {
logger.info(`✅ Auto-created ${createdRoles.length} role permissions on startup`);
}
} else {
console.log(`✅ All default role permissions already exist (${existingRoles.length} roles verified)`);
if (process.env.ENABLE_LOGGING === 'true') {
logger.info(`All default role permissions already exist (${existingRoles.length} roles verified)`);
}
}
} catch (error) {
console.error('❌ Failed to check/create role permissions on startup:', error.message);
if (process.env.ENABLE_LOGGING === 'true') {
logger.error('Failed to check/create role permissions on startup:', error.message);
}
}
}
// Initialize logger - only if logging is enabled // Initialize logger - only if logging is enabled
const logger = process.env.ENABLE_LOGGING === 'true' ? winston.createLogger({ const logger = process.env.ENABLE_LOGGING === 'true' ? winston.createLogger({
level: process.env.LOG_LEVEL || 'info', level: process.env.LOG_LEVEL || 'info',
@@ -405,6 +510,201 @@ process.on('SIGTERM', async () => {
process.exit(0); process.exit(0);
}); });
// Initialize dashboard preferences for all users
async function initializeDashboardPreferences() {
try {
console.log('🔧 Initializing dashboard preferences for all users...');
// Get all users
const users = await prisma.users.findMany({
select: {
id: true,
username: true,
email: true,
role: true,
dashboard_preferences: {
select: {
card_id: true
}
}
}
});
if (users.length === 0) {
console.log(' No users found in database');
return;
}
console.log(`📊 Found ${users.length} users to initialize`);
let initializedCount = 0;
let updatedCount = 0;
for (const user of users) {
const hasPreferences = user.dashboard_preferences.length > 0;
// Get permission-based preferences for this user's role
const expectedPreferences = await getPermissionBasedPreferences(user.role);
const expectedCardCount = expectedPreferences.length;
if (!hasPreferences) {
// User has no preferences - create them
console.log(`⚙️ Creating preferences for ${user.username} (${user.role})`);
const preferencesData = expectedPreferences.map(pref => ({
id: require('uuid').v4(),
user_id: user.id,
card_id: pref.cardId,
enabled: pref.enabled,
order: pref.order,
created_at: new Date(),
updated_at: new Date()
}));
await prisma.dashboard_preferences.createMany({
data: preferencesData
});
initializedCount++;
console.log(` ✅ Created ${expectedCardCount} cards based on permissions`);
} else {
// User already has preferences - check if they need updating
const currentCardCount = user.dashboard_preferences.length;
if (currentCardCount !== expectedCardCount) {
console.log(`🔄 Updating preferences for ${user.username} (${user.role}) - ${currentCardCount}${expectedCardCount} cards`);
// Delete existing preferences
await prisma.dashboard_preferences.deleteMany({
where: { user_id: user.id }
});
// Create new preferences based on permissions
const preferencesData = expectedPreferences.map(pref => ({
id: require('uuid').v4(),
user_id: user.id,
card_id: pref.cardId,
enabled: pref.enabled,
order: pref.order,
created_at: new Date(),
updated_at: new Date()
}));
await prisma.dashboard_preferences.createMany({
data: preferencesData
});
updatedCount++;
console.log(` ✅ Updated to ${expectedCardCount} cards based on permissions`);
} else {
console.log(`${user.username} already has correct preferences (${currentCardCount} cards)`);
}
}
}
console.log(`\n📋 Dashboard Preferences Initialization Complete:`);
console.log(` - New users initialized: ${initializedCount}`);
console.log(` - Existing users updated: ${updatedCount}`);
console.log(` - Users with correct preferences: ${users.length - initializedCount - updatedCount}`);
console.log(`\n🎯 Permission-based preferences:`);
console.log(` - Cards are now assigned based on actual user permissions`);
console.log(` - Each card requires specific permissions (can_view_hosts, can_view_users, etc.)`);
console.log(` - Users only see cards they have permission to access`);
} catch (error) {
console.error('❌ Error initializing dashboard preferences:', error);
throw error;
}
}
// Helper function to get user permissions based on role
async function getUserPermissions(userRole) {
try {
const permissions = await prisma.role_permissions.findUnique({
where: { role: userRole }
});
// If no specific permissions found, return default admin permissions (for backward compatibility)
if (!permissions) {
console.warn(`No permissions found for role: ${userRole}, defaulting to admin access`);
return {
can_view_dashboard: true,
can_view_hosts: true,
can_manage_hosts: true,
can_view_packages: true,
can_manage_packages: true,
can_view_users: true,
can_manage_users: true,
can_view_reports: true,
can_export_data: true,
can_manage_settings: true
};
}
return permissions;
} catch (error) {
console.error('Error fetching user permissions:', error);
// Return admin permissions as fallback
return {
can_view_dashboard: true,
can_view_hosts: true,
can_manage_hosts: true,
can_view_packages: true,
can_manage_packages: true,
can_view_users: true,
can_manage_users: true,
can_view_reports: true,
can_export_data: true,
can_manage_settings: true
};
}
}
// Helper function to get permission-based dashboard preferences for a role
async function getPermissionBasedPreferences(userRole) {
// Get user's actual permissions
const permissions = await getUserPermissions(userRole);
// Define all possible dashboard cards with their required permissions
const allCards = [
// Host-related cards
{ cardId: 'totalHosts', requiredPermission: 'can_view_hosts', order: 0 },
{ cardId: 'hostsNeedingUpdates', requiredPermission: 'can_view_hosts', order: 1 },
{ cardId: 'upToDateHosts', requiredPermission: 'can_view_hosts', order: 2 },
{ cardId: 'totalHostGroups', requiredPermission: 'can_view_hosts', order: 3 },
// Package-related cards
{ cardId: 'totalOutdatedPackages', requiredPermission: 'can_view_packages', order: 4 },
{ cardId: 'securityUpdates', requiredPermission: 'can_view_packages', order: 5 },
{ cardId: 'packagePriority', requiredPermission: 'can_view_packages', order: 6 },
// Repository-related cards
{ cardId: 'totalRepos', requiredPermission: 'can_view_hosts', order: 7 }, // Repos are host-related
// User management cards (admin only)
{ cardId: 'totalUsers', requiredPermission: 'can_view_users', order: 8 },
{ cardId: 'recentUsers', requiredPermission: 'can_view_users', order: 9 },
// System/Report cards
{ cardId: 'osDistribution', requiredPermission: 'can_view_reports', order: 10 },
{ cardId: 'osDistributionBar', requiredPermission: 'can_view_reports', order: 11 },
{ cardId: 'updateStatus', requiredPermission: 'can_view_reports', order: 12 },
{ cardId: 'recentCollection', requiredPermission: 'can_view_hosts', order: 13 }, // Collection is host-related
{ cardId: 'quickStats', requiredPermission: 'can_view_dashboard', order: 14 }
];
// Filter cards based on user's permissions
const allowedCards = allCards.filter(card => {
return permissions[card.requiredPermission] === true;
});
return allowedCards.map((card) => ({
cardId: card.cardId,
enabled: true,
order: card.order // Preserve original order from allCards
}));
}
// Start server with database health check // Start server with database health check
async function startServer() { async function startServer() {
try { try {
@@ -430,7 +730,12 @@ async function startServer() {
// Check and import agent version on startup // Check and import agent version on startup
await checkAndImportAgentVersion(); await checkAndImportAgentVersion();
// Check and create default role permissions on startup
await checkAndCreateRolePermissions();
// Initialize dashboard preferences for all users
await initializeDashboardPreferences();
app.listen(PORT, () => { app.listen(PORT, () => {
if (process.env.ENABLE_LOGGING === 'true') { if (process.env.ENABLE_LOGGING === 'true') {
logger.info(`Server running on port ${PORT}`); logger.info(`Server running on port ${PORT}`);

View File

@@ -8,7 +8,9 @@ const FirstTimeAdminSetup = () => {
username: '', username: '',
email: '', email: '',
password: '', password: '',
confirmPassword: '' confirmPassword: '',
firstName: '',
lastName: ''
}) })
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState('') const [error, setError] = useState('')
@@ -25,6 +27,14 @@ const FirstTimeAdminSetup = () => {
} }
const validateForm = () => { const validateForm = () => {
if (!formData.firstName.trim()) {
setError('First name is required')
return false
}
if (!formData.lastName.trim()) {
setError('Last name is required')
return false
}
if (!formData.username.trim()) { if (!formData.username.trim()) {
setError('Username is required') setError('Username is required')
return false return false
@@ -69,7 +79,9 @@ const FirstTimeAdminSetup = () => {
body: JSON.stringify({ body: JSON.stringify({
username: formData.username.trim(), username: formData.username.trim(),
email: formData.email.trim(), email: formData.email.trim(),
password: formData.password password: formData.password,
firstName: formData.firstName.trim(),
lastName: formData.lastName.trim()
}) })
}) })
@@ -145,6 +157,41 @@ const FirstTimeAdminSetup = () => {
)} )}
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label htmlFor="firstName" className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
First Name
</label>
<input
type="text"
id="firstName"
name="firstName"
value={formData.firstName}
onChange={handleInputChange}
className="input w-full"
placeholder="Enter your first name"
required
disabled={isLoading}
/>
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
Last Name
</label>
<input
type="text"
id="lastName"
name="lastName"
value={formData.lastName}
onChange={handleInputChange}
className="input w-full"
placeholder="Enter your last name"
required
disabled={isLoading}
/>
</div>
</div>
<div> <div>
<label htmlFor="username" className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2"> <label htmlFor="username" className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-2">
Username Username

View File

@@ -47,7 +47,7 @@ const Layout = ({ children }) => {
const [userMenuOpen, setUserMenuOpen] = useState(false) const [userMenuOpen, setUserMenuOpen] = useState(false)
const [githubStars, setGithubStars] = useState(null) const [githubStars, setGithubStars] = useState(null)
const location = useLocation() const location = useLocation()
const { user, logout, canViewHosts, canManageHosts, canViewPackages, canViewUsers, canManageUsers, canManageSettings } = useAuth() const { user, logout, canViewDashboard, canViewHosts, canManageHosts, canViewPackages, canViewUsers, canManageUsers, canViewReports, canExportData, canManageSettings } = useAuth()
const { updateAvailable } = useUpdateNotification() const { updateAvailable } = useUpdateNotification()
const userMenuRef = useRef(null) const userMenuRef = useRef(null)
@@ -66,44 +66,103 @@ const Layout = ({ children }) => {
staleTime: 300000, // Consider data stale after 5 minutes staleTime: 300000, // Consider data stale after 5 minutes
}) })
const navigation = [ // Build navigation based on permissions
{ name: 'Dashboard', href: '/', icon: Home }, const buildNavigation = () => {
{ const nav = []
section: 'Inventory',
items: [ // Dashboard - only show if user can view dashboard
...(canViewHosts() ? [{ name: 'Hosts', href: '/hosts', icon: Server }] : []), if (canViewDashboard()) {
...(canViewPackages() ? [{ name: 'Packages', href: '/packages', icon: Package }] : []), nav.push({ name: 'Dashboard', href: '/', icon: Home })
...(canViewHosts() ? [{ name: 'Repos', href: '/repositories', icon: GitBranch }] : []), }
{ name: 'Services', href: '/services', icon: Activity, comingSoon: true },
{ name: 'Docker', href: '/docker', icon: Container, comingSoon: true }, // Inventory section - only show if user has any inventory permissions
{ name: 'Reporting', href: '/reporting', icon: BarChart3, comingSoon: true }, if (canViewHosts() || canViewPackages() || canViewReports()) {
] const inventoryItems = []
},
...(canViewUsers() || canManageUsers() ? [{ if (canViewHosts()) {
section: 'PatchMon Users', inventoryItems.push({ name: 'Hosts', href: '/hosts', icon: Server })
items: [ inventoryItems.push({ name: 'Repos', href: '/repositories', icon: GitBranch })
...(canViewUsers() ? [{ name: 'Users', href: '/users', icon: Users }] : []), }
...(canManageSettings() ? [{ name: 'Permissions', href: '/permissions', icon: Shield }] : []),
] if (canViewPackages()) {
}] : []), inventoryItems.push({ name: 'Packages', href: '/packages', icon: Package })
{ }
section: 'Settings',
items: [ if (canViewReports()) {
...(canManageHosts() ? [{ inventoryItems.push(
{ name: 'Services', href: '/services', icon: Activity, comingSoon: true },
{ name: 'Docker', href: '/docker', icon: Container, comingSoon: true },
{ name: 'Reporting', href: '/reporting', icon: BarChart3, comingSoon: true }
)
}
if (inventoryItems.length > 0) {
nav.push({
section: 'Inventory',
items: inventoryItems
})
}
}
// PatchMon Users section - only show if user can view/manage users
if (canViewUsers() || canManageUsers()) {
const userItems = []
if (canViewUsers()) {
userItems.push({ name: 'Users', href: '/users', icon: Users })
}
if (canManageSettings()) {
userItems.push({ name: 'Permissions', href: '/permissions', icon: Shield })
}
if (userItems.length > 0) {
nav.push({
section: 'PatchMon Users',
items: userItems
})
}
}
// Settings section - only show if user has any settings permissions
if (canManageSettings() || canViewReports() || canExportData()) {
const settingsItems = []
if (canManageSettings()) {
settingsItems.push({
name: 'PatchMon Options', name: 'PatchMon Options',
href: '/options', href: '/options',
icon: Settings icon: Settings
}] : []), })
{ name: 'Audit Log', href: '/audit-log', icon: FileText, comingSoon: true }, settingsItems.push({
...(canManageSettings() ? [{
name: 'Server Config', name: 'Server Config',
href: '/settings', href: '/settings',
icon: Wrench, icon: Wrench,
showUpgradeIcon: updateAvailable showUpgradeIcon: updateAvailable
}] : []), })
] }
if (canViewReports() || canExportData()) {
settingsItems.push({
name: 'Audit Log',
href: '/audit-log',
icon: FileText,
comingSoon: true
})
}
if (settingsItems.length > 0) {
nav.push({
section: 'Settings',
items: settingsItems
})
}
} }
]
return nav
}
const navigation = buildNavigation()
const isActive = (path) => location.pathname === path const isActive = (path) => location.pathname === path
@@ -221,6 +280,15 @@ const Layout = ({ children }) => {
</div> </div>
</div> </div>
<nav className="mt-8 flex-1 space-y-6 px-2"> <nav className="mt-8 flex-1 space-y-6 px-2">
{/* Show message for users with very limited permissions */}
{navigation.length === 0 && (
<div className="px-2 py-4 text-center">
<div className="text-sm text-secondary-500 dark:text-secondary-400">
<p className="mb-2">Limited access</p>
<p className="text-xs">Contact your administrator for additional permissions</p>
</div>
</div>
)}
{navigation.map((item, index) => { {navigation.map((item, index) => {
if (item.name) { if (item.name) {
// Single item (Dashboard) // Single item (Dashboard)
@@ -346,6 +414,15 @@ const Layout = ({ children }) => {
</div> </div>
<nav className="flex flex-1 flex-col"> <nav className="flex flex-1 flex-col">
<ul className="flex flex-1 flex-col gap-y-6"> <ul className="flex flex-1 flex-col gap-y-6">
{/* Show message for users with very limited permissions */}
{navigation.length === 0 && (
<li className="px-2 py-4 text-center">
<div className="text-sm text-secondary-500 dark:text-secondary-400">
<p className="mb-2">Limited access</p>
<p className="text-xs">Contact your administrator for additional permissions</p>
</div>
</li>
)}
{navigation.map((item, index) => { {navigation.map((item, index) => {
if (item.name) { if (item.name) {
// Single item (Dashboard) // Single item (Dashboard)

View File

@@ -9,7 +9,9 @@ const Login = () => {
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
username: '', username: '',
email: '', email: '',
password: '' password: '',
firstName: '',
lastName: ''
}) })
const [tfaData, setTfaData] = useState({ const [tfaData, setTfaData] = useState({
token: '' token: ''
@@ -76,8 +78,7 @@ const Login = () => {
setError('') setError('')
try { try {
const response = await authAPI.signup(formData.username, formData.email, formData.password) const response = await authAPI.signup(formData.username, formData.email, formData.password, formData.firstName, formData.lastName)
if (response.data && response.data.token) { if (response.data && response.data.token) {
// Store token and user data // Store token and user data
localStorage.setItem('token', response.data.token) localStorage.setItem('token', response.data.token)
@@ -162,7 +163,9 @@ const Login = () => {
setFormData({ setFormData({
username: '', username: '',
email: '', email: '',
password: '' password: '',
firstName: '',
lastName: ''
}) })
setError('') setError('')
} }
@@ -211,10 +214,53 @@ const Login = () => {
</div> </div>
{isSignupMode && ( {isSignupMode && (
<div> <>
<label htmlFor="email" className="block text-sm font-medium text-secondary-700"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
Email <div>
</label> <label htmlFor="firstName" className="block text-sm font-medium text-secondary-700">
First Name
</label>
<div className="mt-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-secondary-400" />
</div>
<input
id="firstName"
name="firstName"
type="text"
required
value={formData.firstName}
onChange={handleInputChange}
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Enter your first name"
/>
</div>
</div>
<div>
<label htmlFor="lastName" className="block text-sm font-medium text-secondary-700">
Last Name
</label>
<div className="mt-1 relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<User className="h-5 w-5 text-secondary-400" />
</div>
<input
id="lastName"
name="lastName"
type="text"
required
value={formData.lastName}
onChange={handleInputChange}
className="appearance-none rounded-md relative block w-full pl-10 pr-3 py-2 border border-secondary-300 placeholder-secondary-500 text-secondary-900 focus:outline-none focus:ring-primary-500 focus:border-primary-500 focus:z-10 sm:text-sm"
placeholder="Enter your last name"
/>
</div>
</div>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-secondary-700">
Email
</label>
<div className="mt-1 relative"> <div className="mt-1 relative">
<input <input
id="email" id="email"
@@ -235,6 +281,7 @@ const Login = () => {
</div> </div>
</div> </div>
</div> </div>
</>
)} )}
<div> <div>

View File

@@ -174,7 +174,7 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
onSave(role.role, permissions) onSave(role.role, permissions)
} }
const isAdminRole = role.role === 'admin' const isBuiltInRole = role.role === 'admin' || role.role === 'user'
return ( return (
<div className="bg-white dark:bg-secondary-800 shadow rounded-lg"> <div className="bg-white dark:bg-secondary-800 shadow rounded-lg">
@@ -183,9 +183,9 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
<div className="flex items-center"> <div className="flex items-center">
<Shield className="h-5 w-5 text-primary-600 mr-3" /> <Shield className="h-5 w-5 text-primary-600 mr-3" />
<h3 className="text-lg font-medium text-secondary-900 dark:text-white capitalize">{role.role}</h3> <h3 className="text-lg font-medium text-secondary-900 dark:text-white capitalize">{role.role}</h3>
{isAdminRole && ( {isBuiltInRole && (
<span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800"> <span className="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
System Role Built-in Role
</span> </span>
)} )}
</div> </div>
@@ -211,13 +211,13 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
<> <>
<button <button
onClick={onEdit} onClick={onEdit}
disabled={isAdminRole} disabled={isBuiltInRole}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed" className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-primary-600 hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Edit className="h-4 w-4 mr-1" /> <Edit className="h-4 w-4 mr-1" />
Edit Edit
</button> </button>
{!isAdminRole && ( {!isBuiltInRole && (
<button <button
onClick={() => onDelete(role.role)} onClick={() => onDelete(role.role)}
className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700" className="inline-flex items-center px-3 py-1 border border-transparent text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700"
@@ -245,7 +245,7 @@ const RolePermissionsCard = ({ role, isEditing, onEdit, onCancel, onSave, onDele
type="checkbox" type="checkbox"
checked={isChecked} checked={isChecked}
onChange={(e) => handlePermissionChange(field.key, e.target.checked)} onChange={(e) => handlePermissionChange(field.key, e.target.checked)}
disabled={!isEditing || (isAdminRole && field.key === 'can_manage_users')} disabled={!isEditing || (isBuiltInRole && field.key === 'can_manage_users')}
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded disabled:opacity-50" className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded disabled:opacity-50"
/> />
</div> </div>

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Save, Server, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon, Clock } from 'lucide-react'; import { Save, Server, Shield, AlertCircle, CheckCircle, Code, Plus, Trash2, Star, Download, X, Settings as SettingsIcon, Clock } from 'lucide-react';
import { settingsAPI, agentVersionAPI, versionAPI } from '../utils/api'; import { settingsAPI, agentVersionAPI, versionAPI, permissionsAPI } from '../utils/api';
import { useUpdateNotification } from '../contexts/UpdateNotificationContext'; import { useUpdateNotification } from '../contexts/UpdateNotificationContext';
import UpgradeNotificationIcon from '../components/UpgradeNotificationIcon'; import UpgradeNotificationIcon from '../components/UpgradeNotificationIcon';
@@ -13,6 +13,7 @@ const Settings = () => {
updateInterval: 60, updateInterval: 60,
autoUpdate: false, autoUpdate: false,
signupEnabled: false, signupEnabled: false,
defaultUserRole: 'user',
githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git', githubRepoUrl: 'git@github.com:9technologygroup/patchmon.net.git',
repositoryType: 'public', repositoryType: 'public',
sshKeyPath: '', sshKeyPath: '',
@@ -68,6 +69,12 @@ const Settings = () => {
queryFn: () => settingsAPI.get().then(res => res.data) queryFn: () => settingsAPI.get().then(res => res.data)
}); });
// Fetch available roles for default user role dropdown
const { data: roles, isLoading: rolesLoading } = useQuery({
queryKey: ['rolePermissions'],
queryFn: () => permissionsAPI.getRoles().then(res => res.data)
});
// Update form data when settings are loaded // Update form data when settings are loaded
useEffect(() => { useEffect(() => {
if (settings) { if (settings) {
@@ -78,6 +85,7 @@ const Settings = () => {
updateInterval: settings.update_interval || 60, updateInterval: settings.update_interval || 60,
autoUpdate: settings.auto_update || false, autoUpdate: settings.auto_update || false,
signupEnabled: settings.signup_enabled === true ? true : false, // Explicit boolean conversion signupEnabled: settings.signup_enabled === true ? true : false, // Explicit boolean conversion
defaultUserRole: settings.default_user_role || 'user',
githubRepoUrl: settings.github_repo_url || 'git@github.com:9technologygroup/patchmon.net.git', githubRepoUrl: settings.github_repo_url || 'git@github.com:9technologygroup/patchmon.net.git',
repositoryType: settings.repository_type || 'public', repositoryType: settings.repository_type || 'public',
sshKeyPath: settings.ssh_key_path || '', sshKeyPath: settings.ssh_key_path || '',
@@ -560,6 +568,37 @@ const Settings = () => {
Enable User Self-Registration Enable User Self-Registration
</div> </div>
</label> </label>
{/* Default User Role Dropdown */}
{formData.signupEnabled && (
<div className="mt-3 ml-6">
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
Default Role for New Users
</label>
<select
value={formData.defaultUserRole}
onChange={(e) => handleInputChange('defaultUserRole', e.target.value)}
className="w-full max-w-xs border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
disabled={rolesLoading}
>
{rolesLoading ? (
<option>Loading roles...</option>
) : roles && Array.isArray(roles) ? (
roles.map((role) => (
<option key={role.role} value={role.role}>
{role.role.charAt(0).toUpperCase() + role.role.slice(1)}
</option>
))
) : (
<option value="user">User</option>
)}
</select>
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
New users will be assigned this role when they register.
</p>
</div>
)}
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400"> <p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
When enabled, users can create their own accounts through the signup page. When disabled, only administrators can create user accounts. When enabled, users can create their own accounts through the signup page. When disabled, only administrators can create user accounts.
</p> </p>

View File

@@ -201,7 +201,7 @@ export const versionAPI = {
export const authAPI = { export const authAPI = {
login: (username, password) => api.post('/auth/login', { username, password }), login: (username, password) => api.post('/auth/login', { username, password }),
verifyTfa: (username, token) => api.post('/auth/verify-tfa', { username, token }), verifyTfa: (username, token) => api.post('/auth/verify-tfa', { username, token }),
signup: (username, email, password) => api.post('/auth/signup', { username, email, password }), signup: (username, email, password, firstName, lastName) => api.post('/auth/signup', { username, email, password, firstName, lastName }),
} }
// TFA API // TFA API