diff --git a/internal/api/router.go b/internal/api/router.go index 91295d9..c1f01e4 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -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. - 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) { + // Admin-only project mutations. + r.Group(func(r chi.Router) { + r.Use(auth.AdminOnly) 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) diff --git a/web/src/lib/components/icons/IconLogout.svelte b/web/src/lib/components/icons/IconLogout.svelte new file mode 100644 index 0000000..b486ade --- /dev/null +++ b/web/src/lib/components/icons/IconLogout.svelte @@ -0,0 +1,9 @@ + + + + + + diff --git a/web/src/lib/components/icons/index.ts b/web/src/lib/components/icons/index.ts index 049c76b..607706c 100644 --- a/web/src/lib/components/icons/index.ts +++ b/web/src/lib/components/icons/index.ts @@ -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'; diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 5cfab28..aa5f03e 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -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", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 0747e70..6431b55 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -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", diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index ad1f695..4aa25d8 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -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 @@ -

{$t('app.name')} {$t('app.version')}

+
+

{$t('app.name')} {$t('app.version')}

+ +