1use reqwest::blocking::Client;
7use std::sync::Arc;
8
9use super::error::{CloudflareError, CloudflareResult};
10use super::models::{
11 ApiResponse, DnsRecord, DnsRecordResponse, ZoneResponse,
12};
13
14struct Inner {
16 client: Client,
17 api_token: String,
18 zone_id: String,
19 base_url: String,
20}
21
22pub struct CloudflareClient {
34 inner: Arc<Inner>,
35}
36
37impl CloudflareClient {
38 pub fn new(api_token: String, zone_id: String) -> Self {
44 Self::with_base_url(
45 api_token,
46 zone_id,
47 "https://api.cloudflare.com/client/v4".to_string(),
48 )
49 }
50
51 pub fn with_base_url(api_token: String, zone_id: String, base_url: String) -> Self {
53 Self {
54 inner: Arc::new(Inner {
55 client: Client::builder()
56 .timeout(std::time::Duration::from_secs(30))
57 .build()
58 .expect("Failed to create HTTP client"),
59 api_token,
60 zone_id,
61 base_url,
62 }),
63 }
64 }
65
66 pub async fn get_zone_name(&self) -> Result<String, CloudflareError> {
71 let inner = self.inner.clone();
72 smol::unblock(move || {
73 let url = format!("{}/zones/{}", inner.base_url, inner.zone_id);
74 let response = inner
75 .client
76 .get(&url)
77 .header("Authorization", format!("Bearer {}", inner.api_token))
78 .header("Content-Type", "application/json")
79 .send()
80 .map_err(CloudflareError::RequestError)?;
81
82 if !response.status().is_success() {
83 return Ok(inner.zone_id.clone());
84 }
85
86 let api_response: ApiResponse<ZoneResponse> = response
87 .json()
88 .map_err(CloudflareError::RequestError)?;
89
90 if let Some(zone) = api_response.result {
91 Ok(zone.name)
92 } else {
93 Ok(inner.zone_id.clone())
94 }
95 })
96 .await
97 }
98
99 pub async fn list_dns_records(&self) -> CloudflareResult<Vec<DnsRecord>> {
103 let inner = self.inner.clone();
104 smol::unblock(move || {
105 let mut all_records = Vec::new();
106 let mut page = 1;
107 let per_page = 100;
108
109 loop {
110 let url = format!(
111 "{}/zones/{}/dns_records?page={}&per_page={}",
112 inner.base_url, inner.zone_id, page, per_page
113 );
114
115 let response = inner
116 .client
117 .get(&url)
118 .header("Authorization", format!("Bearer {}", inner.api_token))
119 .header("Content-Type", "application/json")
120 .send()
121 .map_err(CloudflareError::RequestError)?;
122
123 if !response.status().is_success() {
124 let status = response.status().as_u16();
125 let body = response.text().unwrap_or_default();
126 return Err(CloudflareError::HttpError { status, body });
127 }
128
129 let api_response: ApiResponse<Vec<DnsRecordResponse>> = response
130 .json()
131 .map_err(CloudflareError::RequestError)?;
132
133 if !api_response.success {
134 let errors: Vec<String> = api_response
135 .errors
136 .iter()
137 .map(|e| format!("{} (code: {})", e.message, e.code))
138 .collect();
139 return Err(CloudflareError::ApiErrors(errors));
140 }
141
142 let records = api_response.result.unwrap_or_default();
143 let fetched_count = records.len();
144 if fetched_count == 0 {
145 break;
146 }
147
148 all_records.extend(records.into_iter().map(|r| DnsRecord {
149 id: Some(r.id),
150 record_type: r.record_type,
151 name: r.name,
152 content: r.content,
153 ttl: Some(r.ttl),
154 proxied: Some(r.proxied),
155 comment: r.comment,
156 }));
157
158 if fetched_count < per_page {
159 break;
160 }
161 page += 1;
162 }
163
164 Ok(all_records)
165 })
166 .await
167 }
168
169 pub async fn create_dns_record(&self, record: &DnsRecord) -> CloudflareResult<DnsRecord> {
171 let inner = self.inner.clone();
172 let record = record.clone();
173
174 smol::unblock(move || {
175 let response = inner
176 .client
177 .post(format!(
178 "{}/zones/{}/dns_records",
179 inner.base_url, inner.zone_id
180 ))
181 .header("Authorization", format!("Bearer {}", inner.api_token))
182 .header("Content-Type", "application/json")
183 .json(&record)
184 .send()
185 .map_err(CloudflareError::RequestError)?;
186
187 if !response.status().is_success() {
188 let status = response.status().as_u16();
189 let body = response.text().unwrap_or_default();
190 return Err(CloudflareError::HttpError { status, body });
191 }
192
193 let api_response: ApiResponse<DnsRecordResponse> = response
194 .json()
195 .map_err(CloudflareError::RequestError)?;
196
197 if !api_response.success {
198 let errors: Vec<String> = api_response
199 .errors
200 .iter()
201 .map(|e| format!("{} (code: {})", e.message, e.code))
202 .collect();
203 return Err(CloudflareError::ApiErrors(errors));
204 }
205
206 let result = api_response
207 .result
208 .ok_or(CloudflareError::NoResult)?;
209
210 Ok(DnsRecord {
211 id: Some(result.id),
212 record_type: result.record_type,
213 name: result.name,
214 content: result.content,
215 ttl: Some(result.ttl),
216 proxied: Some(result.proxied),
217 comment: result.comment,
218 })
219 })
220 .await
221 }
222
223 pub async fn update_dns_record(&self, record: &DnsRecord) -> CloudflareResult<DnsRecord> {
227 let inner = self.inner.clone();
228 let record = record.clone();
229
230 smol::unblock(move || {
231 let record_id = record
232 .id
233 .as_ref()
234 .ok_or(CloudflareError::MissingRecordId)?;
235
236 let response = inner
237 .client
238 .put(format!(
239 "{}/zones/{}/dns_records/{}",
240 inner.base_url, inner.zone_id, record_id
241 ))
242 .header("Authorization", format!("Bearer {}", inner.api_token))
243 .header("Content-Type", "application/json")
244 .json(&record)
245 .send()
246 .map_err(CloudflareError::RequestError)?;
247
248 if !response.status().is_success() {
249 let status = response.status().as_u16();
250 let body = response.text().unwrap_or_default();
251 return Err(CloudflareError::HttpError { status, body });
252 }
253
254 let api_response: ApiResponse<DnsRecordResponse> = response
255 .json()
256 .map_err(CloudflareError::RequestError)?;
257
258 if !api_response.success {
259 let errors: Vec<String> = api_response
260 .errors
261 .iter()
262 .map(|e| format!("{} (code: {})", e.message, e.code))
263 .collect();
264 return Err(CloudflareError::ApiErrors(errors));
265 }
266
267 let result = api_response
268 .result
269 .ok_or(CloudflareError::NoResult)?;
270
271 Ok(DnsRecord {
272 id: Some(result.id),
273 record_type: result.record_type,
274 name: result.name,
275 content: result.content,
276 ttl: Some(result.ttl),
277 proxied: Some(result.proxied),
278 comment: result.comment,
279 })
280 })
281 .await
282 }
283
284 pub async fn delete_dns_record(&self, record_id: &str) -> CloudflareResult<()> {
286 let inner = self.inner.clone();
287 let record_id = record_id.to_string();
288
289 smol::unblock(move || {
290 let response = inner
291 .client
292 .delete(format!(
293 "{}/zones/{}/dns_records/{}",
294 inner.base_url, inner.zone_id, record_id
295 ))
296 .header("Authorization", format!("Bearer {}", inner.api_token))
297 .header("Content-Type", "application/json")
298 .send()
299 .map_err(CloudflareError::RequestError)?;
300
301 if !response.status().is_success() {
302 let status = response.status().as_u16();
303 let body = response.text().unwrap_or_default();
304 return Err(CloudflareError::HttpError { status, body });
305 }
306
307 let api_response: ApiResponse<serde_json::Value> = response
308 .json()
309 .map_err(CloudflareError::RequestError)?;
310
311 if !api_response.success {
312 let errors: Vec<String> = api_response
313 .errors
314 .iter()
315 .map(|e| format!("{} (code: {})", e.message, e.code))
316 .collect();
317 return Err(CloudflareError::ApiErrors(errors));
318 }
319
320 Ok(())
321 })
322 .await
323 }
324}