Merge pull request #180 from PatchMon/feature/go-agent

fixing redis environment issue and some UI fixes
This commit is contained in:
9 Technology Group LTD
2025-10-18 02:06:34 +01:00
committed by GitHub
6 changed files with 207 additions and 46 deletions

View File

@@ -847,6 +847,119 @@ router.post(
// }, // },
// ); // );
// Admin endpoint to bulk update host groups (many-to-many)
router.put(
"/bulk/groups",
authenticateToken,
requireManageHosts,
[
body("hostIds").isArray().withMessage("Host IDs must be an array"),
body("hostIds.*")
.isLength({ min: 1 })
.withMessage("Each host ID must be provided"),
body("groupIds").isArray().optional(),
body("groupIds.*")
.optional()
.isUUID()
.withMessage("Each group ID must be a valid UUID"),
],
async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
const { hostIds, groupIds = [] } = req.body;
// Verify all groups exist if provided
if (groupIds.length > 0) {
const existingGroups = await prisma.host_groups.findMany({
where: { id: { in: groupIds } },
select: { id: true },
});
if (existingGroups.length !== groupIds.length) {
return res.status(400).json({
error: "One or more host groups not found",
provided: groupIds,
found: existingGroups.map((g) => g.id),
});
}
}
// Check if all hosts exist
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(400).json({
error: "Some hosts not found",
missingHostIds: missingIds,
});
}
// Use transaction to update group memberships for all hosts
const updatedHosts = await prisma.$transaction(async (tx) => {
const results = [];
for (const hostId of hostIds) {
// Remove existing memberships for this host
await tx.host_group_memberships.deleteMany({
where: { host_id: hostId },
});
// Add new memberships for this host
if (groupIds.length > 0) {
await tx.host_group_memberships.createMany({
data: groupIds.map((groupId) => ({
id: crypto.randomUUID(),
host_id: hostId,
host_group_id: groupId,
})),
});
}
// Get updated host with groups
const updatedHost = await tx.hosts.findUnique({
where: { id: hostId },
include: {
host_group_memberships: {
include: {
host_groups: {
select: {
id: true,
name: true,
color: true,
},
},
},
},
},
});
results.push(updatedHost);
}
return results;
});
res.json({
message: `Successfully updated ${updatedHosts.length} host${updatedHosts.length !== 1 ? "s" : ""}`,
updatedCount: updatedHosts.length,
hosts: updatedHosts,
});
} catch (error) {
console.error("Bulk host groups update error:", error);
res.status(500).json({ error: "Failed to update host groups" });
}
},
);
// Admin endpoint to update host groups (many-to-many) // Admin endpoint to update host groups (many-to-many)
router.put( router.put(
"/:hostId/groups", "/:hostId/groups",

View File

@@ -39,15 +39,15 @@ These tags are available for both backend and frontend images as they are versio
environment: environment:
DATABASE_URL: postgresql://patchmon_user:REPLACE_YOUR_POSTGRES_PASSWORD_HERE@database:5432/patchmon_db DATABASE_URL: postgresql://patchmon_user:REPLACE_YOUR_POSTGRES_PASSWORD_HERE@database:5432/patchmon_db
``` ```
4. Set a Redis password in the Redis service where it says: 4. Set a Redis password in the Redis service command where it says:
```yaml ```yaml
environment: command: redis-server --requirepass your-redis-password-here
REDIS_PASSWORD: # CREATE A STRONG REDIS PASSWORD AND PUT IT HERE
``` ```
Note: The Redis service uses a hardcoded password in the command line for better reliability and to avoid environment variable parsing issues.
5. Update the corresponding `REDIS_PASSWORD` in the backend service where it says: 5. Update the corresponding `REDIS_PASSWORD` in the backend service where it says:
```yaml ```yaml
environment: environment:
REDIS_PASSWORD: REPLACE_YOUR_REDIS_PASSWORD_HERE REDIS_PASSWORD: your-redis-password-here
``` ```
6. Generate a strong JWT secret. You can do this like so: 6. Generate a strong JWT secret. You can do this like so:
```bash ```bash
@@ -123,6 +123,9 @@ When you do this, updating to a new version requires manually updating the image
| -------------- | ------------------ | ---------------- | | -------------- | ------------------ | ---------------- |
| `REDIS_PASSWORD` | Redis password | **MUST BE SET!** | | `REDIS_PASSWORD` | Redis password | **MUST BE SET!** |
> [!NOTE]
> The Redis service uses a hardcoded password in the command line (`redis-server --requirepass your-password`) instead of environment variables or configuration files. This approach eliminates parsing issues and provides better reliability. The password must be set in both the Redis command and the backend service environment variables.
#### Backend Service #### Backend Service
##### Database Configuration ##### Database Configuration

View File

@@ -19,14 +19,11 @@ services:
redis: redis:
image: redis:7-alpine image: redis:7-alpine
restart: unless-stopped restart: unless-stopped
command: redis-server /usr/local/etc/redis/redis.conf command: redis-server --requirepass your-redis-password-here
environment:
REDIS_PASSWORD: # CREATE A STRONG REDIS PASSWORD AND PUT IT HERE
volumes: volumes:
- redis_data:/data - redis_data:/data
- ./docker/redis.conf:/usr/local/etc/redis/redis.conf:ro
healthcheck: healthcheck:
test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "${REDIS_PASSWORD}", "ping"] test: ["CMD", "redis-cli", "--no-auth-warning", "-a", "your-redis-password-here", "ping"]
interval: 3s interval: 3s
timeout: 5s timeout: 5s
retries: 7 retries: 7
@@ -46,7 +43,7 @@ services:
# Redis Configuration # Redis Configuration
REDIS_HOST: redis REDIS_HOST: redis
REDIS_PORT: 6379 REDIS_PORT: 6379
REDIS_PASSWORD: REPLACE_YOUR_REDIS_PASSWORD_HERE REDIS_PASSWORD: your-redis-password-here
REDIS_DB: 0 REDIS_DB: 0
volumes: volumes:
- agent_files:/app/agents - agent_files:/app/agents

View File

@@ -1,10 +1,10 @@
# Redis Configuration for PatchMon Production # Redis Configuration for PatchMon Production
# Security settings # Security settings
requirepass ${REDIS_PASSWORD} # requirepass ${REDIS_PASSWORD} # Disabled - using command-line password instead
rename-command FLUSHDB "" rename-command FLUSHDB ""
rename-command FLUSHALL "" rename-command FLUSHALL ""
rename-command DEBUG "" rename-command DEBUG ""
rename-command CONFIG "CONFIG_${REDIS_PASSWORD}" rename-command CONFIG "CONFIG_DISABLED"
# Memory management # Memory management
maxmemory 256mb maxmemory 256mb
@@ -28,7 +28,7 @@ tcp-keepalive 300
timeout 0 timeout 0
# Disable dangerous commands # Disable dangerous commands
rename-command SHUTDOWN "SHUTDOWN_${REDIS_PASSWORD}" rename-command SHUTDOWN "SHUTDOWN_DISABLED"
rename-command KEYS "" rename-command KEYS ""
rename-command MONITOR "" rename-command MONITOR ""
rename-command SLAVEOF "" rename-command SLAVEOF ""

View File

@@ -505,8 +505,8 @@ const Hosts = () => {
}, [hosts]); }, [hosts]);
const bulkUpdateGroupMutation = useMutation({ const bulkUpdateGroupMutation = useMutation({
mutationFn: ({ hostIds, hostGroupId }) => mutationFn: ({ hostIds, groupIds }) =>
adminHostsAPI.bulkUpdateGroup(hostIds, hostGroupId), adminHostsAPI.bulkUpdateGroups(hostIds, groupIds),
onSuccess: (data) => { onSuccess: (data) => {
console.log("bulkUpdateGroupMutation success:", data); console.log("bulkUpdateGroupMutation success:", data);
@@ -517,11 +517,7 @@ const Hosts = () => {
return oldData.map((host) => { return oldData.map((host) => {
const updatedHost = data.hosts.find((h) => h.id === host.id); const updatedHost = data.hosts.find((h) => h.id === host.id);
if (updatedHost) { if (updatedHost) {
// Ensure hostGroupId is set correctly return updatedHost;
return {
...updatedHost,
hostGroupId: updatedHost.host_groups?.id || null,
};
} }
return host; return host;
}); });
@@ -671,8 +667,8 @@ const Hosts = () => {
} }
}; };
const handleBulkAssign = (hostGroupId) => { const handleBulkAssign = (groupIds) => {
bulkUpdateGroupMutation.mutate({ hostIds: selectedHosts, hostGroupId }); bulkUpdateGroupMutation.mutate({ hostIds: selectedHosts, groupIds });
}; };
const handleBulkDelete = () => { const handleBulkDelete = () => {
@@ -1797,8 +1793,7 @@ const BulkAssignModal = ({
onAssign, onAssign,
isLoading, isLoading,
}) => { }) => {
const [selectedGroupId, setSelectedGroupId] = useState(""); const [selectedGroupIds, setSelectedGroupIds] = useState([]);
const bulkHostGroupId = useId();
// Fetch host groups for selection // Fetch host groups for selection
const { data: hostGroups } = useQuery({ const { data: hostGroups } = useQuery({
@@ -1812,7 +1807,17 @@ const BulkAssignModal = ({
const handleSubmit = (e) => { const handleSubmit = (e) => {
e.preventDefault(); e.preventDefault();
onAssign(selectedGroupId || null); onAssign(selectedGroupIds);
};
const toggleGroup = (groupId) => {
setSelectedGroupIds((prev) => {
if (prev.includes(groupId)) {
return prev.filter((id) => id !== groupId);
} else {
return [...prev, groupId];
}
});
}; };
return ( return (
@@ -1820,7 +1825,7 @@ const BulkAssignModal = ({
<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md"> <div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-md">
<div className="flex justify-between items-center mb-4"> <div className="flex justify-between items-center mb-4">
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white"> <h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
Assign to Host Group Assign to Host Groups
</h3> </h3>
<button <button
type="button" type="button"
@@ -1850,27 +1855,43 @@ const BulkAssignModal = ({
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label <span className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-3">
htmlFor={bulkHostGroupId} Host Groups
className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1" </span>
> <div className="space-y-2 max-h-48 overflow-y-auto">
Host Group {/* Host Group Options */}
</label>
<select
id={bulkHostGroupId}
value={selectedGroupId}
onChange={(e) => setSelectedGroupId(e.target.value)}
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white focus:outline-none focus:ring-2 focus:ring-primary-500"
>
<option value="">No group (ungrouped)</option>
{hostGroups?.map((group) => ( {hostGroups?.map((group) => (
<option key={group.id} value={group.id}> <label
{group.name} key={group.id}
</option> className={`flex items-center gap-3 p-3 border-2 rounded-lg transition-all duration-200 cursor-pointer ${
selectedGroupIds.includes(group.id)
? "border-primary-500 bg-primary-50 dark:bg-primary-900/30"
: "border-secondary-300 dark:border-secondary-600 bg-white dark:bg-secondary-700 hover:border-secondary-400 dark:hover:border-secondary-500"
}`}
>
<input
type="checkbox"
checked={selectedGroupIds.includes(group.id)}
onChange={() => toggleGroup(group.id)}
className="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
/>
<div className="flex items-center gap-2 flex-1">
{group.color && (
<div
className="w-3 h-3 rounded-full border border-secondary-300 dark:border-secondary-500 flex-shrink-0"
style={{ backgroundColor: group.color }}
></div>
)}
<div className="text-sm font-medium text-secondary-700 dark:text-secondary-200">
{group.name}
</div>
</div>
</label>
))} ))}
</select> </div>
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400"> <p className="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
Select a group to assign these hosts to, or leave ungrouped. Select one or more groups to assign these hosts to, or leave
ungrouped.
</p> </p>
</div> </div>
@@ -1884,7 +1905,7 @@ const BulkAssignModal = ({
Cancel Cancel
</button> </button>
<button type="submit" className="btn-primary" disabled={isLoading}> <button type="submit" className="btn-primary" disabled={isLoading}>
{isLoading ? "Assigning..." : "Assign to Group"} {isLoading ? "Assigning..." : "Assign to Groups"}
</button> </button>
</div> </div>
</form> </form>

View File

@@ -570,6 +570,20 @@ install_postgresql() {
fi fi
} }
# Install Redis
install_redis() {
print_info "Installing Redis..."
if systemctl is-active --quiet redis-server; then
print_status "Redis already running"
else
$PKG_INSTALL redis-server
systemctl start redis-server
systemctl enable redis-server
print_status "Redis installed and started"
fi
}
# Install nginx # Install nginx
install_nginx() { install_nginx() {
print_info "Installing nginx..." print_info "Installing nginx..."
@@ -858,6 +872,12 @@ AUTH_RATE_LIMIT_MAX=500
AGENT_RATE_LIMIT_WINDOW_MS=60000 AGENT_RATE_LIMIT_WINDOW_MS=60000
AGENT_RATE_LIMIT_MAX=1000 AGENT_RATE_LIMIT_MAX=1000
# Redis Configuration
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
# Logging # Logging
LOG_LEVEL=info LOG_LEVEL=info
ENABLE_LOGGING=true ENABLE_LOGGING=true
@@ -1356,6 +1376,12 @@ Database Information:
- Host: localhost - Host: localhost
- Port: 5432 - Port: 5432
Redis Information:
- Host: localhost
- Port: 6379
- Password: (none - Redis runs without authentication)
- Database: 0
Networking: Networking:
- Backend Port: $BACKEND_PORT - Backend Port: $BACKEND_PORT
- Nginx Config: /etc/nginx/sites-available/$FQDN - Nginx Config: /etc/nginx/sites-available/$FQDN
@@ -1516,6 +1542,7 @@ deploy_instance() {
# System setup (prerequisites already installed in interactive_setup) # System setup (prerequisites already installed in interactive_setup)
install_nodejs install_nodejs
install_postgresql install_postgresql
install_redis
install_nginx install_nginx
# Only install certbot if SSL is enabled # Only install certbot if SSL is enabled