querysub 0.525.0 → 0.527.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.
@@ -1,354 +0,0 @@
1
- module.allowclient = true;
2
-
3
- import { qreact } from "../../4-dom/qreact";
4
- import { css } from "typesafecss";
5
- import { SocketFunction } from "socket-function/SocketFunction";
6
- import { getBrowserUrlNode } from "../../-f-node-discovery/NodeDiscovery";
7
- import { getSyncedController } from "../../library-components/SyncedController";
8
- import { assertIsManagementUser } from "../managementPages";
9
- import { t } from "../../2-proxy/schema2";
10
- import { Querysub } from "../../4-querysub/Querysub";
11
- import { sort, timeInDay } from "socket-function/src/misc";
12
- import { isNode } from "typesafecss";
13
- import { formatDateJSX } from "../../misc/formatJSX";
14
-
15
- const RECORDS_PER_PAGE = 1000;
16
- const HEX_SUBDOMAIN_LENGTH = 17;
17
- const STALE_HEX_SUBDOMAIN_AGE = timeInDay * 7;
18
- const HEX_SUBDOMAIN_REGEX = new RegExp(`^(\\*\\.)?[0-9a-f]{${HEX_SUBDOMAIN_LENGTH}}\\..+$`);
19
-
20
- export type DNSRecord = {
21
- id: string;
22
- zoneId: string;
23
- zoneName: string;
24
- type: string;
25
- name: string;
26
- content: string;
27
- proxied: boolean;
28
- ttl: number;
29
- createdOn: number;
30
- modifiedOn: number;
31
- // Time we last asserted this record, parsed from our freshness comment tag, falling back to
32
- // Cloudflare's modified_on when the tag is absent. This is the time used for staleness.
33
- setOn: number;
34
- };
35
-
36
- class DNSPageControllerBase {
37
- public async getAllRecords(): Promise<DNSRecord[]> {
38
- if (!isNode()) throw new Error(`getAllRecords must be called serverside`);
39
- const { cloudflareGETCall } = await import("../../-b-authorities/cloudflareHelpers");
40
- const { freshnessTime } = await import("../../-b-authorities/dnsAuthority");
41
- const { getDomain } = await import("../../config");
42
-
43
- let zones = await cloudflareGETCall<{ id: string; name: string }[]>("/zones", {
44
- per_page: String(RECORDS_PER_PAGE),
45
- });
46
- let domain = getDomain();
47
- zones = zones.filter(z => z.name === domain);
48
-
49
- let allRecords: DNSRecord[] = [];
50
- for (let zone of zones) {
51
- let page = 1;
52
- while (true) {
53
- let records = await cloudflareGETCall<{
54
- id: string;
55
- type: string;
56
- name: string;
57
- content: string;
58
- proxied: boolean;
59
- ttl: number;
60
- created_on: string;
61
- modified_on: string;
62
- comment?: string;
63
- }[]>(`/zones/${zone.id}/dns_records`, {
64
- per_page: String(RECORDS_PER_PAGE),
65
- page: String(page),
66
- });
67
- for (let record of records) {
68
- let modifiedOn = new Date(record.modified_on).getTime();
69
- allRecords.push({
70
- id: record.id,
71
- zoneId: zone.id,
72
- zoneName: zone.name,
73
- type: record.type,
74
- name: record.name,
75
- content: record.content,
76
- proxied: record.proxied,
77
- ttl: record.ttl,
78
- createdOn: new Date(record.created_on).getTime(),
79
- modifiedOn,
80
- setOn: freshnessTime(record.comment) || modifiedOn,
81
- });
82
- }
83
- if (records.length < RECORDS_PER_PAGE) break;
84
- page++;
85
- }
86
- }
87
- return allRecords;
88
- }
89
-
90
- public async deleteRecord(config: { zoneId: string; recordId: string }): Promise<void> {
91
- if (!isNode()) throw new Error(`deleteRecord must be called serverside`);
92
- const { cloudflareCall } = await import("../../-b-authorities/cloudflareHelpers");
93
- await cloudflareCall(`/zones/${config.zoneId}/dns_records/${config.recordId}`, Buffer.from([]), "DELETE");
94
- }
95
-
96
- public async deleteRecordsByContent(config: { content: string }): Promise<number> {
97
- if (!isNode()) throw new Error(`deleteRecordsByContent must be called serverside`);
98
- const { cloudflareCall } = await import("../../-b-authorities/cloudflareHelpers");
99
- let all = await this.getAllRecords();
100
- let matching = all.filter(x => x.content === config.content);
101
- for (let record of matching) {
102
- await cloudflareCall(`/zones/${record.zoneId}/dns_records/${record.id}`, Buffer.from([]), "DELETE");
103
- }
104
- return matching.length;
105
- }
106
- }
107
-
108
- export const DNSPageController = SocketFunction.register(
109
- "DNSPageController-3e7b1d92-44a8-4f7e-9bba-d12c4f87a6e0",
110
- new DNSPageControllerBase(),
111
- () => ({
112
- getAllRecords: {},
113
- deleteRecord: {},
114
- deleteRecordsByContent: {},
115
- }),
116
- () => ({
117
- hooks: [assertIsManagementUser],
118
- }),
119
- {
120
- noAutoExpose: true,
121
- }
122
- );
123
-
124
- const DNSPageSynced = getSyncedController(DNSPageController, {
125
- reads: {
126
- getAllRecords: ["dnsRecords"],
127
- },
128
- writes: {
129
- deleteRecord: ["dnsRecords"],
130
- deleteRecordsByContent: ["dnsRecords"],
131
- },
132
- });
133
-
134
- export class DNSPage extends qreact.Component {
135
- state = t.state({
136
- expandedGroups: t.lookup(t.boolean),
137
- busyKeys: t.lookup(t.boolean),
138
- errorMessage: t.string,
139
- });
140
-
141
- private async deleteOne(record: DNSRecord) {
142
- let typed = prompt(`Type the content "${record.content}" to confirm deleting this record:\n\n${record.type} ${record.name} → ${record.content}`);
143
- if (typed === null) return;
144
- if (typed !== record.content) {
145
- alert(`Confirmation does not match. Aborted.`);
146
- return;
147
- }
148
- let busyKey = `record:${record.id}`;
149
- Querysub.commit(() => {
150
- this.state.busyKeys[busyKey] = true;
151
- this.state.errorMessage = "";
152
- });
153
- try {
154
- await DNSPageSynced(getBrowserUrlNode()).deleteRecord.promise({
155
- zoneId: record.zoneId,
156
- recordId: record.id,
157
- });
158
- } catch (err) {
159
- console.error(`DNS deleteRecord failed:`, (err as Error).stack ?? err);
160
- Querysub.commit(() => {
161
- this.state.errorMessage = (err as Error).stack ?? String(err);
162
- });
163
- } finally {
164
- Querysub.commit(() => {
165
- delete this.state.busyKeys[busyKey];
166
- });
167
- }
168
- }
169
-
170
- private async deleteStaleHexRecords(content: string, matching: DNSRecord[]) {
171
- let listText = matching.map(r => ` ${r.type} ${r.name} (set ${new Date(r.setOn).toISOString()})`).join("\n");
172
- let typed = prompt(`Type "${content}" to confirm deleting ${matching.length} stale hex-subdomain record(s):\n\n${listText}`);
173
- if (typed === null) return;
174
- if (typed !== content) {
175
- alert(`Confirmation does not match. Aborted.`);
176
- return;
177
- }
178
- let busyKey = `staleHex:${content}`;
179
- Querysub.commit(() => {
180
- this.state.busyKeys[busyKey] = true;
181
- this.state.errorMessage = "";
182
- });
183
- try {
184
- for (let record of matching) {
185
- await DNSPageSynced(getBrowserUrlNode()).deleteRecord.promise({
186
- zoneId: record.zoneId,
187
- recordId: record.id,
188
- });
189
- }
190
- } catch (err) {
191
- console.error(`DNS deleteStaleHexRecords failed:`, (err as Error).stack ?? err);
192
- Querysub.commit(() => {
193
- this.state.errorMessage = (err as Error).stack ?? String(err);
194
- });
195
- } finally {
196
- Querysub.commit(() => {
197
- delete this.state.busyKeys[busyKey];
198
- });
199
- }
200
- }
201
-
202
- private async deleteGroup(content: string, count: number) {
203
- let typed = prompt(`Type "${content}" to confirm deleting ALL ${count} record(s) pointing to it:`);
204
- if (typed === null) return;
205
- if (typed !== content) {
206
- alert(`Confirmation does not match. Aborted.`);
207
- return;
208
- }
209
- let busyKey = `group:${content}`;
210
- Querysub.commit(() => {
211
- this.state.busyKeys[busyKey] = true;
212
- this.state.errorMessage = "";
213
- });
214
- try {
215
- await DNSPageSynced(getBrowserUrlNode()).deleteRecordsByContent.promise({ content });
216
- } catch (err) {
217
- console.error(`DNS deleteRecordsByContent failed:`, (err as Error).stack ?? err);
218
- Querysub.commit(() => {
219
- this.state.errorMessage = (err as Error).stack ?? String(err);
220
- });
221
- } finally {
222
- Querysub.commit(() => {
223
- delete this.state.busyKeys[busyKey];
224
- });
225
- }
226
- }
227
-
228
- render() {
229
- let records = DNSPageSynced(getBrowserUrlNode()).getAllRecords();
230
- if (!records) {
231
- return <div className={css.pad2(16)}>Loading DNS records...</div>;
232
- }
233
-
234
- let groups = new Map<string, DNSRecord[]>();
235
- for (let record of records) {
236
- let list = groups.get(record.content);
237
- if (!list) {
238
- list = [];
239
- groups.set(record.content, list);
240
- }
241
- list.push(record);
242
- }
243
-
244
- let groupEntries = Array.from(groups.entries()).map(([content, list]) => ({ content, records: list }));
245
- sort(groupEntries, x => -x.records.length);
246
- for (let group of groupEntries) {
247
- sort(group.records, r => -r.setOn);
248
- }
249
-
250
- return <div className={css.vbox(12).pad2(16).fillWidth}>
251
- <div className={css.hbox(12).alignItems("center")}>
252
- <h2 className={css.flexGrow(1)}>DNS Records ({records.length} total, {groupEntries.length} unique contents)</h2>
253
- </div>
254
- {this.state.errorMessage && <pre className={css.colorhsl(0, 60, 40).whiteSpace("pre-wrap").pad2(8).bord2(0, 60, 60).hsl(0, 50, 95)}>{this.state.errorMessage}</pre>}
255
- <div className={css.vbox(8).fillWidth}>
256
- {groupEntries.map(({ content, records: list }) => {
257
- let expanded = !!this.state.expandedGroups[content];
258
- let groupBusy = !!this.state.busyKeys[`group:${content}`];
259
- return <div key={content} className={css.vbox(0).fillWidth.bord2(0, 0, 80).hsl(0, 0, 99)}>
260
- <div className={css.hbox(10).alignItems("center").pad2(10).button}
261
- onClick={() => {
262
- if (this.state.expandedGroups[content]) {
263
- delete this.state.expandedGroups[content];
264
- } else {
265
- this.state.expandedGroups[content] = true;
266
- }
267
- }}
268
- >
269
- <span>{expanded ? "▼" : "▶"}</span>
270
- <span
271
- className={css.boldStyle.fontFamily("monospace").button.pad2(4, 2).bord2(0, 0, 80).hsl(0, 0, 100).hbox(6).alignItems("center")}
272
- title="Click to copy"
273
- onClick={(e) => {
274
- e.stopPropagation();
275
- void navigator.clipboard.writeText(content);
276
- }}
277
- >
278
- <span>{content}</span>
279
- <span className={css.colorhsl(0, 0, 50)}>📋</span>
280
- </span>
281
- <span className={css.colorhsl(0, 0, 40)}>{list.length} record(s)</span>
282
- <div className={css.flexGrow(1)} />
283
- <button
284
- className={css.pad2(10, 6).button.bord2(0, 80, 50)
285
- + (groupBusy ? css.hsl(0, 0, 90).colorhsl(0, 0, 50) : css.hsl(0, 80, 92).colorhsl(0, 80, 30))}
286
- disabled={groupBusy}
287
- onClick={(e) => {
288
- e.stopPropagation();
289
- void this.deleteGroup(content, list.length);
290
- }}
291
- >
292
- {groupBusy ? "Deleting..." : `🗑️ Delete all ${list.length}`}
293
- </button>
294
- </div>
295
- {expanded && <div className={css.vbox(4).pad2(10).hsl(0, 0, 100)}>
296
- {(() => {
297
- let staleHex = list.filter(r =>
298
- HEX_SUBDOMAIN_REGEX.test(r.name)
299
- && Date.now() - r.setOn > STALE_HEX_SUBDOMAIN_AGE
300
- );
301
- if (staleHex.length === 0) return undefined;
302
- let staleHexBusy = !!this.state.busyKeys[`staleHex:${content}`];
303
- return <div className={css.vbox(4).pad2(8).bord2(30, 60, 60).hsl(30, 70, 96)}>
304
- <div className={css.hbox(10).alignItems("center")}>
305
- <span className={css.boldStyle}>Stale hex-subdomain records (set &gt;7d ago): {staleHex.length}</span>
306
- <div className={css.flexGrow(1)} />
307
- <button
308
- className={css.pad2(10, 6).button.bord2(0, 80, 50)
309
- + (staleHexBusy ? css.hsl(0, 0, 90).colorhsl(0, 0, 50) : css.hsl(0, 80, 92).colorhsl(0, 80, 30))}
310
- disabled={staleHexBusy}
311
- onClick={() => void this.deleteStaleHexRecords(content, staleHex)}
312
- >
313
- {staleHexBusy ? "Deleting..." : `🗑️ Delete ${staleHex.length} stale hex record(s)`}
314
- </button>
315
- </div>
316
- <div className={css.vbox(2).fontFamily("monospace").colorhsl(0, 0, 30)}>
317
- {staleHex.map(r => <div key={r.id}>{r.type} {r.name} — set {formatDateJSX(r.setOn)}</div>)}
318
- </div>
319
- </div>;
320
- })()}
321
- {list.map(record => {
322
- let recordBusy = !!this.state.busyKeys[`record:${record.id}`];
323
- return <div key={record.id} className={css.hbox(10).alignItems("center").pad2(6).bord2(0, 0, 92)}>
324
- <span className={css.boldStyle.minWidth(50)}>{record.type}</span>
325
- <span className={css.fontFamily("monospace").flexGrow(1)}>{record.name}</span>
326
- {record.proxied && <span className={css.colorhsl(30, 80, 40).pad2(4, 2).bord2(30, 80, 70).hsl(30, 80, 95)}>proxied</span>}
327
- <span className={css.colorhsl(0, 0, 50)}>ttl {record.ttl}</span>
328
- <span className={css.colorhsl(0, 0, 50)}>
329
- created {formatDateJSX(record.createdOn)}
330
- </span>
331
- <span className={css.colorhsl(0, 0, 50)}>
332
- modified {formatDateJSX(record.modifiedOn)}
333
- </span>
334
- <span className={css.colorhsl(0, 0, 50)}>
335
- set {formatDateJSX(record.setOn)}
336
- </span>
337
- <span className={css.colorhsl(0, 0, 50)}>{record.zoneName}</span>
338
- <button
339
- className={css.pad2(8, 4).button.bord2(0, 0, 60)
340
- + (recordBusy ? css.hsl(0, 0, 90).colorhsl(0, 0, 50) : css.hsl(0, 0, 100))}
341
- disabled={recordBusy}
342
- onClick={() => void this.deleteOne(record)}
343
- >
344
- {recordBusy ? "Deleting..." : "🗑️ Delete"}
345
- </button>
346
- </div>;
347
- })}
348
- </div>}
349
- </div>;
350
- })}
351
- </div>
352
- </div>;
353
- }
354
- }