mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
Merge pull request #180 from PatchMon/feature/go-agent
fixing redis environment issue and some UI fixes
This commit is contained in:
@@ -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)
|
||||
router.put(
|
||||
"/:hostId/groups",
|
||||
|
@@ -39,15 +39,15 @@ These tags are available for both backend and frontend images as they are versio
|
||||
environment:
|
||||
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
|
||||
environment:
|
||||
REDIS_PASSWORD: # CREATE A STRONG REDIS PASSWORD AND PUT IT HERE
|
||||
command: redis-server --requirepass your-redis-password-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:
|
||||
```yaml
|
||||
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:
|
||||
```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!** |
|
||||
|
||||
> [!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
|
||||
|
||||
##### Database Configuration
|
||||
|
@@ -19,14 +19,11 @@ services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
command: redis-server /usr/local/etc/redis/redis.conf
|
||||
environment:
|
||||
REDIS_PASSWORD: # CREATE A STRONG REDIS PASSWORD AND PUT IT HERE
|
||||
command: redis-server --requirepass your-redis-password-here
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
- ./docker/redis.conf:/usr/local/etc/redis/redis.conf:ro
|
||||
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
|
||||
timeout: 5s
|
||||
retries: 7
|
||||
@@ -46,7 +43,7 @@ services:
|
||||
# Redis Configuration
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
REDIS_PASSWORD: REPLACE_YOUR_REDIS_PASSWORD_HERE
|
||||
REDIS_PASSWORD: your-redis-password-here
|
||||
REDIS_DB: 0
|
||||
volumes:
|
||||
- agent_files:/app/agents
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# Redis Configuration for PatchMon Production
|
||||
# Security settings
|
||||
requirepass ${REDIS_PASSWORD}
|
||||
# requirepass ${REDIS_PASSWORD} # Disabled - using command-line password instead
|
||||
rename-command FLUSHDB ""
|
||||
rename-command FLUSHALL ""
|
||||
rename-command DEBUG ""
|
||||
rename-command CONFIG "CONFIG_${REDIS_PASSWORD}"
|
||||
rename-command CONFIG "CONFIG_DISABLED"
|
||||
|
||||
# Memory management
|
||||
maxmemory 256mb
|
||||
@@ -28,7 +28,7 @@ tcp-keepalive 300
|
||||
timeout 0
|
||||
|
||||
# Disable dangerous commands
|
||||
rename-command SHUTDOWN "SHUTDOWN_${REDIS_PASSWORD}"
|
||||
rename-command SHUTDOWN "SHUTDOWN_DISABLED"
|
||||
rename-command KEYS ""
|
||||
rename-command MONITOR ""
|
||||
rename-command SLAVEOF ""
|
||||
|
@@ -505,8 +505,8 @@ const Hosts = () => {
|
||||
}, [hosts]);
|
||||
|
||||
const bulkUpdateGroupMutation = useMutation({
|
||||
mutationFn: ({ hostIds, hostGroupId }) =>
|
||||
adminHostsAPI.bulkUpdateGroup(hostIds, hostGroupId),
|
||||
mutationFn: ({ hostIds, groupIds }) =>
|
||||
adminHostsAPI.bulkUpdateGroups(hostIds, groupIds),
|
||||
onSuccess: (data) => {
|
||||
console.log("bulkUpdateGroupMutation success:", data);
|
||||
|
||||
@@ -517,11 +517,7 @@ const Hosts = () => {
|
||||
return oldData.map((host) => {
|
||||
const updatedHost = data.hosts.find((h) => h.id === host.id);
|
||||
if (updatedHost) {
|
||||
// Ensure hostGroupId is set correctly
|
||||
return {
|
||||
...updatedHost,
|
||||
hostGroupId: updatedHost.host_groups?.id || null,
|
||||
};
|
||||
return updatedHost;
|
||||
}
|
||||
return host;
|
||||
});
|
||||
@@ -671,8 +667,8 @@ const Hosts = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleBulkAssign = (hostGroupId) => {
|
||||
bulkUpdateGroupMutation.mutate({ hostIds: selectedHosts, hostGroupId });
|
||||
const handleBulkAssign = (groupIds) => {
|
||||
bulkUpdateGroupMutation.mutate({ hostIds: selectedHosts, groupIds });
|
||||
};
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
@@ -1797,8 +1793,7 @@ const BulkAssignModal = ({
|
||||
onAssign,
|
||||
isLoading,
|
||||
}) => {
|
||||
const [selectedGroupId, setSelectedGroupId] = useState("");
|
||||
const bulkHostGroupId = useId();
|
||||
const [selectedGroupIds, setSelectedGroupIds] = useState([]);
|
||||
|
||||
// Fetch host groups for selection
|
||||
const { data: hostGroups } = useQuery({
|
||||
@@ -1812,7 +1807,17 @@ const BulkAssignModal = ({
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
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 (
|
||||
@@ -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="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-secondary-900 dark:text-white">
|
||||
Assign to Host Group
|
||||
Assign to Host Groups
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
@@ -1850,27 +1855,43 @@ const BulkAssignModal = ({
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor={bulkHostGroupId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1"
|
||||
>
|
||||
Host Group
|
||||
</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>
|
||||
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-3">
|
||||
Host Groups
|
||||
</span>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto">
|
||||
{/* Host Group Options */}
|
||||
{hostGroups?.map((group) => (
|
||||
<option key={group.id} value={group.id}>
|
||||
{group.name}
|
||||
</option>
|
||||
<label
|
||||
key={group.id}
|
||||
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>
|
||||
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
Select a group to assign these hosts to, or leave ungrouped.
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
Select one or more groups to assign these hosts to, or leave
|
||||
ungrouped.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1884,7 +1905,7 @@ const BulkAssignModal = ({
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn-primary" disabled={isLoading}>
|
||||
{isLoading ? "Assigning..." : "Assign to Group"}
|
||||
{isLoading ? "Assigning..." : "Assign to Groups"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
27
setup.sh
27
setup.sh
@@ -570,6 +570,20 @@ install_postgresql() {
|
||||
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() {
|
||||
print_info "Installing nginx..."
|
||||
@@ -858,6 +872,12 @@ AUTH_RATE_LIMIT_MAX=500
|
||||
AGENT_RATE_LIMIT_WINDOW_MS=60000
|
||||
AGENT_RATE_LIMIT_MAX=1000
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=
|
||||
REDIS_DB=0
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
ENABLE_LOGGING=true
|
||||
@@ -1356,6 +1376,12 @@ Database Information:
|
||||
- Host: localhost
|
||||
- Port: 5432
|
||||
|
||||
Redis Information:
|
||||
- Host: localhost
|
||||
- Port: 6379
|
||||
- Password: (none - Redis runs without authentication)
|
||||
- Database: 0
|
||||
|
||||
Networking:
|
||||
- Backend Port: $BACKEND_PORT
|
||||
- Nginx Config: /etc/nginx/sites-available/$FQDN
|
||||
@@ -1516,6 +1542,7 @@ deploy_instance() {
|
||||
# System setup (prerequisites already installed in interactive_setup)
|
||||
install_nodejs
|
||||
install_postgresql
|
||||
install_redis
|
||||
install_nginx
|
||||
|
||||
# Only install certbot if SSL is enabled
|
||||
|
Reference in New Issue
Block a user