feat: Handle 2FA enforcement error gracefully (#759)

When a workspace has 2FA enforcement enabled and the user hasn't set up
2FA, display a user-friendly error message with the workspace name and
a direct link to the account security page.
This commit is contained in:
Naman Kumar 2026-01-13 09:17:12 +05:30 committed by GitHub
parent 2925ccc490
commit a2e106b5bc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 4 deletions

View file

@ -67,11 +67,26 @@ pub async fn post_graphql<Q: GraphQLQuery, U: reqwest::IntoUrl>(
}
let res: GraphQLResponse<Q::ResponseData> = 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<Q: GraphQLQuery, U: reqwest::IntoUrl>(
}
}
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<Q: GraphQLQuery, U: reqwest::IntoUrl>(
}
let res: GraphQLResponse<Q::ResponseData> = 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)

View file

@ -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),