1use super::http::{AuthStrategy, HttpError, RequestFactory, ResponseBytes};
2use super::models::{
3 CloudflareDnsRecord, CloudflareDnsRecordBatch, CloudflareDnsRecordWrite, CloudflareDnsSettings,
4 CloudflareDnssec, CloudflareDnssecEdit, CloudflareEnvelope, CloudflareZone,
5 CloudflareZoneFilters, DomainAvailability, PaginationInfo, RegisteredDomain, SecretRecord,
6 SecretsQuota, SecretsStore,
7};
8use reqwest::header::{HeaderName, HeaderValue};
9use serde::{Deserialize, Serialize};
10use serde_json::{json, Value};
11
12const CLOUDFLARE_API_BASE: &str = "https://api.cloudflare.com/client/v4";
13
14#[derive(Debug, Clone)]
15pub struct CloudflareClient {
16 account_id: String,
17 http: RequestFactory,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct CloudflareStoreCreateRequest {
22 pub name: String,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct CloudflareSecretCreateRequest {
27 pub name: String,
28 pub value: String,
29 #[serde(default)]
30 pub scopes: Vec<String>,
31 #[serde(default)]
32 pub comment: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, Default)]
36pub struct CloudflareSecretEditRequest {
37 #[serde(default)]
38 pub name: Option<String>,
39 #[serde(default)]
40 pub value: Option<String>,
41 #[serde(default)]
42 pub scopes: Option<Vec<String>>,
43 #[serde(default)]
44 pub comment: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct CloudflareSecretDuplicateRequest {
49 pub name: String,
50 #[serde(default)]
51 pub scopes: Vec<String>,
52 #[serde(default)]
53 pub comment: Option<String>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct CloudflareBulkDeleteRequest {
58 pub ids: Vec<String>,
59}
60
61#[derive(Debug, Clone, Serialize, Default)]
62pub struct CloudflareZoneCreateRequest {
63 pub name: String,
64 #[serde(default)]
65 pub account: Option<CloudflareZoneAccountWrite>,
66 #[serde(default)]
67 pub jump_start: Option<bool>,
68 #[serde(rename = "type", default)]
69 pub zone_type: Option<String>,
70}
71
72#[derive(Debug, Clone, Serialize, Default)]
73pub struct CloudflareZoneEditRequest {
74 #[serde(default)]
75 pub paused: Option<bool>,
76 #[serde(rename = "type", default)]
77 pub zone_type: Option<String>,
78 #[serde(default)]
79 pub vanity_name_servers: Option<Vec<String>>,
80}
81
82#[derive(Debug, Clone, Serialize, Default)]
83pub struct CloudflareZoneAccountWrite {
84 #[serde(default)]
85 pub id: Option<String>,
86}
87
88#[derive(Debug, Clone, Serialize, Default)]
89pub struct CloudflareDnsRecordListFilters {
90 #[serde(default)]
91 pub name: Option<String>,
92 #[serde(rename = "type", default)]
93 pub record_type: Option<String>,
94 #[serde(default)]
95 pub page: Option<u64>,
96 #[serde(default)]
97 pub per_page: Option<u64>,
98}
99
100impl CloudflareClient {
101 pub fn new(
102 api_token: impl Into<String>,
103 account_id: impl Into<String>,
104 ) -> Result<Self, String> {
105 let token = api_token.into();
106 let http = RequestFactory::new(CLOUDFLARE_API_BASE)
107 .map_err(|error| error.to_string())?
108 .with_auth(AuthStrategy::Bearer(token))
109 .with_default_header(
110 HeaderName::from_static("content-type"),
111 HeaderValue::from_static("application/json"),
112 );
113 Ok(Self {
114 account_id: account_id.into(),
115 http,
116 })
117 }
118
119 pub async fn search_domains(
120 &self,
121 query: &str,
122 extension: &[String],
123 ) -> Result<Vec<DomainAvailability>, String> {
124 #[derive(Serialize)]
125 struct SearchQuery<'a> {
126 query: &'a str,
127 #[serde(skip_serializing_if = "<[String]>::is_empty")]
128 tlds: &'a [String],
129 }
130
131 let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
132 .http
133 .get_json(
134 &format!("/accounts/{}/registrar/domain-search", self.account_id),
135 Some(&SearchQuery {
136 query,
137 tlds: extension,
138 }),
139 )
140 .await
141 .map_err(render_http_error)?;
142 Ok(envelope.result)
143 }
144
145 pub async fn check_domains(
146 &self,
147 domains: &[String],
148 ) -> Result<Vec<DomainAvailability>, String> {
149 let envelope: CloudflareEnvelope<Vec<DomainAvailability>> = self
150 .http
151 .post_json(
152 &format!("/accounts/{}/registrar/domain-check", self.account_id),
153 &json!({ "domains": domains }),
154 )
155 .await
156 .map_err(render_http_error)?;
157 Ok(envelope.result)
158 }
159
160 pub async fn list_registered_domains(
161 &self,
162 ) -> Result<(Vec<RegisteredDomain>, Option<PaginationInfo>), String> {
163 let envelope: CloudflareEnvelope<Vec<RegisteredDomain>> = self
164 .http
165 .get_json(
166 &format!("/accounts/{}/registrar/domains", self.account_id),
167 Option::<&Value>::None,
168 )
169 .await
170 .map_err(render_http_error)?;
171 Ok((envelope.result, envelope.result_info))
172 }
173
174 pub async fn list_stores(&self) -> Result<(Vec<SecretsStore>, Option<PaginationInfo>), String> {
175 let envelope: CloudflareEnvelope<Vec<SecretsStore>> = self
176 .http
177 .get_json(
178 &format!("/accounts/{}/secrets_store/stores", self.account_id),
179 Option::<&Value>::None,
180 )
181 .await
182 .map_err(render_http_error)?;
183 Ok((envelope.result, envelope.result_info))
184 }
185
186 pub async fn get_store(&self, store_id: &str) -> Result<SecretsStore, String> {
187 let envelope: CloudflareEnvelope<SecretsStore> = self
188 .http
189 .get_json(
190 &format!(
191 "/accounts/{}/secrets_store/stores/{}",
192 self.account_id, store_id
193 ),
194 Option::<&Value>::None,
195 )
196 .await
197 .map_err(render_http_error)?;
198 Ok(envelope.result)
199 }
200
201 pub async fn create_store(
202 &self,
203 request: &CloudflareStoreCreateRequest,
204 ) -> Result<SecretsStore, String> {
205 let envelope: CloudflareEnvelope<SecretsStore> = self
206 .http
207 .post_json(
208 &format!("/accounts/{}/secrets_store/stores", self.account_id),
209 request,
210 )
211 .await
212 .map_err(render_http_error)?;
213 Ok(envelope.result)
214 }
215
216 pub async fn delete_store(&self, store_id: &str) -> Result<(), String> {
217 let _: CloudflareEnvelope<Option<Value>> = self
218 .http
219 .delete_json(
220 &format!(
221 "/accounts/{}/secrets_store/stores/{}",
222 self.account_id, store_id
223 ),
224 Option::<&Value>::None,
225 )
226 .await
227 .map_err(render_http_error)?;
228 Ok(())
229 }
230
231 pub async fn list_secrets(
232 &self,
233 store_id: &str,
234 ) -> Result<(Vec<SecretRecord>, Option<PaginationInfo>), String> {
235 let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
236 .http
237 .get_json(
238 &format!(
239 "/accounts/{}/secrets_store/stores/{}/secrets",
240 self.account_id, store_id
241 ),
242 Option::<&Value>::None,
243 )
244 .await
245 .map_err(render_http_error)?;
246 Ok((envelope.result, envelope.result_info))
247 }
248
249 pub async fn get_secret(
250 &self,
251 store_id: &str,
252 secret_id: &str,
253 ) -> Result<SecretRecord, String> {
254 let envelope: CloudflareEnvelope<SecretRecord> = self
255 .http
256 .get_json(
257 &format!(
258 "/accounts/{}/secrets_store/stores/{}/secrets/{}",
259 self.account_id, store_id, secret_id
260 ),
261 Option::<&Value>::None,
262 )
263 .await
264 .map_err(render_http_error)?;
265 Ok(envelope.result)
266 }
267
268 pub async fn create_secret(
269 &self,
270 store_id: &str,
271 request: &CloudflareSecretCreateRequest,
272 ) -> Result<SecretRecord, String> {
273 let envelope: CloudflareEnvelope<Vec<SecretRecord>> = self
274 .http
275 .post_json(
276 &format!(
277 "/accounts/{}/secrets_store/stores/{}/secrets",
278 self.account_id, store_id
279 ),
280 &vec![request],
281 )
282 .await
283 .map_err(render_http_error)?;
284 envelope
285 .result
286 .into_iter()
287 .next()
288 .ok_or_else(|| "Cloudflare did not return a created secret.".to_string())
289 }
290
291 pub async fn edit_secret(
292 &self,
293 store_id: &str,
294 secret_id: &str,
295 request: &CloudflareSecretEditRequest,
296 ) -> Result<SecretRecord, String> {
297 let envelope: CloudflareEnvelope<SecretRecord> = self
298 .http
299 .patch_json(
300 &format!(
301 "/accounts/{}/secrets_store/stores/{}/secrets/{}",
302 self.account_id, store_id, secret_id
303 ),
304 request,
305 )
306 .await
307 .map_err(render_http_error)?;
308 Ok(envelope.result)
309 }
310
311 pub async fn delete_secret(&self, store_id: &str, secret_id: &str) -> Result<(), String> {
312 let _: CloudflareEnvelope<Option<Value>> = self
313 .http
314 .delete_json(
315 &format!(
316 "/accounts/{}/secrets_store/stores/{}/secrets/{}",
317 self.account_id, store_id, secret_id
318 ),
319 Option::<&Value>::None,
320 )
321 .await
322 .map_err(render_http_error)?;
323 Ok(())
324 }
325
326 pub async fn bulk_delete_secrets(
327 &self,
328 store_id: &str,
329 request: &CloudflareBulkDeleteRequest,
330 ) -> Result<(), String> {
331 let _: CloudflareEnvelope<Option<Value>> = self
332 .http
333 .delete_json_with_body(
334 &format!(
335 "/accounts/{}/secrets_store/stores/{}/secrets",
336 self.account_id, store_id
337 ),
338 Option::<&Value>::None,
339 request,
340 )
341 .await
342 .map_err(render_http_error)?;
343 Ok(())
344 }
345
346 pub async fn duplicate_secret(
347 &self,
348 store_id: &str,
349 secret_id: &str,
350 request: &CloudflareSecretDuplicateRequest,
351 ) -> Result<SecretRecord, String> {
352 let envelope: CloudflareEnvelope<SecretRecord> = self
353 .http
354 .post_json(
355 &format!(
356 "/accounts/{}/secrets_store/stores/{}/secrets/{}/duplicate",
357 self.account_id, store_id, secret_id
358 ),
359 request,
360 )
361 .await
362 .map_err(render_http_error)?;
363 Ok(envelope.result)
364 }
365
366 pub async fn get_quota(&self) -> Result<SecretsQuota, String> {
367 let envelope: CloudflareEnvelope<SecretsQuota> = self
368 .http
369 .get_json(
370 &format!("/accounts/{}/secrets_store/quota", self.account_id),
371 Option::<&Value>::None,
372 )
373 .await
374 .map_err(render_http_error)?;
375 Ok(envelope.result)
376 }
377
378 pub async fn list_zones(
379 &self,
380 filters: &CloudflareZoneFilters,
381 ) -> Result<(Vec<CloudflareZone>, Option<PaginationInfo>), String> {
382 let envelope: CloudflareEnvelope<Vec<CloudflareZone>> = self
383 .http
384 .get_json("/zones", Some(filters))
385 .await
386 .map_err(render_http_error)?;
387 Ok((envelope.result, envelope.result_info))
388 }
389
390 pub async fn get_zone(&self, zone_id: &str) -> Result<CloudflareZone, String> {
391 let envelope: CloudflareEnvelope<CloudflareZone> = self
392 .http
393 .get_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
394 .await
395 .map_err(render_http_error)?;
396 Ok(envelope.result)
397 }
398
399 pub async fn create_zone(
400 &self,
401 request: &CloudflareZoneCreateRequest,
402 ) -> Result<CloudflareZone, String> {
403 let envelope: CloudflareEnvelope<CloudflareZone> = self
404 .http
405 .post_json("/zones", request)
406 .await
407 .map_err(render_http_error)?;
408 Ok(envelope.result)
409 }
410
411 pub async fn edit_zone(
412 &self,
413 zone_id: &str,
414 request: &CloudflareZoneEditRequest,
415 ) -> Result<CloudflareZone, String> {
416 let envelope: CloudflareEnvelope<CloudflareZone> = self
417 .http
418 .patch_json(&format!("/zones/{}", zone_id), request)
419 .await
420 .map_err(render_http_error)?;
421 Ok(envelope.result)
422 }
423
424 pub async fn delete_zone(&self, zone_id: &str) -> Result<(), String> {
425 let _: CloudflareEnvelope<Option<Value>> = self
426 .http
427 .delete_json(&format!("/zones/{}", zone_id), Option::<&Value>::None)
428 .await
429 .map_err(render_http_error)?;
430 Ok(())
431 }
432
433 pub async fn list_records(
434 &self,
435 zone_id: &str,
436 filters: &CloudflareDnsRecordListFilters,
437 ) -> Result<(Vec<CloudflareDnsRecord>, Option<PaginationInfo>), String> {
438 let envelope: CloudflareEnvelope<Vec<CloudflareDnsRecord>> = self
439 .http
440 .get_json(&format!("/zones/{}/dns_records", zone_id), Some(filters))
441 .await
442 .map_err(render_http_error)?;
443 Ok((envelope.result, envelope.result_info))
444 }
445
446 pub async fn get_record(
447 &self,
448 zone_id: &str,
449 record_id: &str,
450 ) -> Result<CloudflareDnsRecord, String> {
451 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
452 .http
453 .get_json(
454 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
455 Option::<&Value>::None,
456 )
457 .await
458 .map_err(render_http_error)?;
459 Ok(envelope.result)
460 }
461
462 pub async fn create_record(
463 &self,
464 zone_id: &str,
465 request: &CloudflareDnsRecordWrite,
466 ) -> Result<CloudflareDnsRecord, String> {
467 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
468 .http
469 .post_json(&format!("/zones/{}/dns_records", zone_id), request)
470 .await
471 .map_err(render_http_error)?;
472 Ok(envelope.result)
473 }
474
475 pub async fn replace_record(
476 &self,
477 zone_id: &str,
478 record_id: &str,
479 request: &CloudflareDnsRecordWrite,
480 ) -> Result<CloudflareDnsRecord, String> {
481 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
482 .http
483 .put_json(
484 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
485 request,
486 )
487 .await
488 .map_err(render_http_error)?;
489 Ok(envelope.result)
490 }
491
492 pub async fn edit_record(
493 &self,
494 zone_id: &str,
495 record_id: &str,
496 request: &CloudflareDnsRecordWrite,
497 ) -> Result<CloudflareDnsRecord, String> {
498 let envelope: CloudflareEnvelope<CloudflareDnsRecord> = self
499 .http
500 .patch_json(
501 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
502 request,
503 )
504 .await
505 .map_err(render_http_error)?;
506 Ok(envelope.result)
507 }
508
509 pub async fn delete_record(&self, zone_id: &str, record_id: &str) -> Result<(), String> {
510 let _: CloudflareEnvelope<Option<Value>> = self
511 .http
512 .delete_json(
513 &format!("/zones/{}/dns_records/{}", zone_id, record_id),
514 Option::<&Value>::None,
515 )
516 .await
517 .map_err(render_http_error)?;
518 Ok(())
519 }
520
521 pub async fn batch_records(
522 &self,
523 zone_id: &str,
524 request: &CloudflareDnsRecordBatch,
525 ) -> Result<Value, String> {
526 let envelope: CloudflareEnvelope<Value> = self
527 .http
528 .post_json(&format!("/zones/{}/dns_records/batch", zone_id), request)
529 .await
530 .map_err(render_http_error)?;
531 Ok(envelope.result)
532 }
533
534 pub async fn export_records(&self, zone_id: &str) -> Result<ResponseBytes, String> {
535 self.http
536 .get_bytes(
537 &format!("/zones/{}/dns_records/export", zone_id),
538 Option::<&Value>::None,
539 )
540 .await
541 .map_err(render_http_error)
542 }
543
544 pub async fn import_records(&self, zone_id: &str, bytes: Vec<u8>) -> Result<Value, String> {
545 let response = self
546 .http
547 .post_bytes(
548 &format!("/zones/{}/dns_records/import", zone_id),
549 bytes,
550 "text/plain",
551 )
552 .await
553 .map_err(render_http_error)?;
554 let envelope: CloudflareEnvelope<Value> =
555 serde_json::from_slice(&response.body).map_err(|error| error.to_string())?;
556 Ok(envelope.result)
557 }
558
559 pub async fn get_dnssec(&self, zone_id: &str) -> Result<CloudflareDnssec, String> {
560 let envelope: CloudflareEnvelope<CloudflareDnssec> = self
561 .http
562 .get_json(
563 &format!("/zones/{}/dnssec", zone_id),
564 Option::<&Value>::None,
565 )
566 .await
567 .map_err(render_http_error)?;
568 Ok(envelope.result)
569 }
570
571 pub async fn edit_dnssec(
572 &self,
573 zone_id: &str,
574 request: &CloudflareDnssecEdit,
575 ) -> Result<CloudflareDnssec, String> {
576 let envelope: CloudflareEnvelope<CloudflareDnssec> = self
577 .http
578 .patch_json(&format!("/zones/{}/dnssec", zone_id), request)
579 .await
580 .map_err(render_http_error)?;
581 Ok(envelope.result)
582 }
583
584 pub async fn get_dns_settings(&self, zone_id: &str) -> Result<CloudflareDnsSettings, String> {
585 let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
586 .http
587 .get_json(
588 &format!("/zones/{}/dns_settings", zone_id),
589 Option::<&Value>::None,
590 )
591 .await
592 .map_err(render_http_error)?;
593 Ok(envelope.result)
594 }
595
596 pub async fn edit_dns_settings(
597 &self,
598 zone_id: &str,
599 request: &CloudflareDnsSettings,
600 ) -> Result<CloudflareDnsSettings, String> {
601 let envelope: CloudflareEnvelope<CloudflareDnsSettings> = self
602 .http
603 .patch_json(&format!("/zones/{}/dns_settings", zone_id), request)
604 .await
605 .map_err(render_http_error)?;
606 Ok(envelope.result)
607 }
608}
609
610fn render_http_error(error: HttpError) -> String {
611 error.to_string()
612}