fix(observability): router conflict, logout button, missing i18n
- Fix chi duplicate Route() panic by consolidating read/write routes into single Route blocks with nested admin Group - Add logout button to sidebar with token cleanup - Add missing settingsAuth.password i18n key
This commit is contained in:
+61
-53
@@ -138,55 +138,10 @@ func (s *Server) Router() chi.Router {
|
||||
r.Get("/stages/{stage}/instances", s.listInstances)
|
||||
r.Get("/stages/{stage}/instances/{iid}/stats", s.getInstanceStats)
|
||||
r.Get("/volumes", s.listVolumes)
|
||||
})
|
||||
r.Get("/deploys", s.listDeploys)
|
||||
r.Get("/deploys/{id}/logs", s.streamDeployLogs)
|
||||
r.Get("/events", s.streamEvents)
|
||||
r.Get("/events/log", s.listEventLog)
|
||||
r.Get("/events/log/stats", s.getEventLogStats)
|
||||
r.Get("/registries", s.listRegistries)
|
||||
r.Route("/registries/{id}", func(r chi.Router) {
|
||||
r.Get("/tags/*", s.listRegistryTags)
|
||||
r.Get("/images", s.listRegistryImages)
|
||||
})
|
||||
r.Get("/settings", s.getSettings)
|
||||
r.Get("/settings/npm-certificates", s.listNpmCertificates)
|
||||
|
||||
// Stale container endpoints.
|
||||
r.Get("/containers/stale", s.listStaleContainers)
|
||||
|
||||
// Proxy endpoints (read-only for any authenticated user).
|
||||
r.Get("/proxies", s.listProxies)
|
||||
r.Get("/proxies/all", s.listAllProxies)
|
||||
r.Route("/proxies/{id}", func(r chi.Router) {
|
||||
r.Get("/", s.getProxy)
|
||||
})
|
||||
|
||||
// Admin-only routes: require admin role.
|
||||
// Admin-only project mutations.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.AdminOnly)
|
||||
|
||||
// Proxy mutation endpoints.
|
||||
r.Post("/proxies/validate", s.validateProxy)
|
||||
r.Post("/proxies", s.createProxy)
|
||||
r.Route("/proxies/{id}", func(r chi.Router) {
|
||||
r.Put("/", s.updateProxy)
|
||||
r.Delete("/", s.deleteProxy)
|
||||
})
|
||||
|
||||
// Config export (reveals project/infra details).
|
||||
r.Get("/config/export", s.exportConfig)
|
||||
|
||||
// Auth management.
|
||||
r.Get("/auth/settings", s.getAuthSettings)
|
||||
r.Put("/auth/settings", s.updateAuthSettings)
|
||||
r.Get("/auth/users", s.listUsers)
|
||||
r.Post("/auth/users", s.createUser)
|
||||
r.Delete("/auth/users/{uid}", s.deleteUser)
|
||||
|
||||
// Project mutation endpoints.
|
||||
r.Post("/projects", s.createProject)
|
||||
r.Route("/projects/{id}", func(r chi.Router) {
|
||||
r.Put("/", s.updateProject)
|
||||
r.Delete("/", s.deleteProject)
|
||||
|
||||
@@ -214,20 +169,73 @@ func (s *Server) Router() chi.Router {
|
||||
r.Put("/volumes/{volId}", s.updateVolume)
|
||||
r.Delete("/volumes/{volId}", s.deleteVolume)
|
||||
})
|
||||
})
|
||||
r.Get("/deploys", s.listDeploys)
|
||||
r.Get("/deploys/{id}/logs", s.streamDeployLogs)
|
||||
r.Get("/events", s.streamEvents)
|
||||
r.Get("/events/log", s.listEventLog)
|
||||
r.Get("/events/log/stats", s.getEventLogStats)
|
||||
r.Get("/registries", s.listRegistries)
|
||||
r.Route("/registries/{id}", func(r chi.Router) {
|
||||
r.Get("/tags/*", s.listRegistryTags)
|
||||
r.Get("/images", s.listRegistryImages)
|
||||
|
||||
// Admin-only registry mutations.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.AdminOnly)
|
||||
r.Put("/", s.updateRegistry)
|
||||
r.Delete("/", s.deleteRegistry)
|
||||
r.Post("/test", s.testRegistry)
|
||||
})
|
||||
})
|
||||
r.Get("/settings", s.getSettings)
|
||||
r.Get("/settings/npm-certificates", s.listNpmCertificates)
|
||||
|
||||
// Stale container endpoints (read).
|
||||
r.Get("/containers/stale", s.listStaleContainers)
|
||||
|
||||
// Proxy endpoints (read-only for any authenticated user).
|
||||
r.Get("/proxies", s.listProxies)
|
||||
r.Get("/proxies/all", s.listAllProxies)
|
||||
r.Route("/proxies/{id}", func(r chi.Router) {
|
||||
r.Get("/", s.getProxy)
|
||||
// Admin-only proxy mutations.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.AdminOnly)
|
||||
r.Put("/", s.updateProxy)
|
||||
r.Delete("/", s.deleteProxy)
|
||||
})
|
||||
})
|
||||
|
||||
// Admin-only routes: require admin role.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(auth.AdminOnly)
|
||||
|
||||
// Config export (reveals project/infra details).
|
||||
r.Get("/config/export", s.exportConfig)
|
||||
|
||||
// Auth management.
|
||||
r.Get("/auth/settings", s.getAuthSettings)
|
||||
r.Put("/auth/settings", s.updateAuthSettings)
|
||||
r.Get("/auth/users", s.listUsers)
|
||||
r.Post("/auth/users", s.createUser)
|
||||
r.Delete("/auth/users/{uid}", s.deleteUser)
|
||||
|
||||
// Project creation.
|
||||
r.Post("/projects", s.createProject)
|
||||
|
||||
// Quick deploy endpoints.
|
||||
r.Post("/deploy/inspect", s.inspectImage)
|
||||
r.Post("/deploy/quick", s.quickDeploy)
|
||||
|
||||
// Registry mutation endpoints.
|
||||
// Registry creation.
|
||||
r.Post("/registries", s.createRegistry)
|
||||
r.Route("/registries/{id}", func(r chi.Router) {
|
||||
r.Put("/", s.updateRegistry)
|
||||
r.Delete("/", s.deleteRegistry)
|
||||
r.Post("/test", s.testRegistry)
|
||||
})
|
||||
|
||||
// Stale container cleanup endpoints (admin-only).
|
||||
// Proxy mutation endpoints.
|
||||
r.Post("/proxies/validate", s.validateProxy)
|
||||
r.Post("/proxies", s.createProxy)
|
||||
|
||||
// Stale container cleanup endpoints.
|
||||
// Bulk route must be registered before parameterized route.
|
||||
r.Post("/containers/stale/cleanup", s.bulkCleanupStaleContainers)
|
||||
r.Post("/containers/stale/{id}/cleanup", s.cleanupStaleContainer)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
interface Props { size?: number; class?: string; }
|
||||
const { size = 24, class: className = '' }: Props = $props();
|
||||
</script>
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={className}>
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
|
||||
<polyline points="16 17 21 12 16 7" />
|
||||
<line x1="21" y1="12" x2="9" y2="12" />
|
||||
</svg>
|
||||
@@ -47,3 +47,4 @@ export { default as IconWifi } from './IconWifi.svelte';
|
||||
export { default as IconRefresh } from './IconRefresh.svelte';
|
||||
export { default as IconProxies } from './IconProxies.svelte';
|
||||
export { default as IconEvents } from './IconEvents.svelte';
|
||||
export { default as IconLogout } from './IconLogout.svelte';
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"deploy": "Deploy",
|
||||
"proxies": "Proxies",
|
||||
"events": "Events",
|
||||
"settings": "Settings"
|
||||
"settings": "Settings",
|
||||
"logout": "Log out"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
@@ -311,7 +312,8 @@
|
||||
"createFailed": "Failed to create user",
|
||||
"deleteFailed": "Failed to delete user",
|
||||
"deleteConfirm": "Are you sure you want to delete this user?",
|
||||
"usernameRequired": "Username and password are required"
|
||||
"usernameRequired": "Username and password are required",
|
||||
"password": "Password"
|
||||
},
|
||||
"login": {
|
||||
"title": "Docker Watcher",
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
"deploy": "Деплой",
|
||||
"proxies": "Прокси",
|
||||
"events": "События",
|
||||
"settings": "Настройки"
|
||||
"settings": "Настройки",
|
||||
"logout": "Выйти"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Панель управления",
|
||||
@@ -311,7 +312,8 @@
|
||||
"createFailed": "Не удалось создать пользователя",
|
||||
"deleteFailed": "Не удалось удалить пользователя",
|
||||
"deleteConfirm": "Вы уверены, что хотите удалить этого пользователя?",
|
||||
"usernameRequired": "Имя пользователя и пароль обязательны"
|
||||
"usernameRequired": "Имя пользователя и пароль обязательны",
|
||||
"password": "Пароль"
|
||||
},
|
||||
"login": {
|
||||
"title": "Docker Watcher",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||
import LocaleSwitcher from '$lib/components/LocaleSwitcher.svelte';
|
||||
import { IconDashboard, IconProjects, IconDeploy, IconProxies, IconEvents, IconSettings, IconMenu, IconX } from '$lib/components/icons';
|
||||
import { IconDashboard, IconProjects, IconDeploy, IconProxies, IconEvents, IconSettings, IconMenu, IconX, IconLogout } from '$lib/components/icons';
|
||||
import { connectGlobalEvents, type SSEConnection } from '$lib/sse';
|
||||
import { instanceStatusStore } from '$lib/stores/instance-status';
|
||||
import { resolvedTheme, applyTheme } from '$lib/stores/theme';
|
||||
@@ -58,6 +58,15 @@
|
||||
sidebarOpen = false;
|
||||
});
|
||||
|
||||
function logout() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem('auth_token');
|
||||
}
|
||||
sseConnection?.close();
|
||||
sseConnection = null;
|
||||
window.location.href = '/login';
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
sseConnection = connectGlobalEvents({
|
||||
onInstanceStatus(payload) {
|
||||
@@ -151,7 +160,17 @@
|
||||
<ThemeToggle />
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs text-[var(--text-tertiary)]">{$t('app.name')} {$t('app.version')}</p>
|
||||
<button
|
||||
onclick={logout}
|
||||
class="inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-[var(--text-secondary)] transition-colors hover:bg-[var(--surface-card-hover)] hover:text-[var(--color-danger)]"
|
||||
title={$t('nav.logout')}
|
||||
>
|
||||
<IconLogout size={14} />
|
||||
{$t('nav.logout')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user