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')}
+ +