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"