diff --git a/src/client.rs b/src/client.rs index 9e243ad..e001c5d 100644 --- a/src/client.rs +++ b/src/client.rs @@ -67,11 +67,26 @@ pub async fn post_graphql( } let res: GraphQLResponse = response.json().await?; if let Some(errors) = res.errors { - if errors[0].message.to_lowercase().contains("not authorized") { + let error = &errors[0]; + if error.message.to_lowercase().contains("not authorized") { // Handle unauthorized errors in a custom way Err(RailwayError::Unauthorized) + } else if error.message == "Two Factor Authentication Required" { + // Extract workspace name from extensions if available + 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(errors[0].message.clone())) + Err(RailwayError::GraphQLError(error.message.clone())) } } else if let Some(data) = res.data { Ok(data) @@ -80,6 +95,15 @@ pub async fn post_graphql( } } +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) +} + /// Like post_graphql, but removes null values from the variables object before sending. /// /// This is needed because graphql-client 0.14.0 has a bug where skip_serializing_none @@ -110,10 +134,25 @@ pub async fn post_graphql_skip_none( } let res: GraphQLResponse = response.json().await?; if let Some(errors) = res.errors { - if errors[0].message.to_lowercase().contains("not authorized") { + let error = &errors[0]; + if error.message.to_lowercase().contains("not authorized") { Err(RailwayError::Unauthorized) + } else if error.message == "Two Factor Authentication Required" { + // Extract workspace name from extensions if available + 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(errors[0].message.clone())) + Err(RailwayError::GraphQLError(error.message.clone())) } } else if let Some(data) = res.data { Ok(data) diff --git a/src/errors.rs b/src/errors.rs index fbb279d..c8e710a 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -76,6 +76,9 @@ pub enum RailwayError { #[error("2FA code is incorrect. Please try again.")] InvalidTwoFactorCode, + #[error("Two-factor authentication is required for workspace \"{0}\".\nEnable 2FA at: {1}")] + TwoFactorEnforcementRequired(String, String), + #[error("Could not find a variable to connect to the service with. Looking for \"{0}\".")] ConnectionVariableNotFound(String),