fix: resolve Docker socket permissions and notification page routing

- Add runtime Docker socket permission detection in entrypoint
  - Detects socket GID and logs helpful deployment guidance
  - Provides three resolution options (root user, group-add, or chmod)
  - Non-intrusive: logs only, doesn't modify permissions

- Fix notification page routing mismatch
  - Move notifications route from /notifications to /settings/notifications
  - Add notifications tab to Settings page with Bell icon
  - Align navigation structure with route definitions

- Enhance Docker API error handling
  - Return 503 (not 500) when Docker daemon unavailable
  - Add DockerUnavailableError type for clear error distinction
  - Implement SSRF hardening (reject arbitrary host values)

- Improve security and testability
  - Move ProxyHost routes to protected auth group
  - Refactor Docker handler tests to use mocks
  - Simplify useDocker hook query enablement logic

Docker socket fix addresses deployment-level permission issue without
code changes. The 503 error correctly signals service unavailability
due to configuration, not application bugs.

Closes #XX (if applicable)
This commit is contained in:
GitHub Actions
2025-12-22 21:58:20 +00:00
parent ffa74d0968
commit baf822e084
14 changed files with 938 additions and 153 deletions
+1 -1
View File
@@ -67,7 +67,6 @@ export default function App() {
<Route path="security/headers" element={<SecurityHeaders />} />
<Route path="access-lists" element={<AccessLists />} />
<Route path="uptime" element={<Uptime />} />
<Route path="notifications" element={<Notifications />} />
<Route path="users" element={<UsersPage />} />
<Route path="import" element={<Navigate to="/tasks/import/caddyfile" replace />} />
@@ -75,6 +74,7 @@ export default function App() {
<Route path="settings" element={<Settings />}>
<Route index element={<SystemSettings />} />
<Route path="system" element={<SystemSettings />} />
<Route path="notifications" element={<Notifications />} />
<Route path="smtp" element={<SMTPSettings />} />
<Route path="crowdsec" element={<Navigate to="/security/crowdsec" replace />} />
<Route path="account" element={<Account />} />
+1 -3
View File
@@ -72,14 +72,13 @@ export default function Layout({ children }: LayoutProps) {
{ name: t('navigation.waf'), path: '/security/waf', icon: '🛡️' },
{ name: t('navigation.securityHeaders'), path: '/security/headers', icon: '🔐' },
]},
{ name: t('navigation.notifications'), path: '/notifications', icon: '🔔' },
// Import group moved under Tasks
{
name: t('navigation.settings'),
path: '/settings',
icon: '⚙️',
children: [
{ name: t('navigation.system'), path: '/settings/system', icon: '⚙️' },
{ name: t('navigation.notifications'), path: '/settings/notifications', icon: '🔔' },
{ name: t('navigation.email'), path: '/settings/smtp', icon: '📧' },
{ name: t('navigation.adminAccount'), path: '/settings/account', icon: '🛡️' },
{ name: t('navigation.accountManagement'), path: '/settings/account-management', icon: '👥' },
@@ -93,7 +92,6 @@ export default function Layout({ children }: LayoutProps) {
{
name: t('navigation.import'),
path: '/tasks/import',
icon: '📥',
children: [
{ name: t('navigation.caddyfile'), path: '/tasks/import/caddyfile', icon: '📥' },
{ name: t('navigation.crowdsec'), path: '/tasks/import/crowdsec', icon: '🛡️' },
@@ -81,6 +81,15 @@ describe('useDocker', () => {
expect(result.current.containers).toEqual([]);
});
it('does not fetch when both host and serverId are undefined', async () => {
const { result } = renderHook(() => useDocker(undefined, undefined), {
wrapper: createWrapper(),
});
expect(dockerApi.listContainers).not.toHaveBeenCalled();
expect(result.current.containers).toEqual([]);
});
it('returns empty array as default when no data', async () => {
vi.mocked(dockerApi.listContainers).mockResolvedValue([]);
+1 -1
View File
@@ -10,7 +10,7 @@ export function useDocker(host?: string | null, serverId?: string | null) {
} = useQuery({
queryKey: ['docker-containers', host, serverId],
queryFn: () => dockerApi.listContainers(host || undefined, serverId || undefined),
enabled: host !== null || serverId !== null, // Disable if both are explicitly null/undefined
enabled: Boolean(host) || Boolean(serverId),
retry: 1, // Don't retry too much if docker is not available
})
+2 -1
View File
@@ -2,7 +2,7 @@ import { Link, Outlet, useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { PageShell } from '../components/layout/PageShell'
import { cn } from '../utils/cn'
import { Settings as SettingsIcon, Server, Mail, User } from 'lucide-react'
import { Settings as SettingsIcon, Server, Mail, User, Bell } from 'lucide-react'
export default function Settings() {
const { t } = useTranslation()
@@ -12,6 +12,7 @@ export default function Settings() {
const navItems = [
{ path: '/settings/system', label: t('settings.system'), icon: Server },
{ path: '/settings/notifications', label: t('settings.notifications'), icon: Bell },
{ path: '/settings/smtp', label: t('settings.smtp'), icon: Mail },
{ path: '/settings/account', label: t('settings.account'), icon: User },
]