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:
@@ -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 />} />
|
||||
|
||||
@@ -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([]);
|
||||
|
||||
|
||||
@@ -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,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 },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user