use std::time::Duration;
use graphql_client::GraphQLQuery;
use reqwest::{
Client,
header::{HeaderMap, HeaderValue},
};
use crate::{
commands::Environment,
config::Configs,
consts::{self, RAILWAY_API_TOKEN_ENV, RAILWAY_TOKEN_ENV},
errors::RailwayError,
};
use anyhow::Result;
use graphql_client::Response as GraphQLResponse;
pub struct GQLClient;
impl GQLClient {
pub fn new_authorized(configs: &Configs) -> Result<Client, RailwayError> {
let mut headers = HeaderMap::new();
if let Some(token) = &Configs::get_railway_token() {
headers.insert("project-access-token", HeaderValue::from_str(token)?);
} else if let Some(token) = configs.get_railway_auth_token() {
headers.insert(
"authorization",
HeaderValue::from_str(&format!("Bearer {token}"))?,
);
} else {
return Err(RailwayError::Unauthorized);
}
headers.insert(
"x-source",
HeaderValue::from_static(consts::get_user_agent()),
);
let client = Client::builder()
.danger_accept_invalid_certs(matches!(Configs::get_environment_id(), Environment::Dev))
.user_agent(consts::get_user_agent())
.default_headers(headers)
.timeout(Duration::from_secs(30))
.build()
.unwrap();
Ok(client)
}
pub fn new_unauthorized() -> Result<Client> {
let mut headers = HeaderMap::new();
headers.insert(
"x-source",
HeaderValue::from_static(consts::get_user_agent()),
);
let client = Client::builder()
.danger_accept_invalid_certs(matches!(Configs::get_environment_id(), Environment::Dev))
.user_agent(consts::get_user_agent())
.default_headers(headers)
.build()?;
Ok(client)
}
}
pub async fn post_graphql<Q: GraphQLQuery, U: reqwest::IntoUrl>(
client: &reqwest::Client,
url: U,
variables: Q::Variables,
) -> Result<Q::ResponseData, RailwayError> {
let body = Q::build_query(variables);
let response = client.post(url).json(&body).send().await?;
if response.status() == 429 {
return Err(RailwayError::Ratelimited);
}
let res: GraphQLResponse<Q::ResponseData> = response.json().await?;
if let Some(errors) = res.errors {
let error = &errors[0];
if error
.message
.to_lowercase()
.contains("project token not found")
{
Err(RailwayError::InvalidRailwayToken(
RAILWAY_TOKEN_ENV.to_string(),
))
} else if error.message.to_lowercase().contains("not authorized") {
Err(auth_failure_error())
} else if error.message == "Two Factor Authentication Required" {
let workspace_name = error
.extensions
.as_ref()
.and_then(|ext| ext.get("workspaceName"))
.and_then(|v| v.as_str())
.unwrap_or("this workspace")
.to_string();
let security_url = get_security_url();
Err(RailwayError::TwoFactorEnforcementRequired(
workspace_name,
security_url,
))
} else {
Err(RailwayError::GraphQLError(error.message.clone()))
}
} else if let Some(data) = res.data {
Ok(data)
} else {
Err(RailwayError::MissingResponseData)
}
}
fn get_security_url() -> String {
let host = match Configs::get_environment_id() {
Environment::Production => "railway.com",
Environment::Staging => "railway-staging.com",
Environment::Dev => "railway-develop.com",
};
format!("https://{}/account/security", host)
}
pub(crate) fn auth_failure_error() -> RailwayError {
if Configs::get_railway_token().is_some() {
RailwayError::UnauthorizedToken(RAILWAY_TOKEN_ENV.to_string())
} else if Configs::get_railway_api_token().is_some() {
RailwayError::UnauthorizedToken(RAILWAY_API_TOKEN_ENV.to_string())
} else if Configs::new()
.ok()
.and_then(|configs| configs.get_railway_auth_token())
.is_some()
{
RailwayError::UnauthorizedLogin
} else {
RailwayError::Unauthorized
}
}
pub async fn post_graphql_skip_none<Q: GraphQLQuery, U: reqwest::IntoUrl>(
client: &reqwest::Client,
url: U,
variables: Q::Variables,
) -> Result<Q::ResponseData, RailwayError> {
let body = Q::build_query(variables);
let mut body_json =
serde_json::to_value(&body).expect("Failed to serialize GraphQL query body");
if let Some(obj) = body_json.as_object_mut() {
if let Some(vars) = obj.get_mut("variables").and_then(|v| v.as_object_mut()) {
vars.retain(|_, v| !v.is_null());
}
}
let response = client.post(url).json(&body_json).send().await?;
if response.status() == 429 {
return Err(RailwayError::Ratelimited);
}
let res: GraphQLResponse<Q::ResponseData> = response.json().await?;
if let Some(errors) = res.errors {
let error = &errors[0];
if error
.message
.to_lowercase()
.contains("project token not found")
{
Err(RailwayError::InvalidRailwayToken(
RAILWAY_TOKEN_ENV.to_string(),
))
} else if error.message.to_lowercase().contains("not authorized") {
Err(auth_failure_error())
} else if error.message == "Two Factor Authentication Required" {
let workspace_name = error
.extensions
.as_ref()
.and_then(|ext| ext.get("workspaceName"))
.and_then(|v| v.as_str())
.unwrap_or("this workspace")
.to_string();
let security_url = get_security_url();
Err(RailwayError::TwoFactorEnforcementRequired(
workspace_name,
security_url,
))
} else {
Err(RailwayError::GraphQLError(error.message.clone()))
}
} else if let Some(data) = res.data {
Ok(data)
} else {
Err(RailwayError::MissingResponseData)
}
}