refactor admin api

Kim committed Mar 17, 2026 at 15:57 UTC f770e0a784a820bc288eac6d4ddae2c58a73434f
15 files changed +139 -113
cmd/relay-server/frontend.go
+1 -1
@@ -135,7 +135,7 @@ func (f *Frontend) servePortalHTMLWithSSR(w http.ResponseWriter) {
135 }
136
137 func (f *Frontend) injectServerData(htmlContent string) string {
138 - rows := admin.BuildLeaseRows(f.server, false, f.portalURL)
138 + rows := admin.BuildLeaseRows(f.server, false)
139 jsonData, err := json.Marshal(rows)
140 if err != nil {
141 jsonData = []byte("[]")
cmd/relay-server/serve.go
+1 -1
@@ -49,7 +49,7 @@ func runServer(cfg relayServerConfig) error {
49 }
50
51 frontend := NewFrontend(cfg.PortalURL)
52 - adminHandler := admin.NewHandler(cfg.PortalURL, cfg.AdminSecretKey, "admin_settings.json", cfg.TrustProxyHeaders, func(w http.ResponseWriter, r *http.Request, appPath string) {
52 + adminHandler := admin.NewHandler(cfg.AdminSecretKey, "admin_settings.json", cfg.TrustProxyHeaders, func(w http.ResponseWriter, r *http.Request, appPath string) {
53 frontend.ServeAppStatic(w, r, appPath)
54 })
55 frontend.Bind(server)
docs/architecture.md
+4 -3
@@ -39,7 +39,7 @@ That distinction matters because `/sdk/connect` stops being ordinary HTTP once h
39 - Admin/API TLS listener on `--api-port` (default `:4017`)
40 - SNI listener on `--sni-port` (default `:443`)
41 - Public frontend routes under `/`, `/app`, `/assets/*`
42 -- Minimal admin surface at `/admin` and `/admin/leases`
42 +- Minimal admin surface at `/admin`, `/admin/snapshot`, and admin action/auth routes under `/admin/*`
43 - Tunnel bootstrap routes at `/install.sh`, `/install.ps1`, and `/install/bin/*`
44 - Keyless signer endpoint at `/v1/sign`
45
@@ -149,7 +149,8 @@ Current relay-served public routes:
149 - `/app/*`
150 - `/assets/*`
151 - `/admin`
152 -- `/admin/leases`
152 +- `/admin/snapshot`
153 +- `/admin/leases/*`
154 - `/install.sh`
155 - `/install.ps1`
156 - `/install/bin/*`
@@ -157,7 +158,7 @@ Current relay-served public routes:
158 - `/v1/sign`
159 - `/sdk/*`
160
160 -The admin surface is intentionally small in the current Go runtime: an HTML index plus a JSON lease list.
161 +The admin surface is intentionally small in the current Go runtime: an HTML index, one JSON snapshot endpoint, and a small set of admin action/auth routes.
162
163 ## Shared Contract Surface
164
docs/glossary.md
+2 -1
@@ -85,7 +85,8 @@ The relay HTTPS listener (default `:4017`) serving:
85
86 - `/sdk/*`
87 - `/admin`
88 -- `/admin/leases`
88 +- `/admin/snapshot`
89 +- `/admin/leases/*`
90 - `/healthz`
91 - `/v1/sign`
92 - frontend root/app routes through root-host fallback
frontend/AGENTS.md
+5 -5
@@ -5,13 +5,13 @@ High-signal constraints for the relay-server frontend. Only items expensive to r
5 ## Frontend-Backend Contracts (Manually Synced)
6
7 1. **SSR data shape is a 3-way contract.**
8 - Go `leaseRow` (`cmd/relay-server/frontend.go`) -> TS `ServerData` (`src/hooks/useSSRData.ts`) -> `<script id="__SSR_DATA__">` injection (`cmd/relay-server/frontend.go`).
8 + Go `types.LeaseRow` (`../types/api.go`) + row builder (`../portal/admin/rows.go`) -> TS `ServerData` (`src/hooks/useSSRData.ts`) -> `<script id="__SSR_DATA__">` injection (`cmd/relay-server/frontend.go`).
9 - Why: no shared schema or codegen. Field drift silently breaks SSR hydration. The script tag ID `__SSR_DATA__` is hardcoded in all three locations.
10
11 2. **API path constants require dual maintenance.**
12 Go definitions live in `types/paths.go`; TS duplicates live in `src/lib/apiPaths.ts`.
13 - Why: no codegen. A path mismatch produces same-origin 404s.
14 - - Current Go runtime only serves `/admin` and `/admin/leases` on the admin surface. Extra TS admin paths need matching backend work or they will 404.
14 + - Current Go runtime serves `/admin` and `/admin/snapshot` for admin reads; `/admin/leases/*`, `/admin/settings/approval-mode`, and related auth/IP routes are action surfaces. Extra TS admin paths still need backend work or they will 404.
15
16 3. **API envelope shape must match across Go and TS.**
17 All JSON control-plane responses use `{ ok, data?, error?: { code, message } }`.
@@ -26,9 +26,9 @@ High-signal constraints for the relay-server frontend. Only items expensive to r
26 `index.html` (renamed to `portal.html`) contains `[%OG_TITLE%]`, `[%OG_DESCRIPTION%]`, `[%OG_IMAGE_URL%]`, `[%RELEASE_VERSION%]`. Server-side substitution happens in `cmd/relay-server/frontend.go`.
27 - Why: renaming a placeholder in one place without the other leaves raw placeholder strings in production HTML.
28
29 -6. **Frontend admin action helpers currently outpace the Go runtime.**
30 - `src/lib/apiPaths.ts` still contains action routes such as `/admin/leases/{id}/{action}` and `/admin/settings/*`.
31 - - Why: do not assume those routes exist in the current Go relay unless you implement the backend in the same change.
29 +6. **Admin state reads are aggregated through `/admin/snapshot`.**
30 + `src/hooks/useAdmin.ts` expects one payload carrying `leases`, `banned_leases`, and `approval_mode`.
31 + - Why: splitting those reads across multiple endpoints reintroduces extra request coordination and drift in the admin bootstrap path.
32
33 ## Frontend Conventions
34
frontend/README.md
+3 -3
@@ -58,7 +58,7 @@ frontend/
58 1. Go backend injects lease data into `portal.html` using `<script id="__SSR_DATA__">`.
59 2. Frontend reads it with `useSSRData()`.
60 3. UI renders server list immediately without an initial fetch.
61 -4. Admin pages then call `/admin/*` endpoints through `apiClient` for stateful actions (approve, deny, ban, settings).
61 +4. Admin pages load state through `/admin/snapshot`, then call `/admin/*` action endpoints through `apiClient` for changes (approve, deny, ban, settings).
62
63 ### List Filtering and Sort
64
@@ -132,8 +132,8 @@ Admin endpoints use a JSON envelope contract (`{ ok, data, error }`) and reject
132
133 Admin lease ID contract:
134
135 -- `/admin/leases` rows return plain lease IDs in `Peer`.
136 -- `/admin/leases/banned` returns plain lease IDs (`[]string`).
135 +- `/admin/snapshot` returns `leases`, `banned_leases`, and `approval_mode` in one envelope payload.
136 +- `leases` rows inside the snapshot return plain lease IDs in `Peer`.
137 - Frontend only Base64URL-encodes lease IDs when constructing admin action routes (`/admin/leases/{encodedLeaseID}/{action}`).
138
139 ### SDK-Related Runtime Contract
frontend/src/hooks/useAdmin.test.ts
+7 -19
@@ -82,18 +82,12 @@ describe("useAdmin", () => {
82 vi.clearAllMocks();
83
84 mockGet.mockImplementation(async (path: string) => {
85 - if (path === API_PATHS.admin.leases) {
86 - return [buildLease("peer-a")] as never;
87 - }
88 - if (path === API_PATHS.admin.bannedLeases) {
89 - return [
90 - "peer-a",
91 - "peer-a",
92 - "peer-b",
93 - ] as never;
94 - }
95 - if (path === API_PATHS.admin.approvalMode) {
96 - return { approval_mode: "not-a-mode" } as never;
85 + if (path === API_PATHS.admin.snapshot) {
86 + return {
87 + leases: [buildLease("peer-a")],
88 + banned_leases: ["peer-a", "peer-a", "peer-b"],
89 + approval_mode: "not-a-mode",
90 + } as never;
91 }
92 throw new Error(`Unexpected GET path: ${path}`);
93 });
@@ -115,15 +109,9 @@ describe("useAdmin", () => {
109
110 it("surfaces fetchData API errors", async () => {
111 mockGet.mockImplementation(async (path: string) => {
118 - if (path === API_PATHS.admin.leases) {
112 + if (path === API_PATHS.admin.snapshot) {
113 throw new APIClientError("failed to load leases", 500, "server_error");
114 }
121 - if (path === API_PATHS.admin.bannedLeases) {
122 - return [] as never;
123 - }
124 - if (path === API_PATHS.admin.approvalMode) {
125 - return { approval_mode: "manual" } as never;
126 - }
115 throw new Error(`Unexpected GET path: ${path}`);
116 });
117
frontend/src/hooks/useAdmin.ts
+13 -13
@@ -15,13 +15,17 @@ export type ApprovalMode = "auto" | "manual";
15
16 type LeaseAction = "approve" | "deny" | "ban";
17
18 -type SettingsResponse = {
18 +type ApprovalModeResponse = {
19 approval_mode?: ApprovalMode;
20 };
21
22 -interface LeaseActionResult {
22 +type AdminSnapshotResponse = {
23 approval_mode?: ApprovalMode;
24 -}
24 + banned_leases?: string[];
25 + leases?: ServerData[];
26 +};
27 +
28 +type LeaseActionResult = ApprovalModeResponse;
29
30 export interface AdminServer extends BaseServer {
31 peerId: string;
@@ -123,20 +127,16 @@ interface AdminSnapshot {
127 }
128
129 async function loadAdminSnapshot(): Promise<AdminSnapshot> {
126 - const [leasesData, bannedData, settings] = await Promise.all([
127 - apiClient.get<ServerData[]>(API_PATHS.admin.leases),
128 - apiClient.get<string[]>(API_PATHS.admin.bannedLeases),
129 - apiClient.get<SettingsResponse>(API_PATHS.admin.approvalMode),
130 - ]);
131 -
132 - const normalizedBans = (Array.isArray(bannedData) ? bannedData : []).filter(
130 + const snapshot = await apiClient.get<AdminSnapshotResponse>(API_PATHS.admin.snapshot);
131 + const normalizedLeases = Array.isArray(snapshot?.leases) ? snapshot.leases : [];
132 + const normalizedBans = (Array.isArray(snapshot?.banned_leases) ? snapshot.banned_leases : []).filter(
133 (leaseID): leaseID is string => typeof leaseID === "string"
134 );
135
136 return {
137 - serverData: Array.isArray(leasesData) ? leasesData : [],
137 + serverData: normalizedLeases,
138 bannedLeases: dedupeStrings(normalizedBans),
139 - approvalMode: normalizeApprovalMode(settings?.approval_mode),
139 + approvalMode: normalizeApprovalMode(snapshot?.approval_mode),
140 };
141 }
142
@@ -278,7 +278,7 @@ export function useAdmin() {
278
279 const handleApprovalModeChange = async (mode: ApprovalMode) => {
280 await runAdminAction(async () => {
281 - const response = await apiClient.post<SettingsResponse>(
281 + const response = await apiClient.post<ApprovalModeResponse>(
282 API_PATHS.admin.approvalMode,
283 { mode }
284 );
frontend/src/lib/apiPaths.test.ts
+4
@@ -3,6 +3,10 @@ import { describe, expect, it } from "vitest";
3 import { API_PATHS, adminLeasePath, encodeLeaseID } from "@/lib/apiPaths";
4
5 describe("API_PATHS contract alignment", () => {
6 + it("keeps admin snapshot path aligned", () => {
7 + expect(API_PATHS.admin.snapshot).toBe("/admin/snapshot");
8 + });
9 +
10 it("keeps sdk endpoint paths aligned", () => {
11 expect(API_PATHS.sdk).toEqual({
12 prefix: "/sdk",
frontend/src/lib/apiPaths.ts
+1 -2
@@ -1,13 +1,12 @@
1 export const API_PATHS = {
2 admin: {
3 prefix: "/admin",
4 + snapshot: "/admin/snapshot",
5 login: "/admin/login",
6 logout: "/admin/logout",
7 authStatus: "/admin/auth/status",
8 leases: "/admin/leases",
8 - bannedLeases: "/admin/leases/banned",
9 stats: "/admin/stats",
10 - settings: "/admin/settings",
10 approvalMode: "/admin/settings/approval-mode",
11 },
12 sdk: {
portal/admin/handler.go
+11 -32
@@ -20,16 +20,16 @@ type Handler struct {
20 server *portal.Server
21 settings *stateStore
22 serveAppStatic func(http.ResponseWriter, *http.Request, string)
23 - buildLeaseRows func(*portal.Server, bool) []LeaseRow
23 + buildLeaseRows func(*portal.Server, bool) []types.LeaseRow
24 trustProxy bool
25 }
26
27 -func NewHandler(portalURL, secret, settingsPath string, trustProxy bool, serveAppStatic func(http.ResponseWriter, *http.Request, string)) *Handler {
27 +func NewHandler(secret, settingsPath string, trustProxy bool, serveAppStatic func(http.ResponseWriter, *http.Request, string)) *Handler {
28 h := &Handler{
29 auth: policy.NewAuthenticator(strings.TrimSpace(secret)),
30 settings: newStateStore(settingsPath),
31 - buildLeaseRows: func(serv *portal.Server, includeAdmin bool) []LeaseRow {
32 - return BuildLeaseRows(serv, includeAdmin, portalURL)
31 + buildLeaseRows: func(serv *portal.Server, includeAdmin bool) []types.LeaseRow {
32 + return BuildLeaseRows(serv, includeAdmin)
33 },
34 trustProxy: trustProxy,
35 }
@@ -88,12 +88,8 @@ func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
88 }
89
90 switch path {
91 - case types.PathAdminLeases:
92 - h.handleLeases(w, r)
93 - case types.PathAdminBanned:
94 - h.handleBannedLeases(w, r)
95 - case types.PathAdminSettings:
96 - h.handleSettings(w, r)
91 + case types.PathAdminSnapshot:
92 + h.handleSnapshot(w, r)
93 case types.PathAdminApproval:
94 h.handleApprovalMode(w, r)
95 default:
@@ -193,32 +189,17 @@ func (h *Handler) handleAuthStatus(w http.ResponseWriter, r *http.Request) {
189 })
190 }
191
196 -func (h *Handler) handleLeases(w http.ResponseWriter, r *http.Request) {
192 +func (h *Handler) handleSnapshot(w http.ResponseWriter, r *http.Request) {
193 if r.Method != http.MethodGet {
194 utils.WriteAPIError(w, http.StatusMethodNotAllowed, types.APIErrorCodeMethodNotAllowed, "method not allowed")
195 return
196 }
201 - utils.WriteAPIData(w, http.StatusOK, h.buildLeaseRows(h.server, true))
202 -}
203 -
204 -func (h *Handler) handleBannedLeases(w http.ResponseWriter, r *http.Request) {
205 - if r.Method != http.MethodGet {
206 - utils.WriteAPIError(w, http.StatusMethodNotAllowed, types.APIErrorCodeMethodNotAllowed, "method not allowed")
207 - return
208 - }
209 - utils.WriteAPIData(w, http.StatusOK, h.policyRuntime().BannedLeases())
210 -}
197
212 -func (h *Handler) handleSettings(w http.ResponseWriter, r *http.Request) {
213 - if r.Method != http.MethodGet {
214 - utils.WriteAPIError(w, http.StatusMethodNotAllowed, types.APIErrorCodeMethodNotAllowed, "method not allowed")
215 - return
216 - }
198 approver := h.policyRuntime().Approver()
218 - utils.WriteAPIData(w, http.StatusOK, types.AdminSettingsResponse{
219 - ApprovalMode: string(approver.Mode()),
220 - ApprovedLeases: approver.ApprovedLeases(),
221 - DeniedLeases: approver.DeniedLeases(),
199 + utils.WriteAPIData(w, http.StatusOK, types.AdminSnapshotResponse{
200 + ApprovalMode: string(approver.Mode()),
201 + BannedLeases: h.policyRuntime().BannedLeases(),
202 + Leases: h.buildLeaseRows(h.server, true),
203 })
204 }
205
@@ -226,8 +207,6 @@ func (h *Handler) handleApprovalMode(w http.ResponseWriter, r *http.Request) {
207 runtime := h.policyRuntime()
208 approver := runtime.Approver()
209 switch r.Method {
229 - case http.MethodGet:
230 - utils.WriteAPIData(w, http.StatusOK, types.AdminApprovalModeResponse{ApprovalMode: string(approver.Mode())})
210 case http.MethodPost:
211 var req types.AdminApprovalModeRequest
212 if err := decodeJSON(w, r, &req); err != nil {
portal/admin/handler_test.go
+53 -1
@@ -16,7 +16,7 @@ import (
16 func TestLoginAndProtectedActions(t *testing.T) {
17 t.Parallel()
18
19 - handler := NewHandler("https://portal.example.com", "secret-key", filepath.Join(t.TempDir(), "admin_settings.json"), false, func(w http.ResponseWriter, _ *http.Request, _ string) {
19 + handler := NewHandler("secret-key", filepath.Join(t.TempDir(), "admin_settings.json"), false, func(w http.ResponseWriter, _ *http.Request, _ string) {
20 w.WriteHeader(http.StatusOK)
21 })
22 server, err := portal.NewServer(portal.ServerConfig{PortalURL: "https://portal.example.com"})
@@ -52,6 +52,58 @@ func TestLoginAndProtectedActions(t *testing.T) {
52 t.Fatalf("auth status = %+v, want authenticated + auth enabled", authStatus)
53 }
54
55 + snapshotRecorder := httptest.NewRecorder()
56 + snapshotRequest := httptest.NewRequest(http.MethodGet, types.PathAdminSnapshot, nil)
57 + snapshotRequest.RemoteAddr = "127.0.0.1:1234"
58 + snapshotRequest.AddCookie(cookies[0])
59 + handler.HandleRequest(snapshotRecorder, snapshotRequest)
60 + if snapshotRecorder.Code != http.StatusOK {
61 + t.Fatalf("snapshot status = %d, want %d", snapshotRecorder.Code, http.StatusOK)
62 + }
63 + snapshot := decodeEnvelope[types.AdminSnapshotResponse](t, snapshotRecorder)
64 + if snapshot.ApprovalMode != string(policy.ModeAuto) {
65 + t.Fatalf("snapshot approval mode = %q, want %q", snapshot.ApprovalMode, policy.ModeAuto)
66 + }
67 + if len(snapshot.Leases) != 0 {
68 + t.Fatalf("snapshot leases len = %d, want 0", len(snapshot.Leases))
69 + }
70 +
71 + legacyLeasesRecorder := httptest.NewRecorder()
72 + legacyLeasesRequest := httptest.NewRequest(http.MethodGet, types.PathAdminLeases, nil)
73 + legacyLeasesRequest.RemoteAddr = "127.0.0.1:1234"
74 + legacyLeasesRequest.AddCookie(cookies[0])
75 + handler.HandleRequest(legacyLeasesRecorder, legacyLeasesRequest)
76 + if legacyLeasesRecorder.Code != http.StatusNotFound {
77 + t.Fatalf("legacy leases status = %d, want %d", legacyLeasesRecorder.Code, http.StatusNotFound)
78 + }
79 +
80 + legacyBannedRecorder := httptest.NewRecorder()
81 + legacyBannedRequest := httptest.NewRequest(http.MethodGet, "/admin/leases/banned", nil)
82 + legacyBannedRequest.RemoteAddr = "127.0.0.1:1234"
83 + legacyBannedRequest.AddCookie(cookies[0])
84 + handler.HandleRequest(legacyBannedRecorder, legacyBannedRequest)
85 + if legacyBannedRecorder.Code != http.StatusNotFound {
86 + t.Fatalf("legacy banned status = %d, want %d", legacyBannedRecorder.Code, http.StatusNotFound)
87 + }
88 +
89 + legacySettingsRecorder := httptest.NewRecorder()
90 + legacySettingsRequest := httptest.NewRequest(http.MethodGet, "/admin/settings", nil)
91 + legacySettingsRequest.RemoteAddr = "127.0.0.1:1234"
92 + legacySettingsRequest.AddCookie(cookies[0])
93 + handler.HandleRequest(legacySettingsRecorder, legacySettingsRequest)
94 + if legacySettingsRecorder.Code != http.StatusNotFound {
95 + t.Fatalf("legacy settings status = %d, want %d", legacySettingsRecorder.Code, http.StatusNotFound)
96 + }
97 +
98 + legacyApprovalGetRecorder := httptest.NewRecorder()
99 + legacyApprovalGetRequest := httptest.NewRequest(http.MethodGet, types.PathAdminApproval, nil)
100 + legacyApprovalGetRequest.RemoteAddr = "127.0.0.1:1234"
101 + legacyApprovalGetRequest.AddCookie(cookies[0])
102 + handler.HandleRequest(legacyApprovalGetRecorder, legacyApprovalGetRequest)
103 + if legacyApprovalGetRecorder.Code != http.StatusMethodNotAllowed {
104 + t.Fatalf("legacy approval GET status = %d, want %d", legacyApprovalGetRecorder.Code, http.StatusMethodNotAllowed)
105 + }
106 +
107 approvalRecorder := httptest.NewRecorder()
108 approvalRequest := httptest.NewRequest(http.MethodPost, types.PathAdminApproval, bytes.NewBufferString(`{"mode":"manual"}`))
109 approvalRequest.RemoteAddr = "127.0.0.1:1234"
portal/admin/rows.go
+4 -24
@@ -7,39 +7,19 @@ import (
7 "time"
8
9 "github.com/gosuda/portal/v2/portal"
10 + "github.com/gosuda/portal/v2/types"
11 )
12
13 const staleLeaseHideWindow = 3 * time.Minute
14
14 -type LeaseRow struct {
15 - TTL string
16 - Metadata string
17 - Kind string
18 - IP string
19 - DNS string
20 - LastSeen string
21 - LastSeenISO string
22 - FirstSeenISO string
23 - Name string
24 - Peer string
25 - Link string
26 - BPS int64
27 - Hide bool
28 - StaleRed bool
29 - IsApproved bool
30 - IsDenied bool
31 - Connected bool
32 - IsIPBanned bool
33 -}
34 -
35 -func BuildLeaseRows(serv *portal.Server, includeAdmin bool, portalURL string) []LeaseRow {
15 +func BuildLeaseRows(serv *portal.Server, includeAdmin bool) []types.LeaseRow {
16 if serv == nil {
17 return nil
18 }
19
20 now := time.Now()
21 snapshots := serv.ListLeases()
42 - rows := make([]LeaseRow, 0, len(snapshots))
22 + rows := make([]types.LeaseRow, 0, len(snapshots))
23 for _, snapshot := range snapshots {
24 if now.After(snapshot.ExpiresAt) {
25 continue
@@ -62,7 +42,7 @@ func BuildLeaseRows(serv *portal.Server, includeAdmin bool, portalURL string) []
42 metadataJSON, _ := json.Marshal(snapshot.Metadata)
43 host := snapshot.Hostname
44
65 - rows = append(rows, LeaseRow{
45 + rows = append(rows, types.LeaseRow{
46 TTL: formatDuration(time.Until(snapshot.ExpiresAt)),
47 Metadata: string(metadataJSON),
48 Kind: "https",
types/api.go
+29 -6
@@ -71,6 +71,29 @@ func (m LeaseMetadata) Copy() LeaseMetadata {
71 }
72 }
73
74 +// LeaseRow is the shared lease-list contract used by the relay SSR bootstrap
75 +// payload and the admin lease API.
76 +type LeaseRow struct {
77 + TTL string
78 + Metadata string
79 + Kind string
80 + IP string
81 + DNS string
82 + LastSeen string
83 + LastSeenISO string
84 + FirstSeenISO string
85 + Name string
86 + Peer string
87 + Link string
88 + BPS int64
89 + Hide bool
90 + StaleRed bool
91 + IsApproved bool
92 + IsDenied bool
93 + Connected bool
94 + IsIPBanned bool
95 +}
96 +
97 type RegisterRequest struct {
98 Name string `json:"name"`
99 ReverseToken string `json:"reverse_token"`
@@ -121,6 +144,12 @@ type AdminAuthStatusResponse struct {
144 AuthEnabled bool `json:"auth_enabled"`
145 }
146
147 +type AdminSnapshotResponse struct {
148 + ApprovalMode string `json:"approval_mode"`
149 + BannedLeases []string `json:"banned_leases,omitempty"`
150 + Leases []LeaseRow `json:"leases,omitempty"`
151 +}
152 +
153 type AdminApprovalModeRequest struct {
154 Mode string `json:"mode"`
155 }
@@ -128,9 +157,3 @@ type AdminApprovalModeRequest struct {
157 type AdminApprovalModeResponse struct {
158 ApprovalMode string `json:"approval_mode"`
159 }
131 -
132 -type AdminSettingsResponse struct {
133 - ApprovalMode string `json:"approval_mode"`
134 - ApprovedLeases []string `json:"approved_leases,omitempty"`
135 - DeniedLeases []string `json:"denied_leases,omitempty"`
136 -}
types/paths.go
+1 -2
@@ -9,13 +9,12 @@ const (
9 PathAppPrefix = "/app/"
10 PathAdmin = "/admin"
11 PathAdminPrefix = "/admin/"
12 + PathAdminSnapshot = "/admin/snapshot"
13 PathAdminLeases = "/admin/leases"
14 PathAdminLeasesPrefix = "/admin/leases/"
14 - PathAdminBanned = "/admin/leases/banned"
15 PathAdminLogin = "/admin/login"
16 PathAdminLogout = "/admin/logout"
17 PathAdminAuthStatus = "/admin/auth/status"
18 - PathAdminSettings = "/admin/settings"
18 PathAdminApproval = "/admin/settings/approval-mode"
19 PathAdminIPsPrefix = "/admin/ips/"
20 PathInstallShell = "/install.sh"