Skip to content

Commit 8a1811b

Browse files
committed
feat(cloudflare): batch ruleset management
1 parent 90448b8 commit 8a1811b

File tree

5 files changed

+257
-28
lines changed

5 files changed

+257
-28
lines changed

assets/cloudflare.html

+156-12
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,35 @@
4242
<span v-if="loadDNSRecordsLoading" class="loading loading-spinner"></span>
4343
Load Selected Zone's DNS Records
4444
</button>
45-
<button class="btn btn-sm" :disabled="!hasSelectedZone || loadPageRulesLoading"
46-
@click="loadPageRules">
47-
<span v-if="loadPageRulesLoading" class="loading loading-spinner"></span>
48-
Load Selected Zone's Page Rules
49-
</button>
5045
<button class="btn btn-sm" :disabled="!hasSelectedRecord || deleteDNSRecordsLoading"
5146
@click="deleteDNSRecords">
5247
<span v-if="deleteDNSRecordsLoading" class="loading loading-spinner"></span>
5348
Delete Selected DNS Records
5449
</button>
50+
<button class="btn btn-sm" :disabled="!hasSelectedZone || loadPageRulesLoading"
51+
@click="loadPageRules">
52+
<span v-if="loadPageRulesLoading" class="loading loading-spinner"></span>
53+
Load Selected Zone's Page Rules
54+
</button>
5555
<button class="btn btn-sm" :disabled="!hasSelectedPageRule || deletePageRulesLoading"
5656
@click="deletePageRules">
5757
<span v-if="deletePageRulesLoading" class="loading loading-spinner"></span>
5858
Delete Selected Page Rules
5959
</button>
60+
<button class="btn btn-sm" :disabled="!hasSelectedZone || loadRulesetsLoading"
61+
@click="loadRulesets">
62+
<span v-if="loadRulesetsLoading" class="loading loading-spinner"></span>
63+
Load Selected Zone's Rulesets
64+
</button>
65+
<button class="btn btn-sm" :disabled="!hasSelectedRulesets || deleteRulesetsLoading"
66+
@click="deleteRulesets">
67+
<span v-if="deleteRulesetsLoading" class="loading loading-spinner"></span>
68+
Delete Selected Ruleset
69+
</button>
70+
<p class="mt-2 text-sm">
71+
Permissions Required: Zone_DNS, Account_AccountRulesets,
72+
Zone_DynamicRedirect, Zone_PageRules
73+
</p>
6074
</div>
6175
</div>
6276

