@mug-lab/cookie-auth-proxy 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +642 -0
- package/SECURITY.md +9 -0
- package/config.json +43 -0
- package/cookie-auth-proxy.config.example.json +43 -0
- package/overrides/README.md +4 -0
- package/package.json +35 -0
- package/src/admin/admin.css +508 -0
- package/src/admin/editors.js +600 -0
- package/src/admin/events.js +325 -0
- package/src/admin/files.js +113 -0
- package/src/admin/renderers.js +513 -0
- package/src/admin/state.js +325 -0
- package/src/admin.html +178 -0
- package/src/config/index.js +372 -0
- package/src/cookie-auth-proxy.schema.json +233 -0
- package/src/diagnostics.js +154 -0
- package/src/init.js +201 -0
- package/src/logger.js +18 -0
- package/src/proxy.js +158 -0
- package/src/regex.js +101 -0
- package/src/runtime/index.js +671 -0
- package/src/sensitive.js +107 -0
- package/src/server/admin-server.js +562 -0
- package/src/server/proxy-server.js +671 -0
- package/src/server/rewrite.js +228 -0
- package/stubs/README.md +8 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ID指定でDOM要素を取得する短縮関数。
|
|
3
|
+
*/
|
|
4
|
+
const $ = (id) => document.getElementById(id);
|
|
5
|
+
const adminCsrfToken = window.__COOKIE_AUTH_PROXY_CSRF__ || "";
|
|
6
|
+
let runtimeSnapshot = { clientOrigin: "", apiOrigin: "", apiCookie: "" };
|
|
7
|
+
let historyFilter = "all";
|
|
8
|
+
let pathFilter = "";
|
|
9
|
+
let lastHistory = [];
|
|
10
|
+
let lastStubs = [];
|
|
11
|
+
let lastOverrides = [];
|
|
12
|
+
let historyRenderSignature = "";
|
|
13
|
+
const historyDetails = new Map();
|
|
14
|
+
const historyDisplayLimit = 80;
|
|
15
|
+
const dirtyNewStubIds = new Set();
|
|
16
|
+
const dirtyNewOverrideIds = new Set();
|
|
17
|
+
let allStubsEnabled = true;
|
|
18
|
+
let allJsonOverridesEnabled = true;
|
|
19
|
+
let pendingImportMode = "append";
|
|
20
|
+
let pendingOverrideImportMode = "append";
|
|
21
|
+
let serverAppPath = "/";
|
|
22
|
+
let apiOriginAllowedPatterns = [];
|
|
23
|
+
let sensitiveHeaderNames = [];
|
|
24
|
+
let cookieValuesVisible = false;
|
|
25
|
+
let proxyOrigin = "http://localhost:4300";
|
|
26
|
+
let stubFilter = "";
|
|
27
|
+
let overrideFilter = "";
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 管理APIへJSONリクエストを送り、失敗時はレスポンス本文からエラー内容を復元する。
|
|
31
|
+
*/
|
|
32
|
+
async function api(path, options) {
|
|
33
|
+
const res = await fetch(path, {
|
|
34
|
+
...options,
|
|
35
|
+
headers: {
|
|
36
|
+
"content-type": "application/json",
|
|
37
|
+
...(options && options.headers ? options.headers : {}),
|
|
38
|
+
"x-csrf-token": adminCsrfToken,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
const body = await res.text();
|
|
43
|
+
try {
|
|
44
|
+
throw new Error(JSON.parse(body).error || body);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (error instanceof SyntaxError) throw new Error(body);
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return res.json();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 管理画面の状態、ルール、履歴をサーバから再取得して描画する。
|
|
55
|
+
*/
|
|
56
|
+
async function load() {
|
|
57
|
+
const state = await api("/admin/state");
|
|
58
|
+
$("listen").textContent =
|
|
59
|
+
"Proxy " +
|
|
60
|
+
state.listen +
|
|
61
|
+
" / Client " +
|
|
62
|
+
state.clientOrigin +
|
|
63
|
+
" / Admin " +
|
|
64
|
+
state.adminOrigin;
|
|
65
|
+
proxyOrigin = state.listen;
|
|
66
|
+
$("clientOrigin").value = state.clientOrigin;
|
|
67
|
+
$("apiOrigin").value = state.apiOrigin;
|
|
68
|
+
renderCookieRows(parseCookieHeader(state.apiCookie || ""));
|
|
69
|
+
$("adminNotice").textContent = state.adminNotice || "";
|
|
70
|
+
$("adminNotice").classList.toggle(
|
|
71
|
+
"hidden",
|
|
72
|
+
!state.adminNotice || !state.adminNotice.trim(),
|
|
73
|
+
);
|
|
74
|
+
apiOriginAllowedPatterns = state.apiOriginAllowedPatterns || [];
|
|
75
|
+
sensitiveHeaderNames = state.sensitiveHeaderNames || [];
|
|
76
|
+
serverAppPath = state.serverAppPath || "/";
|
|
77
|
+
runtimeSnapshot = {
|
|
78
|
+
clientOrigin: $("clientOrigin").value,
|
|
79
|
+
apiOrigin: $("apiOrigin").value,
|
|
80
|
+
apiCookie: currentCookieHeader(),
|
|
81
|
+
};
|
|
82
|
+
updateApplyState();
|
|
83
|
+
$("cookieNote").textContent = state.hasCookie
|
|
84
|
+
? "API Cookie is active."
|
|
85
|
+
: "No API cookie is active.";
|
|
86
|
+
allStubsEnabled = Boolean(state.stubsEnabled);
|
|
87
|
+
$("stubsEnabled").checked = allStubsEnabled;
|
|
88
|
+
allJsonOverridesEnabled = Boolean(state.jsonOverridesEnabled);
|
|
89
|
+
$("jsonOverridesEnabled").checked = allJsonOverridesEnabled;
|
|
90
|
+
renderStubs(state.stubs);
|
|
91
|
+
renderOverrides(state.jsonOverrides || []);
|
|
92
|
+
lastHistory = await api("/admin/history?summary=1");
|
|
93
|
+
renderHistory(lastHistory);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Runtime設定のApplyボタン活性状態を更新する。
|
|
98
|
+
*/
|
|
99
|
+
function updateApplyState() {
|
|
100
|
+
const clientOriginError = validateClientOrigin($("clientOrigin").value);
|
|
101
|
+
const originError = validateApiOrigin($("apiOrigin").value);
|
|
102
|
+
$("clientOriginError").textContent = clientOriginError;
|
|
103
|
+
$("apiOriginError").textContent = originError;
|
|
104
|
+
$("saveState").disabled =
|
|
105
|
+
Boolean(clientOriginError) ||
|
|
106
|
+
Boolean(originError) ||
|
|
107
|
+
($("clientOrigin").value === runtimeSnapshot.clientOrigin &&
|
|
108
|
+
$("apiOrigin").value === runtimeSnapshot.apiOrigin &&
|
|
109
|
+
currentCookieHeader() === runtimeSnapshot.apiCookie);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Client Origin入力値がURLとして正しいか検証する。
|
|
114
|
+
*/
|
|
115
|
+
function validateClientOrigin(value) {
|
|
116
|
+
try {
|
|
117
|
+
const origin = new URL(value).origin;
|
|
118
|
+
if (origin === "null") throw new Error("origin is null");
|
|
119
|
+
return "";
|
|
120
|
+
} catch {
|
|
121
|
+
return "Enter a valid client origin.";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* API Origin入力値がURLとして正しく許可パターンに一致するか検証する.
|
|
127
|
+
*/
|
|
128
|
+
function validateApiOrigin(value) {
|
|
129
|
+
if (!String(value || "").trim()) return "";
|
|
130
|
+
|
|
131
|
+
let origin;
|
|
132
|
+
try {
|
|
133
|
+
origin = new URL(value).origin;
|
|
134
|
+
if (origin === "null") throw new Error("origin is null");
|
|
135
|
+
} catch {
|
|
136
|
+
return "Enter a valid API origin.";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (apiOriginAllowedPatterns.length === 0) return "";
|
|
140
|
+
try {
|
|
141
|
+
return apiOriginAllowedPatterns.some((pattern) =>
|
|
142
|
+
compileSafeRegex(pattern, "apiOriginAllowedPatterns").test(origin),
|
|
143
|
+
)
|
|
144
|
+
? ""
|
|
145
|
+
: "This API origin is not allowed by the configured whitelist.";
|
|
146
|
+
} catch {
|
|
147
|
+
return "API origin whitelist configuration is invalid.";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 管理画面内の正規表現を、明らかに危険な形だけ拒否してからRegExpへ変換する。
|
|
153
|
+
*/
|
|
154
|
+
function compileSafeRegex(pattern, label) {
|
|
155
|
+
assertSafeRegexPattern(pattern, label);
|
|
156
|
+
return new RegExp(pattern);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* ReDoSにつながりやすい正規表現を軽量検査する。
|
|
161
|
+
*/
|
|
162
|
+
function assertSafeRegexPattern(pattern, label) {
|
|
163
|
+
const value = String(pattern);
|
|
164
|
+
if (value.length > 1000) {
|
|
165
|
+
throw new Error(`${label}: pattern is too long.`);
|
|
166
|
+
}
|
|
167
|
+
if (/(^|[^\\])\\(?:[1-9]\d*|k<[^>]+>)/.test(value)) {
|
|
168
|
+
throw new Error(`${label}: backreferences are not allowed.`);
|
|
169
|
+
}
|
|
170
|
+
if (hasNestedQuantifier(value)) {
|
|
171
|
+
throw new Error(`${label}: nested quantified groups are not allowed.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* (a+)+ や (.*)+ のような形を検出する。
|
|
177
|
+
*/
|
|
178
|
+
function hasNestedQuantifier(pattern) {
|
|
179
|
+
const source = stripCharacterClassesAndEscapes(pattern);
|
|
180
|
+
return /\((?:\?:|\?=|\?!|\?<=|\?<!)?[^)]*(?:[+*]|\{\d+(?:,\d*)?\})[^)]*\)(?:[+*?]|\{\d+(?:,\d*)?\})/.test(
|
|
181
|
+
source,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 構造検査の誤検知を減らすため、文字クラスとエスケープ済み文字をプレースホルダ化する。
|
|
187
|
+
*/
|
|
188
|
+
function stripCharacterClassesAndEscapes(pattern) {
|
|
189
|
+
let result = "";
|
|
190
|
+
let inClass = false;
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
193
|
+
const char = pattern[i];
|
|
194
|
+
if (char === "\\") {
|
|
195
|
+
result += "x";
|
|
196
|
+
i += 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (inClass) {
|
|
200
|
+
if (char === "]") {
|
|
201
|
+
inClass = false;
|
|
202
|
+
result += "x";
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (char === "[") {
|
|
207
|
+
inClass = true;
|
|
208
|
+
result += "x";
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
result += char;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Cookieヘッダー文字列を、管理画面で編集しやすい名前/値の行配列へ分解する。
|
|
219
|
+
*/
|
|
220
|
+
function parseCookieHeader(header) {
|
|
221
|
+
return String(header || "")
|
|
222
|
+
.split(";")
|
|
223
|
+
.map((part) => part.trim())
|
|
224
|
+
.filter(Boolean)
|
|
225
|
+
.map((part) => {
|
|
226
|
+
const index = part.indexOf("=");
|
|
227
|
+
if (index === -1) return { name: part, value: "" };
|
|
228
|
+
return {
|
|
229
|
+
name: part.slice(0, index).trim(),
|
|
230
|
+
value: part.slice(index + 1).trim(),
|
|
231
|
+
};
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Cookie行エディタを現在のCookie配列から描画する。
|
|
237
|
+
*/
|
|
238
|
+
function renderCookieRows(cookies) {
|
|
239
|
+
const rows = cookies.length ? cookies : [{ name: "", value: "" }];
|
|
240
|
+
$("cookieRows").innerHTML = rows
|
|
241
|
+
.map((cookie) => cookieRowHtml(cookie))
|
|
242
|
+
.join("");
|
|
243
|
+
updateCookieVisibility();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Cookie 1行分のHTMLを作る。
|
|
248
|
+
*/
|
|
249
|
+
function cookieRowHtml(cookie) {
|
|
250
|
+
return `
|
|
251
|
+
<div class="cookie-row">
|
|
252
|
+
<input data-cookie-name placeholder="Name" value="${escapeAttr(cookie.name)}">
|
|
253
|
+
<input data-cookie-value placeholder="Value" type="${cookieValuesVisible ? "text" : "password"}" value="${escapeAttr(cookie.value)}">
|
|
254
|
+
<button type="button" title="Show or hide this value" onclick="toggleCookieRowVisible(event)">View</button>
|
|
255
|
+
<button type="button" class="danger" title="Remove cookie" onclick="removeCookieRow(event)">🗑</button>
|
|
256
|
+
</div>`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Cookie行エディタの現在値をCookieヘッダー文字列へ戻す。
|
|
261
|
+
*/
|
|
262
|
+
function currentCookieHeader() {
|
|
263
|
+
return Array.from(document.querySelectorAll(".cookie-row"))
|
|
264
|
+
.map((row) => ({
|
|
265
|
+
name: row.querySelector("[data-cookie-name]").value.trim(),
|
|
266
|
+
value: row.querySelector("[data-cookie-value]").value.trim(),
|
|
267
|
+
}))
|
|
268
|
+
.filter((cookie) => cookie.name)
|
|
269
|
+
.map((cookie) => `${cookie.name}=${cookie.value}`)
|
|
270
|
+
.join("; ");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Cookie行を1つ追加する。
|
|
275
|
+
*/
|
|
276
|
+
function addCookieRow(name = "", value = "") {
|
|
277
|
+
$("cookieRows").insertAdjacentHTML(
|
|
278
|
+
"beforeend",
|
|
279
|
+
cookieRowHtml({ name, value }),
|
|
280
|
+
);
|
|
281
|
+
updateApplyState();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Cookie値入力欄のpassword/text表示を全行まとめて反映する。
|
|
286
|
+
*/
|
|
287
|
+
function updateCookieVisibility() {
|
|
288
|
+
document.querySelectorAll("[data-cookie-value]").forEach((input) => {
|
|
289
|
+
input.type = cookieValuesVisible ? "text" : "password";
|
|
290
|
+
});
|
|
291
|
+
$("toggleCookieValues").textContent = cookieValuesVisible
|
|
292
|
+
? "Hide Values"
|
|
293
|
+
: "Show Values";
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Cookie値の全体表示/マスク表示を切り替える。
|
|
298
|
+
*/
|
|
299
|
+
function toggleCookieValuesVisible() {
|
|
300
|
+
cookieValuesVisible = !cookieValuesVisible;
|
|
301
|
+
updateCookieVisibility();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* 指定行のCookie値だけ表示/マスク表示を切り替える。
|
|
306
|
+
*/
|
|
307
|
+
function toggleCookieRowVisible(event) {
|
|
308
|
+
event.preventDefault();
|
|
309
|
+
const input = event.currentTarget
|
|
310
|
+
.closest(".cookie-row")
|
|
311
|
+
.querySelector("[data-cookie-value]");
|
|
312
|
+
input.type = input.type === "password" ? "text" : "password";
|
|
313
|
+
flash(event.currentTarget);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Cookie行を削除する。最後の1行を消した場合は空行を残す。
|
|
318
|
+
*/
|
|
319
|
+
function removeCookieRow(event) {
|
|
320
|
+
event.preventDefault();
|
|
321
|
+
const row = event.currentTarget.closest(".cookie-row");
|
|
322
|
+
row.remove();
|
|
323
|
+
if (!document.querySelector(".cookie-row")) addCookieRow();
|
|
324
|
+
updateApplyState();
|
|
325
|
+
}
|
package/src/admin.html
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="ja">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>Cookie Auth Proxy Admin</title>
|
|
7
|
+
<link rel="stylesheet" href="/admin/admin.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header><h1>Cookie Auth Proxy Admin</h1></header>
|
|
11
|
+
<div class="admin-notice hidden" id="adminNotice" role="status"></div>
|
|
12
|
+
<main>
|
|
13
|
+
<div>
|
|
14
|
+
<section>
|
|
15
|
+
<h2>Runtime</h2>
|
|
16
|
+
<div class="runtime-group">
|
|
17
|
+
<h3>Connection</h3>
|
|
18
|
+
<div class="muted" id="listen"></div>
|
|
19
|
+
<label>Client Origin</label>
|
|
20
|
+
<input id="clientOrigin" />
|
|
21
|
+
<p class="muted error-message" id="clientOriginError"></p>
|
|
22
|
+
<label>API Origin</label>
|
|
23
|
+
<input id="apiOrigin" />
|
|
24
|
+
<p class="muted error-message" id="apiOriginError"></p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="runtime-group">
|
|
28
|
+
<div class="section-head">
|
|
29
|
+
<h3>API Cookie</h3>
|
|
30
|
+
<span class="muted" id="cookieNote"></span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="cookie-header" aria-hidden="true">
|
|
33
|
+
<span>Name</span>
|
|
34
|
+
<span>Value</span>
|
|
35
|
+
<span></span>
|
|
36
|
+
<span></span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="cookie-list" id="cookieRows"></div>
|
|
39
|
+
<div class="row cookie-actions">
|
|
40
|
+
<button id="addCookie">Add Cookie</button>
|
|
41
|
+
<button id="toggleCookieValues">Show Values</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="runtime-group runtime-command-group">
|
|
46
|
+
<div class="row">
|
|
47
|
+
<button class="primary" id="saveState" disabled>Apply</button>
|
|
48
|
+
<button id="refresh">Refresh</button>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="runtime-actions">
|
|
51
|
+
<button id="openApp">
|
|
52
|
+
Open App
|
|
53
|
+
<span class="external-link-icon" aria-hidden="true"
|
|
54
|
+
>↗</span
|
|
55
|
+
>
|
|
56
|
+
</button>
|
|
57
|
+
<button id="openServerApp">
|
|
58
|
+
Open Server App
|
|
59
|
+
<span class="external-link-icon" aria-hidden="true"
|
|
60
|
+
>↗</span
|
|
61
|
+
>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<section class="main-panel" id="mainPanel">
|
|
69
|
+
<div class="panel active" id="historyPanel">
|
|
70
|
+
<div class="panel-sticky">
|
|
71
|
+
<div class="tabs">
|
|
72
|
+
<button class="tab active" data-tab="historyPanel">History</button>
|
|
73
|
+
<button class="tab" data-tab="stubsPanel">Stubs</button>
|
|
74
|
+
<button class="tab" data-tab="overridesPanel">Overrides</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="toolbar">
|
|
77
|
+
<h2>API History</h2>
|
|
78
|
+
<div class="filters">
|
|
79
|
+
<button class="active" data-status-filter="all">All</button>
|
|
80
|
+
<button data-status-filter="2">2xx</button>
|
|
81
|
+
<button data-status-filter="3">3xx</button>
|
|
82
|
+
<button data-status-filter="4">4xx</button>
|
|
83
|
+
<button data-status-filter="5">5xx</button>
|
|
84
|
+
<button data-status-filter="other">Other</button>
|
|
85
|
+
</div>
|
|
86
|
+
<button id="clearHistory">Clear</button>
|
|
87
|
+
</div>
|
|
88
|
+
<label>Path Filter Regex</label>
|
|
89
|
+
<input id="pathFilter" placeholder="^/api/" />
|
|
90
|
+
</div>
|
|
91
|
+
<div class="scroll-area" id="historyScroll">
|
|
92
|
+
<div class="history" id="history"></div>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="panel" id="stubsPanel">
|
|
97
|
+
<div class="panel-sticky">
|
|
98
|
+
<div class="tabs">
|
|
99
|
+
<button class="tab" data-tab="historyPanel">History</button>
|
|
100
|
+
<button class="tab active" data-tab="stubsPanel">Stubs</button>
|
|
101
|
+
<button class="tab" data-tab="overridesPanel">Overrides</button>
|
|
102
|
+
</div>
|
|
103
|
+
<div class="toolbar">
|
|
104
|
+
<h2>Stub Settings</h2>
|
|
105
|
+
<div class="toolbar-group">
|
|
106
|
+
<label class="switch">
|
|
107
|
+
<span>All stubs</span>
|
|
108
|
+
<input id="stubsEnabled" type="checkbox" checked />
|
|
109
|
+
<span class="slider"></span>
|
|
110
|
+
</label>
|
|
111
|
+
<input
|
|
112
|
+
id="importFile"
|
|
113
|
+
type="file"
|
|
114
|
+
accept="application/json,.json"
|
|
115
|
+
style="display: none"
|
|
116
|
+
/>
|
|
117
|
+
<button id="exportStubs">Export</button>
|
|
118
|
+
<button class="danger" id="cleanImportStubs">
|
|
119
|
+
Clean Import
|
|
120
|
+
</button>
|
|
121
|
+
<button id="addImportStubs">Add Import</button>
|
|
122
|
+
<button class="primary" id="addStub">Add Stub</button>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<label>Filter</label>
|
|
126
|
+
<input id="stubFilter" placeholder="name / method / path" />
|
|
127
|
+
</div>
|
|
128
|
+
<div class="scroll-area" id="stubsScroll">
|
|
129
|
+
<div class="stubs" id="stubs"></div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="panel" id="overridesPanel">
|
|
134
|
+
<div class="panel-sticky">
|
|
135
|
+
<div class="tabs">
|
|
136
|
+
<button class="tab" data-tab="historyPanel">History</button>
|
|
137
|
+
<button class="tab" data-tab="stubsPanel">Stubs</button>
|
|
138
|
+
<button class="tab active" data-tab="overridesPanel">Overrides</button>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="toolbar">
|
|
141
|
+
<h2>JSON Overrides</h2>
|
|
142
|
+
<div class="toolbar-group">
|
|
143
|
+
<label class="switch">
|
|
144
|
+
<span>All overrides</span>
|
|
145
|
+
<input id="jsonOverridesEnabled" type="checkbox" checked />
|
|
146
|
+
<span class="slider"></span>
|
|
147
|
+
</label>
|
|
148
|
+
<input
|
|
149
|
+
id="overrideImportFile"
|
|
150
|
+
type="file"
|
|
151
|
+
accept="application/json,.json"
|
|
152
|
+
style="display: none"
|
|
153
|
+
/>
|
|
154
|
+
<button id="exportOverrides">Export</button>
|
|
155
|
+
<button class="danger" id="cleanImportOverrides">
|
|
156
|
+
Clean Import
|
|
157
|
+
</button>
|
|
158
|
+
<button id="addImportOverrides">Add Import</button>
|
|
159
|
+
<button class="primary" id="addOverride">Add Override</button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
<label>Filter</label>
|
|
163
|
+
<input id="overrideFilter" placeholder="name / method / path" />
|
|
164
|
+
</div>
|
|
165
|
+
<div class="scroll-area" id="overridesScroll">
|
|
166
|
+
<div class="overrides" id="overrides"></div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</section>
|
|
170
|
+
</main>
|
|
171
|
+
|
|
172
|
+
<script src="/admin/state.js"></script>
|
|
173
|
+
<script src="/admin/renderers.js"></script>
|
|
174
|
+
<script src="/admin/editors.js"></script>
|
|
175
|
+
<script src="/admin/files.js"></script>
|
|
176
|
+
<script src="/admin/events.js"></script>
|
|
177
|
+
</body>
|
|
178
|
+
</html>
|