Phase 4 — P4-03/04: RBAC + user management #14
+133
-216
@@ -85,11 +85,6 @@ func New(deps Deps) *Server {
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(requestLogger)
|
||||
|
||||
// Health endpoint — unauthenticated, no audit, deliberately cheap.
|
||||
r.Get("/healthz", func(w stdhttp.ResponseWriter, _ *stdhttp.Request) {
|
||||
w.WriteHeader(stdhttp.StatusNoContent)
|
||||
})
|
||||
|
||||
s := &Server{
|
||||
deps: deps,
|
||||
drainLocks: make(map[string]*sync.Mutex),
|
||||
@@ -113,132 +108,17 @@ func New(deps Deps) *Server {
|
||||
// routes wires the API tree. Subtrees live in this file by area so a
|
||||
// reader can scan one place and see the surface.
|
||||
func (s *Server) routes(r chi.Router) {
|
||||
r.Route("/api", func(r chi.Router) {
|
||||
r.Post("/auth/login", s.handleLogin)
|
||||
r.Post("/auth/logout", s.handleLogout)
|
||||
r.Post("/bootstrap", s.handleBootstrap)
|
||||
|
||||
// Agent enrollment (open endpoint — token is the credential).
|
||||
r.Post("/agents/enroll", s.handleAgentEnroll)
|
||||
|
||||
// Announce-and-approve enrolment (open endpoint — fingerprint
|
||||
// comparison in the UI is the gate). Per-IP rate-limited and
|
||||
// globally capped (P2-18).
|
||||
r.Post("/agents/announce", s.handleAnnounce)
|
||||
|
||||
// Pending host management — admin-only (gated inside the handler).
|
||||
r.Post("/pending-hosts/{id}/accept", s.handleAcceptPendingHost)
|
||||
r.Post("/pending-hosts/{id}/reject", s.handleRejectPendingHost)
|
||||
|
||||
// Operator → server (authenticated). Spec.md §6.1's
|
||||
// /hosts/{id}/enrollment-token (regenerate) lands when the
|
||||
// host page can call it; for now just the create endpoint.
|
||||
r.Post("/enrollment-tokens", s.handleCreateEnrollmentToken)
|
||||
|
||||
// Fleet read endpoints — back the dashboard.
|
||||
r.Get("/hosts", s.handleListHosts)
|
||||
r.Get("/fleet/summary", s.handleFleetSummary)
|
||||
|
||||
// Run-now: dispatch a job to a host's agent.
|
||||
r.Post("/hosts/{id}/jobs", s.handleRunNow)
|
||||
|
||||
// Snapshot projection (refreshed by the agent after each backup).
|
||||
r.Get("/hosts/{id}/snapshots", s.handleListHostSnapshots)
|
||||
|
||||
// Repo credentials — operator can edit after enrollment. The
|
||||
// initial set is supplied at token-mint time (see enrollment.go).
|
||||
// GET returns a redacted view (URL, username, has_password).
|
||||
r.Get("/hosts/{id}/repo-credentials", s.handleGetHostCredentials)
|
||||
r.Put("/hosts/{id}/repo-credentials", s.handleSetHostCredentials)
|
||||
|
||||
// Admin credentials — the prune-capable slot (separate from the
|
||||
// everyday repo creds). Optional: hosts that don't prune against
|
||||
// a rest-server repo with a separate admin user never need this.
|
||||
r.Get("/hosts/{id}/admin-credentials", s.handleGetAdminCredentials)
|
||||
r.Put("/hosts/{id}/admin-credentials", s.handleSetAdminCredentials)
|
||||
r.Delete("/hosts/{id}/admin-credentials", s.handleDeleteAdminCredentials)
|
||||
|
||||
// Per-host schedule CRUD. Mutations bump host_schedule_version
|
||||
// and async-push to a connected agent (see schedule_push.go).
|
||||
r.Get("/hosts/{id}/schedules", s.handleListSchedules)
|
||||
r.Post("/hosts/{id}/schedules", s.handleCreateSchedule)
|
||||
r.Put("/hosts/{id}/schedules/{sid}", s.handleUpdateSchedule)
|
||||
r.Delete("/hosts/{id}/schedules/{sid}", s.handleDeleteSchedule)
|
||||
|
||||
// Source-group CRUD. A group is "what gets backed up" — paths,
|
||||
// excludes, retention, retry. Group name doubles as the
|
||||
// snapshot tag (restic --tag <name>).
|
||||
r.Get("/hosts/{id}/source-groups", s.handleListSourceGroups)
|
||||
r.Post("/hosts/{id}/source-groups", s.handleCreateSourceGroup)
|
||||
r.Get("/hosts/{id}/source-groups/{gid}", s.handleGetSourceGroup)
|
||||
r.Put("/hosts/{id}/source-groups/{gid}", s.handleUpdateSourceGroup)
|
||||
r.Delete("/hosts/{id}/source-groups/{gid}", s.handleDeleteSourceGroup)
|
||||
|
||||
// Repo maintenance cadences (forget / prune / check). Driven
|
||||
// by the server-side ticker (P2R-06), not the agent's cron.
|
||||
r.Get("/hosts/{id}/repo-maintenance", s.handleGetRepoMaintenance)
|
||||
r.Put("/hosts/{id}/repo-maintenance", s.handleUpdateRepoMaintenance)
|
||||
|
||||
// Host-wide bandwidth caps (host.bandwidth_up_kbps /
|
||||
// bandwidth_down_kbps). Apply to every restic invocation.
|
||||
r.Put("/hosts/{id}/bandwidth", s.handleUpdateHostBandwidth)
|
||||
|
||||
// Per-source-group Run-now (JSON variant). HTMX action is
|
||||
// mounted at the equivalent path outside /api below — both
|
||||
// resolve to the same handler, which sniffs HX-Request.
|
||||
r.Post("/hosts/{id}/source-groups/{gid}/run", s.handleRunSourceGroup)
|
||||
|
||||
// Repo-level run-now: prune (needs admin creds), check, unlock.
|
||||
// HTMX forms are also mounted outside /api below.
|
||||
r.Post("/hosts/{id}/repo/prune", s.handleRunRepoPrune)
|
||||
r.Post("/hosts/{id}/repo/check", s.handleRunRepoCheck)
|
||||
r.Post("/hosts/{id}/repo/unlock", s.handleRunRepoUnlock)
|
||||
|
||||
// Cancel a running job. Operator-driven, sends command.cancel
|
||||
// to the agent which kills the restic subprocess; the agent's
|
||||
// resulting job.finished (status=canceled) is what flips the
|
||||
// job row.
|
||||
r.Post("/jobs/{id}/cancel", s.handleCancelJob)
|
||||
|
||||
// Snapshot diff (P3-09). Dispatches a JobDiff against two
|
||||
// snapshots; output streams to the standard live job page.
|
||||
r.Post("/hosts/{id}/snapshots/diff", s.handleSnapshotDiff)
|
||||
|
||||
// Alert list (JSON variant). Same filter shape as the UI page.
|
||||
r.Get("/alerts", s.handleAPIAlerts)
|
||||
|
||||
// Audit log (JSON variant).
|
||||
r.Get("/audit", s.handleAPIAudit)
|
||||
|
||||
// Notification channel test-fire. Dispatches a synthetic payload
|
||||
// through a single named channel; returns JSON result.
|
||||
r.Post("/notifications/{id}/test", s.handleAPINotificationTest)
|
||||
// Public, unauthenticated.
|
||||
r.Get("/healthz", func(w stdhttp.ResponseWriter, _ *stdhttp.Request) {
|
||||
w.WriteHeader(stdhttp.StatusNoContent)
|
||||
})
|
||||
|
||||
// HTMX form variant of diff (mounted outside /api so HTMX forms
|
||||
// can post against it without the api/ prefix).
|
||||
r.Post("/hosts/{id}/snapshots/diff", s.handleSnapshotDiff)
|
||||
|
||||
// Per-source-group Run-now (HTMX form action). Available even
|
||||
// when the server is started without UI templates so REST callers
|
||||
// against the non-/api path also work.
|
||||
r.Post("/hosts/{id}/source-groups/{gid}/run", s.handleRunSourceGroup)
|
||||
// Repo-level run-now (HTMX form actions). Same handlers as the /api
|
||||
// variants — wantsHTML sniff distinguishes JSON vs HTMX response.
|
||||
r.Post("/hosts/{id}/repo/prune", s.handleRunRepoPrune)
|
||||
r.Post("/hosts/{id}/repo/check", s.handleRunRepoCheck)
|
||||
r.Post("/hosts/{id}/repo/unlock", s.handleRunRepoUnlock)
|
||||
// Retired routes — see ui_handlers.go for the messages. Mounted
|
||||
// outside the UI gate so cached browser tabs get a clear 410
|
||||
// even if the server runs without templates.
|
||||
r.Post("/hosts/{id}/run-backup", s.handleUIRunBackupGone)
|
||||
r.Post("/hosts/{id}/init-repo", s.handleUIInitRepoGone)
|
||||
|
||||
// Pending-host WebSocket (announce-and-approve, P2-18b). Mounted
|
||||
// before /ws/agent so the more-specific route matches first.
|
||||
r.Get("/ws/agent/pending", s.handlePendingWS)
|
||||
|
||||
// Agent ↔ server WebSocket. Bearer-authenticated inside the handler.
|
||||
r.Post("/api/auth/login", s.handleLogin)
|
||||
r.Post("/api/auth/logout", s.handleLogout)
|
||||
r.Post("/api/bootstrap", s.handleBootstrap)
|
||||
r.Post("/api/agents/enroll", s.handleAgentEnroll)
|
||||
r.Post("/api/agents/announce", s.handleAnnounce)
|
||||
r.Get("/agent/binary", s.handleAgentBinary)
|
||||
r.Get("/install/*", s.handleInstallAsset)
|
||||
if s.deps.Hub != nil {
|
||||
r.Mount("/ws/agent", ws.AgentHandler(ws.HandlerDeps{
|
||||
Hub: s.deps.Hub,
|
||||
@@ -250,101 +130,138 @@ func (s *Server) routes(r chi.Router) {
|
||||
OnScheduleFire: s.dispatchScheduledJob,
|
||||
}))
|
||||
}
|
||||
|
||||
// Agent binaries + install scripts. Open endpoints — content is
|
||||
// unprivileged on its own, gating happens via the enrollment
|
||||
// token. See agent_assets.go.
|
||||
r.Get("/agent/binary", s.handleAgentBinary)
|
||||
r.Get("/install/*", s.handleInstallAsset)
|
||||
|
||||
// Static assets (Tailwind CSS bundle, future favicon).
|
||||
r.Get("/ws/agent/pending", s.handlePendingWS)
|
||||
r.Mount("/static/", staticHandler())
|
||||
|
||||
// HTML UI. The renderer is required — fail loud if the binary
|
||||
// was built without templates (impossible in practice given
|
||||
// embed, but guards bad test wiring).
|
||||
if s.deps.UI != nil {
|
||||
r.Get("/", s.handleUIDashboard)
|
||||
r.Get("/login", s.handleUILoginGet)
|
||||
r.Post("/login", s.handleUILoginPost)
|
||||
r.Post("/logout", s.handleUILogoutPost)
|
||||
// Per-host Run-now and manual Init-repo are mounted at the
|
||||
// outer router (so they reply 410 even without UI). Per-
|
||||
// source-group Run-now lives there too — same reason.
|
||||
// Add host flow.
|
||||
r.Get("/hosts/new", s.handleUIAddHostGet)
|
||||
r.Post("/hosts/new", s.handleUIAddHostPost)
|
||||
// Durable post-Add-host page (operator can refresh / come
|
||||
// back; password decrypted from the token row each render).
|
||||
// Polled fragment under /awaiting flips to "connected" once
|
||||
// the agent enrols.
|
||||
r.Get("/hosts/pending/{token}", s.handleUIPendingHost)
|
||||
r.Get("/hosts/pending/{token}/awaiting", s.handleUIPendingAwaiting)
|
||||
// Host detail (Snapshots tab is the default).
|
||||
r.Get("/hosts/{id}", s.handleUIHostDetail)
|
||||
// Sources tab + source-group CRUD forms.
|
||||
r.Get("/hosts/{id}/sources", s.handleUIHostSources)
|
||||
r.Get("/hosts/{id}/sources/new", s.handleUISourceGroupNewGet)
|
||||
r.Post("/hosts/{id}/sources/new", s.handleUISourceGroupSave)
|
||||
r.Get("/hosts/{id}/sources/{gid}/edit", s.handleUISourceGroupEditGet)
|
||||
r.Post("/hosts/{id}/sources/{gid}/edit", s.handleUISourceGroupSave)
|
||||
r.Post("/hosts/{id}/sources/{gid}/delete", s.handleUISourceGroupDelete)
|
||||
// Repo tab — connection / bandwidth / maintenance. Three
|
||||
// independent forms so saving one doesn't touch the others.
|
||||
r.Get("/hosts/{id}/repo", s.handleUIHostRepo)
|
||||
r.Post("/hosts/{id}/repo/credentials", s.handleUIRepoCredentialsSave)
|
||||
r.Post("/hosts/{id}/repo/bandwidth", s.handleUIRepoBandwidthSave)
|
||||
r.Post("/hosts/{id}/repo/maintenance", s.handleUIRepoMaintenanceSave)
|
||||
r.Post("/hosts/{id}/repo/reinit", s.handleUIRepoReinit)
|
||||
r.Post("/hosts/{id}/repo/hooks", s.handleUIRepoHooksSave)
|
||||
// Admin credentials form (separate slot for prune-capable user).
|
||||
r.Post("/hosts/{id}/admin-credentials", s.handleUIAdminCredentialsSave)
|
||||
r.Post("/hosts/{id}/admin-credentials/delete", s.handleUIAdminCredentialsDelete)
|
||||
// Schedules tab + create/edit/delete forms.
|
||||
r.Get("/hosts/{id}/schedules", s.handleUISchedulesList)
|
||||
r.Get("/hosts/{id}/schedules/new", s.handleUIScheduleNewGet)
|
||||
r.Post("/hosts/{id}/schedules/new", s.handleUIScheduleSave)
|
||||
r.Get("/hosts/{id}/schedules/{sid}/edit", s.handleUIScheduleEditGet)
|
||||
r.Post("/hosts/{id}/schedules/{sid}/edit", s.handleUIScheduleSave)
|
||||
r.Post("/hosts/{id}/schedules/{sid}/delete", s.handleUIScheduleDelete)
|
||||
r.Post("/hosts/{id}/schedules/{sid}/run", s.handleUIScheduleRun)
|
||||
// Live job log.
|
||||
r.Get("/jobs/{id}", s.handleUIJobDetail)
|
||||
// Restore wizard (P3-01/P3-02). Two GET variants land on the
|
||||
// same handler; the second deep-links a chosen snapshot.
|
||||
r.Get("/hosts/{id}/restore", s.handleUIRestoreGet)
|
||||
r.Get("/hosts/{id}/snapshots/{sid}/restore", s.handleUIRestoreGet)
|
||||
r.Post("/hosts/{id}/restore", s.handleUIRestorePost)
|
||||
r.Get("/hosts/{id}/restore/tree", s.handleUIRestoreTree)
|
||||
// Alerts list + operator actions.
|
||||
r.Get("/alerts", s.handleUIAlerts)
|
||||
r.Post("/alerts/{id}/acknowledge", s.handleUIAlertAcknowledge)
|
||||
r.Post("/alerts/{id}/resolve", s.handleUIAlertResolve)
|
||||
// Audit log (read-only).
|
||||
r.Get("/audit", s.handleUIAudit)
|
||||
r.Get("/audit.csv", s.handleUIAuditCSV)
|
||||
// Settings shell + Notifications sub-tab CRUD.
|
||||
r.Get("/settings", s.handleUISettings)
|
||||
r.Get("/settings/notifications", s.handleUINotificationsList)
|
||||
r.Get("/settings/notifications/new", s.handleUINotificationNewGet)
|
||||
r.Post("/settings/notifications/new", s.handleUINotificationNewPost)
|
||||
r.Get("/settings/notifications/{id}/edit", s.handleUINotificationEditGet)
|
||||
r.Post("/settings/notifications/{id}/edit", s.handleUINotificationEditPost)
|
||||
r.Post("/settings/notifications/{id}/delete", s.handleUINotificationDelete)
|
||||
r.Post("/settings/notifications/{id}/toggle", s.handleUINotificationToggle)
|
||||
}
|
||||
|
||||
// Browser job-log stream (separate from /ws/agent so the auth
|
||||
// layer is session-cookie not bearer). Mounted regardless of
|
||||
// whether the UI is up — JSON callers may also subscribe.
|
||||
if s.deps.JobHub != nil {
|
||||
r.Get("/api/jobs/{id}/stream", s.handleJobStream)
|
||||
}
|
||||
// Viewer band — anyone authenticated can read.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.requireRole(store.RoleViewer))
|
||||
|
||||
// Job log download (txt + ndjson). Source of truth is the
|
||||
// persisted job_logs table; safe to call any time, no pause
|
||||
// needed against the live stream.
|
||||
r.Get("/api/jobs/{id}/log.{format:txt|ndjson}", s.handleJobLogDownload)
|
||||
// Read APIs.
|
||||
r.Get("/api/hosts", s.handleListHosts)
|
||||
r.Get("/api/fleet/summary", s.handleFleetSummary)
|
||||
r.Get("/api/hosts/{id}/snapshots", s.handleListHostSnapshots)
|
||||
r.Get("/api/hosts/{id}/repo-credentials", s.handleGetHostCredentials)
|
||||
r.Get("/api/hosts/{id}/admin-credentials", s.handleGetAdminCredentials)
|
||||
r.Get("/api/hosts/{id}/schedules", s.handleListSchedules)
|
||||
r.Get("/api/hosts/{id}/source-groups", s.handleListSourceGroups)
|
||||
r.Get("/api/hosts/{id}/source-groups/{gid}", s.handleGetSourceGroup)
|
||||
r.Get("/api/hosts/{id}/repo-maintenance", s.handleGetRepoMaintenance)
|
||||
r.Get("/api/alerts", s.handleAPIAlerts)
|
||||
r.Get("/api/audit", s.handleAPIAudit)
|
||||
|
||||
// Job log stream + download (read-only; any authenticated user).
|
||||
if s.deps.JobHub != nil {
|
||||
r.Get("/api/jobs/{id}/stream", s.handleJobStream)
|
||||
}
|
||||
r.Get("/api/jobs/{id}/log.{format:txt|ndjson}", s.handleJobLogDownload)
|
||||
|
||||
if s.deps.UI != nil {
|
||||
r.Get("/", s.handleUIDashboard)
|
||||
r.Get("/hosts/{id}", s.handleUIHostDetail)
|
||||
r.Get("/hosts/{id}/sources", s.handleUIHostSources)
|
||||
r.Get("/hosts/{id}/sources/new", s.handleUISourceGroupNewGet)
|
||||
r.Get("/hosts/{id}/sources/{gid}/edit", s.handleUISourceGroupEditGet)
|
||||
r.Get("/hosts/{id}/repo", s.handleUIHostRepo)
|
||||
r.Get("/hosts/{id}/schedules", s.handleUISchedulesList)
|
||||
r.Get("/hosts/{id}/schedules/new", s.handleUIScheduleNewGet)
|
||||
r.Get("/hosts/{id}/schedules/{sid}/edit", s.handleUIScheduleEditGet)
|
||||
r.Get("/jobs/{id}", s.handleUIJobDetail)
|
||||
r.Get("/hosts/{id}/restore", s.handleUIRestoreGet)
|
||||
r.Get("/hosts/{id}/snapshots/{sid}/restore", s.handleUIRestoreGet)
|
||||
r.Get("/hosts/{id}/restore/tree", s.handleUIRestoreTree)
|
||||
r.Get("/alerts", s.handleUIAlerts)
|
||||
r.Get("/audit", s.handleUIAudit)
|
||||
r.Get("/audit.csv", s.handleUIAuditCSV)
|
||||
}
|
||||
})
|
||||
|
||||
// Operator band — mutating endpoints up to backup ops.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.requireRole(store.RoleOperator))
|
||||
|
||||
// Pending hosts approval.
|
||||
r.Post("/api/pending-hosts/{id}/accept", s.handleAcceptPendingHost)
|
||||
r.Post("/api/pending-hosts/{id}/reject", s.handleRejectPendingHost)
|
||||
r.Post("/api/enrollment-tokens", s.handleCreateEnrollmentToken)
|
||||
|
||||
// Run-now, restore, repo ops (JSON).
|
||||
r.Post("/api/hosts/{id}/jobs", s.handleRunNow)
|
||||
r.Put("/api/hosts/{id}/repo-credentials", s.handleSetHostCredentials)
|
||||
r.Put("/api/hosts/{id}/admin-credentials", s.handleSetAdminCredentials)
|
||||
r.Delete("/api/hosts/{id}/admin-credentials", s.handleDeleteAdminCredentials)
|
||||
r.Post("/api/hosts/{id}/schedules", s.handleCreateSchedule)
|
||||
r.Put("/api/hosts/{id}/schedules/{sid}", s.handleUpdateSchedule)
|
||||
r.Delete("/api/hosts/{id}/schedules/{sid}", s.handleDeleteSchedule)
|
||||
r.Post("/api/hosts/{id}/source-groups", s.handleCreateSourceGroup)
|
||||
r.Put("/api/hosts/{id}/source-groups/{gid}", s.handleUpdateSourceGroup)
|
||||
r.Delete("/api/hosts/{id}/source-groups/{gid}", s.handleDeleteSourceGroup)
|
||||
r.Put("/api/hosts/{id}/repo-maintenance", s.handleUpdateRepoMaintenance)
|
||||
r.Put("/api/hosts/{id}/bandwidth", s.handleUpdateHostBandwidth)
|
||||
r.Post("/api/hosts/{id}/source-groups/{gid}/run", s.handleRunSourceGroup)
|
||||
r.Post("/api/hosts/{id}/repo/prune", s.handleRunRepoPrune)
|
||||
r.Post("/api/hosts/{id}/repo/check", s.handleRunRepoCheck)
|
||||
r.Post("/api/hosts/{id}/repo/unlock", s.handleRunRepoUnlock)
|
||||
r.Post("/api/jobs/{id}/cancel", s.handleCancelJob)
|
||||
r.Post("/api/hosts/{id}/snapshots/diff", s.handleSnapshotDiff)
|
||||
|
||||
// HTMX form variants outside /api.
|
||||
r.Post("/hosts/{id}/snapshots/diff", s.handleSnapshotDiff)
|
||||
r.Post("/hosts/{id}/source-groups/{gid}/run", s.handleRunSourceGroup)
|
||||
r.Post("/hosts/{id}/repo/prune", s.handleRunRepoPrune)
|
||||
r.Post("/hosts/{id}/repo/check", s.handleRunRepoCheck)
|
||||
r.Post("/hosts/{id}/repo/unlock", s.handleRunRepoUnlock)
|
||||
r.Post("/hosts/{id}/run-backup", s.handleUIRunBackupGone)
|
||||
r.Post("/hosts/{id}/init-repo", s.handleUIInitRepoGone)
|
||||
|
||||
if s.deps.UI != nil {
|
||||
r.Get("/hosts/new", s.handleUIAddHostGet)
|
||||
r.Post("/hosts/new", s.handleUIAddHostPost)
|
||||
r.Get("/hosts/pending/{token}", s.handleUIPendingHost)
|
||||
r.Get("/hosts/pending/{token}/awaiting", s.handleUIPendingAwaiting)
|
||||
r.Post("/hosts/{id}/sources/new", s.handleUISourceGroupSave)
|
||||
r.Post("/hosts/{id}/sources/{gid}/edit", s.handleUISourceGroupSave)
|
||||
r.Post("/hosts/{id}/sources/{gid}/delete", s.handleUISourceGroupDelete)
|
||||
r.Post("/hosts/{id}/repo/credentials", s.handleUIRepoCredentialsSave)
|
||||
r.Post("/hosts/{id}/repo/bandwidth", s.handleUIRepoBandwidthSave)
|
||||
r.Post("/hosts/{id}/repo/maintenance", s.handleUIRepoMaintenanceSave)
|
||||
r.Post("/hosts/{id}/repo/reinit", s.handleUIRepoReinit)
|
||||
r.Post("/hosts/{id}/repo/hooks", s.handleUIRepoHooksSave)
|
||||
r.Post("/hosts/{id}/admin-credentials", s.handleUIAdminCredentialsSave)
|
||||
r.Post("/hosts/{id}/admin-credentials/delete", s.handleUIAdminCredentialsDelete)
|
||||
r.Post("/hosts/{id}/schedules/new", s.handleUIScheduleSave)
|
||||
r.Post("/hosts/{id}/schedules/{sid}/edit", s.handleUIScheduleSave)
|
||||
r.Post("/hosts/{id}/schedules/{sid}/delete", s.handleUIScheduleDelete)
|
||||
r.Post("/hosts/{id}/schedules/{sid}/run", s.handleUIScheduleRun)
|
||||
r.Post("/hosts/{id}/restore", s.handleUIRestorePost)
|
||||
r.Post("/alerts/{id}/acknowledge", s.handleUIAlertAcknowledge)
|
||||
r.Post("/alerts/{id}/resolve", s.handleUIAlertResolve)
|
||||
}
|
||||
})
|
||||
|
||||
// Admin band — channels, server-shape config.
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(s.requireRole(store.RoleAdmin))
|
||||
|
||||
r.Post("/api/notifications/{id}/test", s.handleAPINotificationTest)
|
||||
|
||||
if s.deps.UI != nil {
|
||||
r.Get("/settings", s.handleUISettings)
|
||||
r.Get("/settings/notifications", s.handleUINotificationsList)
|
||||
r.Get("/settings/notifications/new", s.handleUINotificationNewGet)
|
||||
r.Post("/settings/notifications/new", s.handleUINotificationNewPost)
|
||||
r.Get("/settings/notifications/{id}/edit", s.handleUINotificationEditGet)
|
||||
r.Post("/settings/notifications/{id}/edit", s.handleUINotificationEditPost)
|
||||
r.Post("/settings/notifications/{id}/delete", s.handleUINotificationDelete)
|
||||
r.Post("/settings/notifications/{id}/toggle", s.handleUINotificationToggle)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Start begins listening. Blocks until ListenAndServe returns
|
||||
|
||||
Reference in New Issue
Block a user