@@ -67,7 +81,7 @@
6781
<div class="label">
6882
<span class="label-text">Name</span>
6983
</div>
70-
<input v-model="newDNSRecord.name" type="text" placeholder="@.example.com"
84+
<input v-model="newDNSRecord.name" type="text" placeholder="@"
7185
class="input input-bordered" />
7286
</label>
7387
<label class="form-control">
@@ -106,7 +120,7 @@
106120
<span v-if="batchCreateDNSRecordLoading" class="loading loading-spinner"></span>
107121
Batch Create
108122
</button>
109-
<p class="mt-2">
123+
<p class="mt-2 text-sm">
110124
<i>* <code>#DOMAIN#</code> in content or name will be replaced with the domain name. eg.
111125
<code>example.com</code></i><br>
112126
<i>* <code>#DOMAIN.SUFFIX#</code> in content or name will be replaced with the domain
@@ -124,13 +138,15 @@
124138
<div class="label">
125139
<span class="label-text">Actions</span>
126140
</div>
127-
<textarea v-model="newPageRule.actions" type="text"class="textarea textarea-bordered"></textarea>
141+
<textarea v-model="newPageRule.actions" type="text"
142+
class="textarea textarea-bordered"></textarea>
128143
</label>
129144
<label class="form-control">
130145
<div class="label">
131146
<span class="label-text">Targets</span>
132147
</div>
133-
<textarea v-model="newPageRule.targets" type="text"class="textarea textarea-bordered"></textarea>
148+
<textarea v-model="newPageRule.targets" type="text"
149+
class="textarea textarea-bordered"></textarea>
134150
</label>
135151
<label class="form-control">
136152
<div class="label">
@@ -143,7 +159,7 @@
143159
<span v-if="batchCreatePageRuleLoading" class="loading loading-spinner"></span>
144160
Batch Create
145161
</button>
146-
<p class="mt-2">
162+
<p class="mt-2 text-sm">
147163
<i>* <code>#DOMAIN#</code> in content or name will be replaced with the domain name. eg.
148164
<code>example.com</code></i><br>
149165
<i>* <code>#DOMAIN.SUFFIX#</code> in content or name will be replaced with the domain
@@ -153,6 +169,27 @@
153169
without the last part. eg. <code>example</code></i>
154170
</p>
155171
</div>
172+
173+
<input type="radio" name="my_tabs_2" role="tab" class="tab min-w-max"
174+
aria-label="Batch Create Ruleset" />
175+
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6">
176+
<textarea v-model="newRuleset" type="text"
177+
class="textarea textarea-bordered block w-full"></textarea>
178+
<button class="btn btn-sm mt-2" :disabled="!hasSelectedZone || batchCreateRulesetLoading"
179+
@click="batchCreateRuleset">
180+
<span v-if="batchCreateRulesetLoading" class="loading loading-spinner"></span>
181+
Batch Create
182+
</button>
183+
<p class="mt-2 text-sm">
184+
<i>* <code>#DOMAIN#</code> in content will be replaced with the domain name. eg.
185+
<code>example.com</code></i><br>
186+
<i>* <code>#DOMAIN.SUFFIX#</code> in content will be replaced with the domain
187+
name without the first part. eg. <code>com</code></i><br>
188+
<i>* <code>#DOMAIN.PREFIX#</code> in content will be replaced with the domain
189+
name
190+
without the last part. eg. <code>example</code></i>
191+
</p>
192+
</div>
156193
</div>
157194
</div>
158195
<div class="overflow-x-scroll mt-2">
@@ -166,6 +203,8 @@
166203
@click="toggleSelectAllRecords" />
167204
<input type="checkbox" class="ml-2 checkbox checkbox-xs" :checked="selectedAllPageRules"
168205
@click="toggleSelectAllPageRules" />
206+
<input type="checkbox" class="ml-2 checkbox checkbox-xs" :checked="selectedAllRulesets"
207+
@click="toggleSelectAllRulesets" />
169208
</th>
170209
<th>Name</th>
171210
<th>Status</th>
@@ -221,6 +260,21 @@
221260
<td></td>
222261
<td></td>
223262
</tr>
263+
<tr class="bg-primary-200" v-for="(ruleset,sIndex) in rulesets[zone.id]" :key="ruleset.id">
264+
<th>
265+
<input type="checkbox" class="ml-[4.5rem] checkbox checkbox-xs"
266+
:checked="ruleset.selected" @click="toggleSelectRuleset(zone.id,sIndex)" />
267+
</th>
268+
<td>
269+
<div class="badge badge-sm badge-info badge-outline">Ruleset</div>
270+
</td>
271+
<td>{{ ruleset.kind }}</td>
272+
<td>
273+
</td>
274+
<td class="max-w-40 text-ellipsis overflow-hidden">{{ ruleset.phase }}</td>
275+
<td></td>
276+
<td></td>
277+
</tr>
224278
</template>
225279
</tbody>
226280
</table>
@@ -234,29 +288,36 @@
234288
const selectedAll = ref(false)
235289
const selectedAllRecords = ref(false)
236290
const selectedAllPageRules = ref(false)
291+
const selectedAllRulesets = ref(false)
237292
const deleteDNSRecordsLoading = ref(false)
238293
const deletePageRulesLoading = ref(false)
294+
const deleteRulesetsLoading = ref(false)
239295
const loadDNSRecordsLoading = ref(false)
240296
const loadPageRulesLoading = ref(false)
297+
const loadRulesetsLoading = ref(false)
241298
const zones = ref(null)
242299
const dnsRecords = ref({})
243300
const pageRules = ref({})
301+
const rulesets = ref({})
244302
const apiToken = ref(null)
245303
const newDNSRecord = ref({
246304
type: 'A',
247305
ttl: 1,
248306
})
249307
const newPageRule = ref({
250-
actions: '[{"id":"forwarding_url","value":{"status_code":302,"url":"https://bigtoyscompany.com/assets/5.nu?utm_source=5.nu"}}]',
251-
targets: '[{"target":"url","constraint":{"operator":"matches","value":"5.nu/*"}}]',
308+
actions: '[{"id":"forwarding_url","value":{"status_code":302,"url":"https://example.com/assets/#DOMAIN#?utm_source#DOMAIN#"}}]',
309+
targets: '[{"target":"url","constraint":{"operator":"matches","value":"#DOMAIN#/*"}}]',
252310
priority: 1,
253311
})
312+
const newRuleset = ref('{"name":"Domain Portfolio","kind":"zone","phase":"http_request_dynamic_redirect","description":"domain_portfolio","rules":[{"action":"redirect","action_parameters":{"from_value":{"status_code":302,"target_url":{"value":"https://example.com/assets/#DOMAIN#?utm_source=#DOMAIN#"},"preserve_query_string":false}},"description":"domain_portfolio","expression":"true","enabled":true}]}')
254313
const batchCreateDNSRecordLoading = ref(false)
255314
const batchCreatePageRuleLoading = ref(false)
315+
const batchCreateRulesetLoading = ref(false)
256316

