1use anyhow::{Result, anyhow};
2use serde::{Deserialize, Serialize};
3
4use crate::cloudflare::CloudSetup;
5use crate::cloudflare_provision::Provisioner;
6
7#[derive(Serialize)]
8struct DomainSetInput<'a> {
9 project_id: &'a str,
10 domain: &'a str,
11 certificate_pem: &'a str,
12 private_key_pem: &'a str,
13 not_after_epoch_seconds: i64,
14}
15
16#[derive(Deserialize)]
17#[serde(tag = "t", rename_all_fields = "camelCase")]
18enum DomainSet {
19 Ok {
20 origin_hostname: String,
21 replaced_domain: Option<String>,
22 },
23 NotLoggedIn,
24 NotFound,
25 InvalidDomain {
26 message: String,
27 },
28 DomainTaken {
29 existing_project_id: String,
30 },
31 InternalError,
32}
33
34pub struct DomainOutcome {
36 pub origin_hostname: String,
38 pub replaced_domain: Option<String>,
39}
40
41pub async fn set_domain(setup: &CloudSetup<'_>) -> Result<DomainOutcome> {
58 println!(
59 "signing an origin certificate for {} (this runs locally)...",
60 setup.domain
61 );
62 let issued = provisioner(setup)
63 .issue_origin_certificate(setup.domain, setup.mint_from_setup_token)
64 .await?;
65
66 let creds = crate::credentials::require()?;
67 let url = format!(
68 "{}/__forte_action/domain_set",
69 creds.control_url.trim_end_matches('/')
70 );
71 let project_id = setup.project_id;
72 let domain = setup.domain;
73 let response: DomainSet = reqwest::Client::new()
74 .post(&url)
75 .bearer_auth(&creds.token)
76 .json(&DomainSetInput {
77 project_id,
78 domain,
79 certificate_pem: &issued.certificate_pem,
80 private_key_pem: &issued.private_key_pem,
81 not_after_epoch_seconds: issued.not_after_epoch_seconds,
82 })
83 .send()
84 .await?
85 .error_for_status()?
86 .json()
87 .await?;
88
89 let (origin_hostname, replaced_domain) = match response {
90 DomainSet::Ok {
91 origin_hostname,
92 replaced_domain,
93 } => (origin_hostname, replaced_domain),
94 DomainSet::NotLoggedIn => {
95 return Err(anyhow!("control rejected token; sign in again."));
96 }
97 DomainSet::NotFound => {
98 return Err(anyhow!(
99 "project '{project_id}' not found or not owned by you."
100 ));
101 }
102 DomainSet::InvalidDomain { message } => {
103 return Err(anyhow!("invalid domain: {message}"));
104 }
105 DomainSet::DomainTaken {
106 existing_project_id,
107 } => {
108 return Err(anyhow!(
109 "domain '{domain}' is already in use by project '{existing_project_id}'"
110 ));
111 }
112 DomainSet::InternalError => {
113 return Err(anyhow!("domain_set: server error; check fn0-control logs"));
114 }
115 };
116
117 provisioner(setup)
118 .put_app_cors(project_id, &setup.app_origin(), setup.mint_from_setup_token)
119 .await?;
120 provisioner(setup)
121 .ensure_app_cache(
122 setup.domain,
123 replaced_domain.as_deref(),
124 setup.mint_from_setup_token,
125 )
126 .await?;
127 provisioner(setup)
128 .ensure_app_dns_record(
129 setup.domain,
130 &origin_hostname,
131 replaced_domain.as_deref(),
132 setup.mint_from_setup_token,
133 )
134 .await?;
135
136 Ok(DomainOutcome {
137 origin_hostname,
138 replaced_domain,
139 })
140}
141
142fn provisioner(setup: &CloudSetup<'_>) -> Provisioner {
143 Provisioner::new(
144 setup.api_token.to_string(),
145 setup.account_id.to_string(),
146 setup.zone_id.to_string(),
147 )
148}
149
150#[derive(Serialize)]
151struct DomainProjectInput<'a> {
152 project_id: &'a str,
153}
154
155#[derive(Deserialize)]
156#[serde(tag = "t", rename_all_fields = "camelCase")]
157pub enum DomainStatus {
158 NotConfigured,
159 SelfHosted {
163 domain: String,
164 origin_certificate_ready: bool,
165 origin_certificate_expires_epoch_seconds: Option<i64>,
166 origin_hostname: String,
167 },
168 NotLoggedIn,
169 NotFound,
170 InternalError,
171}
172
173pub async fn fetch_domain_status(
174 creds: &crate::credentials::Credentials,
175 project_id: &str,
176) -> Result<DomainStatus> {
177 let url = format!(
178 "{}/__forte_action/domain_status",
179 creds.control_url.trim_end_matches('/')
180 );
181 let resp = reqwest::Client::new()
182 .post(&url)
183 .bearer_auth(&creds.token)
184 .json(&DomainProjectInput { project_id })
185 .send()
186 .await?
187 .error_for_status()?;
188 Ok(resp.json().await?)
189}