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