257317
const hasSelectedZone = computed(() => zones.value && zones.value.some(zone => zone.selected))
258318
const hasSelectedRecord = computed(() => Object.values(dnsRecords.value).some(records => records.some(record => record.selected)))
259319
const hasSelectedPageRule = computed(() => Object.values(pageRules.value).some(records => records.some(record => record.selected)))
320+
const hasSelectedRulesets = computed(() => Object.values(rulesets.value).some(records => records.some(record => record.selected)))
260321

261322
fetch('/state').then(async response => {
262323
if (response.ok) {
@@ -265,6 +326,7 @@
265326
zones.value = data.zones
266327
dnsRecords.value = data.dnsRecords
267328
pageRules.value = data.pageRules
329+
rulesets.value = data.rulesets
268330
}
269331
})
270332

@@ -301,6 +363,11 @@
301363
Object.values(pageRules.value).forEach(records => records.forEach(record => record.selected = selectedAllPageRules.value))
302364
}
303365

366+
const toggleSelectAllRulesets = () => {
367+
selectedAllRulesets.value = !selectedAllRulesets.value
368+
Object.values(rulesets.value).forEach(records => records.forEach(record => record.selected = selectedAllRulesets.value))
369+
}
370+
304371
const toggleSelect = (index) => {
305372
zones.value[index].selected = !zones.value[index].selected
306373
selectedAll.value = zones.value.every(zone => zone.selected)
@@ -316,6 +383,11 @@
316383
selectedAllPageRules.value = Object.values(pageRules.value).every(records => records.every(record => record.selected))
317384
}
318385

386+
const toggleSelectRuleset = (zoneId, rIndex) => {
387+
rulesets.value[zoneId][rIndex].selected = !rulesets.value[zoneId][rIndex].selected
388+
selectedAllRulesets.value = Object.values(rulesets.value).every(records => records.every(record => record.selected))
389+
}
390+
319391
const loadPageRules = (zone) => {
320392
loadPageRulesLoading.value = true
321393
fetch(`/load-zone-page-rules`, {
@@ -348,6 +420,22 @@
348420
})
349421
}
350422

423+
const loadRulesets = (zone) => {
424+
loadRulesetsLoading.value = true
425+
fetch(`/load-zone-rulesets`, {
426+
method: 'POST',
427+
body: JSON.stringify(zones.value.filter(zone => zone.selected).map(zone => zone.id)),
428+
}).then(async response => {
429+
if (!response.ok) {
430+
alert(await response.text())
431+
return
432+
}
433+
rulesets.value = await response.json()
434+
}).finally(() => {
435+
loadRulesetsLoading.value = false
436+
})
437+
}
438+
351439
const deleteDNSRecords = () => {
352440
deleteDNSRecordsLoading.value = true
353441
const data = {}
@@ -398,6 +486,31 @@
398486
})
399487
}
400488

