use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize};
use crate::cloudflare::CloudSetup;
use crate::cloudflare_provision::Provisioner;
#[derive(Serialize)]
struct DomainSetInput<'a> {
project_id: &'a str,
domain: &'a str,
certificate_pem: &'a str,
private_key_pem: &'a str,
not_after_epoch_seconds: i64,
}
#[derive(Deserialize)]
#[serde(tag = "t", rename_all_fields = "camelCase")]
enum DomainSet {
Ok {
origin_hostname: String,
replaced_domain: Option<String>,
},
NotLoggedIn,
NotFound,
InvalidDomain {
message: String,
},
DomainTaken {
existing_project_id: String,
},
InternalError,
}
pub struct DomainOutcome {
pub origin_hostname: String,
pub replaced_domain: Option<String>,
}
pub async fn set_domain(setup: &CloudSetup<'_>) -> Result<DomainOutcome> {
println!(
"signing an origin certificate for {} (this runs locally)...",
setup.domain
);
let issued = provisioner(setup)
.issue_origin_certificate(setup.domain, setup.mint_from_setup_token)
.await?;
let creds = crate::credentials::require()?;
let url = format!(
"{}/__forte_action/domain_set",
creds.control_url.trim_end_matches('/')
);
let project_id = setup.project_id;
let domain = setup.domain;
let response: DomainSet = reqwest::Client::new()
.post(&url)
.bearer_auth(&creds.token)
.json(&DomainSetInput {
project_id,
domain,
certificate_pem: &issued.certificate_pem,
private_key_pem: &issued.private_key_pem,
not_after_epoch_seconds: issued.not_after_epoch_seconds,
})
.send()
.await?
.error_for_status()?
.json()
.await?;
let (origin_hostname, replaced_domain) = match response {
DomainSet::Ok {
origin_hostname,
replaced_domain,
} => (origin_hostname, replaced_domain),
DomainSet::NotLoggedIn => {
return Err(anyhow!("control rejected token; sign in again."));
}
DomainSet::NotFound => {
return Err(anyhow!(
"project '{project_id}' not found or not owned by you."
));
}
DomainSet::InvalidDomain { message } => {
return Err(anyhow!("invalid domain: {message}"));
}
DomainSet::DomainTaken {
existing_project_id,
} => {
return Err(anyhow!(
"domain '{domain}' is already in use by project '{existing_project_id}'"
));
}
DomainSet::InternalError => {
return Err(anyhow!("domain_set: server error; check fn0-control logs"));
}
};
provisioner(setup)
.put_app_cors(project_id, &setup.app_origin(), setup.mint_from_setup_token)
.await?;
provisioner(setup)
.ensure_app_cache(
setup.domain,
replaced_domain.as_deref(),
setup.mint_from_setup_token,
)
.await?;
provisioner(setup)
.ensure_app_dns_record(
setup.domain,
&origin_hostname,
replaced_domain.as_deref(),
setup.mint_from_setup_token,
)
.await?;
Ok(DomainOutcome {
origin_hostname,
replaced_domain,
})
}
fn provisioner(setup: &CloudSetup<'_>) -> Provisioner {
Provisioner::new(
setup.api_token.to_string(),
setup.account_id.to_string(),
setup.zone_id.to_string(),
)
}
#[derive(Serialize)]
struct DomainProjectInput<'a> {
project_id: &'a str,
}
#[derive(Deserialize)]
#[serde(tag = "t", rename_all_fields = "camelCase")]
pub enum DomainStatus {
NotConfigured,
SelfHosted {
domain: String,
origin_certificate_ready: bool,
origin_certificate_expires_epoch_seconds: Option<i64>,
origin_hostname: String,
},
NotLoggedIn,
NotFound,
InternalError,
}
pub async fn fetch_domain_status(
creds: &crate::credentials::Credentials,
project_id: &str,
) -> Result<DomainStatus> {
let url = format!(
"{}/__forte_action/domain_status",
creds.control_url.trim_end_matches('/')
);
let resp = reqwest::Client::new()
.post(&url)
.bearer_auth(&creds.token)
.json(&DomainProjectInput { project_id })
.send()
.await?
.error_for_status()?;
Ok(resp.json().await?)
}