489+
const deleteRulesets = () => {
490+
deleteRulesetsLoading.value = true
491+
const data = {}
492+
Object.entries(rulesets.value).forEach(([zoneId, records]) => {
493+
data[zoneId] = records.filter(record => record.selected).map(record => record.id)
494+
})
495+
Object.entries(data).forEach(([zoneId, records]) => {
496+
if (records.length === 0) {
497+
delete data[zoneId]
498+
}
499+
})
500+
fetch('/delete-rulesets', {
501+
method: 'POST',
502+
body: JSON.stringify(data),
503+
}).then(async response => {
504+
if (!response.ok) {
505+
alert(await response.text())
506+
return
507+
}
508+
alert('Purge Rulesets success, please reload Rulesets to see the changes.')
509+
}).finally(() => {
510+
deleteRulesetsLoading.value = false
511+
})
512+
}
513+
401514
const batchCreateDNSRecord = () => {
402515
batchCreateDNSRecordLoading.value = true
403516
fetch('/batch-create-dns-record', {
@@ -440,6 +553,25 @@
440553
})
441554
}
442555

556+
const batchCreateRuleset = () => {
557+
batchCreateRulesetLoading.value = true
558+
fetch('/batch-create-ruleset', {
559+
method: 'POST',
560+
body: JSON.stringify({
561+
zones: zones.value.filter(zone => zone.selected).map(zone => zone.id),
562+
ruleset: newRuleset.value
563+
}),
564+
}).then(async response => {
565+
if (!response.ok) {
566+
alert(await response.text())
567+
return
568+
}
569+
alert('Create Rulesets success, please reload Rulesets to see the changes.')
570+
}).finally(() => {
571+
batchCreateRulesetLoading.value = false
572+
})
573+
}
574+
443575
return {
444576
checkApiToken,
445577
zones,
@@ -472,6 +604,18 @@
472604
newPageRule,
473605
batchCreatePageRule,
474606
batchCreatePageRuleLoading,
607+
rulesets,
608+
loadRulesets,
609+
loadRulesetsLoading,
610+
selectedAllRulesets,
611+
toggleSelectAllRulesets,
612+
toggleSelectRuleset,
613+
deleteRulesets,
614+
deleteRulesetsLoading,
615+
hasSelectedRulesets,
616+
newRuleset,
617+
batchCreateRuleset,
618+
batchCreateRulesetLoading,
475619
}
476620
}
477621
}).mount('#app')

cmd/cloudflare.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ func init() {
1717
}
1818

1919
var cloudflareCmd = &cli.Command{
20-
Name: "cloudflare",
21-
Usage: "Cloudflare helpers.",
20+
Name: "cloudflare",
21+
Description: "Run a web interface for bulk management of DNS records, Page Rules, and Rulesets.",
2222
Action: func(c *cli.Context) error {
2323
listener, err := net.Listen("tcp", "127.0.0.1:0")
2424
if err != nil {
@@ -29,10 +29,13 @@ var cloudflareCmd = &cli.Command{
2929
mux.Handle("/state", http.HandlerFunc(internal.Cloudflared.State))
3030
mux.Handle("/load-zone-records", http.HandlerFunc(internal.Cloudflared.LoadZoneRecords))
3131
mux.Handle("/load-zone-page-rules", http.HandlerFunc(internal.Cloudflared.LoadZonePageRules))
32+
mux.Handle("/load-zone-rulesets", http.HandlerFunc(internal.Cloudflared.LoadZoneRulesets))
3233
mux.Handle("/delete-dns-records", http.HandlerFunc(internal.Cloudflared.DeleteDNSRecords))
3334
mux.Handle("/delete-page-rules", http.HandlerFunc(internal.Cloudflared.DeletePageRules))
35+
mux.Handle("/delete-rulesets", http.HandlerFunc(internal.Cloudflared.DeleteRulesets))
3436
mux.Handle("/batch-create-dns-record", http.HandlerFunc(internal.Cloudflared.BatchCreteDNSRecord))
3537
mux.Handle("/batch-create-page-rule", http.HandlerFunc(internal.Cloudflared.BatchCreatePageRule))
38+
mux.Handle("/batch-create-ruleset", http.HandlerFunc(internal.Cloudflared.BatchCreateRuleset))
3639
mux.Handle("/check-token", http.HandlerFunc(internal.Cloudflared.CheckToken))
3740
var errCh = make(chan error)
3841
go func() {

0 commit comments

Comments
 (0)