Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 7 additions & 35 deletions src/commands/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crate::{
errors::RailwayError,
util::{
progress::create_spinner_if,
prompt::{fake_select, prompt_confirm_with_default, prompt_options, prompt_text},
prompt::{fake_select, prompt_confirm_with_default, prompt_options},
two_factor::validate_two_factor_if_enabled,
},
workspace::{Project, Workspace, workspaces},
};
Expand All @@ -25,13 +26,13 @@ pub struct Args {
#[clap(short = 'y', long = "yes")]
yes: bool,

/// 2FA code for verification (required if 2FA is enabled in non-interactive mode)
#[clap(long = "2fa-code")]
two_factor_code: Option<String>,

/// Output in JSON format
#[clap(long)]
json: bool,

/// 2FA code for verification (required if 2FA is enabled in non-interactive mode)
#[clap(long = "2fa-code")]
two_factor_code: Option<String>,
}

pub async fn command(args: Args) -> Result<()> {
Expand Down Expand Up @@ -64,36 +65,7 @@ pub async fn command(args: Args) -> Result<()> {
}
}

let is_two_factor_enabled = {
let vars = queries::two_factor_info::Variables {};

let info =
post_graphql::<queries::TwoFactorInfo, _>(&client, configs.get_backboard(), vars)
.await?
.two_factor_info;

info.is_verified
};

if is_two_factor_enabled {
let token = if let Some(code) = args.two_factor_code {
code
} else if is_terminal {
prompt_text("Enter your 2FA code")?
} else {
return Err(RailwayError::TwoFactorRequiresInteractive.into());
};
let vars = mutations::validate_two_factor::Variables { token };

let valid =
post_graphql::<mutations::ValidateTwoFactor, _>(&client, configs.get_backboard(), vars)
.await?
.two_factor_info_validate;

if !valid {
return Err(RailwayError::InvalidTwoFactorCode.into());
}
}
validate_two_factor_if_enabled(&client, &configs, is_terminal, args.two_factor_code).await?;

let spinner = create_spinner_if(!args.json, "Deleting project...".into());

Expand Down
32 changes: 3 additions & 29 deletions src/commands/environment/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use crate::{
errors::RailwayError,
util::{
progress::create_spinner_if,
prompt::{prompt_confirm_with_default, prompt_options, prompt_text},
prompt::{prompt_confirm_with_default, prompt_options},
two_factor::validate_two_factor_if_enabled,
},
};
use anyhow::{Result, bail};
Expand Down Expand Up @@ -70,35 +71,8 @@ pub async fn delete_environment(args: Args) -> Result<()> {
return Ok(());
}

let is_two_factor_enabled = {
let vars = queries::two_factor_info::Variables {};
validate_two_factor_if_enabled(&client, &configs, is_terminal, args.two_factor_code).await?;

let info =
post_graphql::<queries::TwoFactorInfo, _>(&client, configs.get_backboard(), vars)
.await?
.two_factor_info;

info.is_verified
};
if is_two_factor_enabled {
let token = if let Some(code) = args.two_factor_code {
code
} else if is_terminal {
prompt_text("Enter your 2FA code")?
} else {
return Err(RailwayError::TwoFactorRequiresInteractive.into());
};
let vars = mutations::validate_two_factor::Variables { token };

let valid =
post_graphql::<mutations::ValidateTwoFactor, _>(&client, configs.get_backboard(), vars)
.await?
.two_factor_info_validate;

if !valid {
return Err(RailwayError::InvalidTwoFactorCode.into());
}
}
let spinner = create_spinner_if(!args.json, "Deleting environment...".into());
let _r = post_graphql::<mutations::EnvironmentDelete, _>(
&client,
Expand Down
8 changes: 4 additions & 4 deletions src/commands/environment/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ structstruck::strike! {
/// The environment to delete
pub environment: Option<String>,

/// 2FA code for verification (required if 2FA is enabled in non-interactive mode)
#[clap(long = "2fa-code")]
pub two_factor_code: Option<String>,

/// Output in JSON format
#[clap(long)]
pub json: bool,

/// 2FA code for verification (required if 2FA is enabled in non-interactive mode)
#[clap(long = "2fa-code")]
pub two_factor_code: Option<String>,
}),

/// Edit an environment's configuration
Expand Down
40 changes: 3 additions & 37 deletions src/commands/functions/delete.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ use crate::{
queries::project::ProjectProjectEnvironmentsEdges,
util::{
progress::{create_spinner, success_spinner},
prompt::{fake_select, prompt_select, prompt_text},
prompt::{fake_select, prompt_select},
two_factor::validate_two_factor_if_enabled,
},
};
use anyhow::bail;
Expand All @@ -27,7 +28,7 @@ pub async fn delete(environment: &ProjectProjectEnvironmentsEdges, args: Delete)
return Ok(());
}

validate_two_factor_if_enabled(&client, &configs).await?;
validate_two_factor_if_enabled(&client, &configs, terminal, args.two_factor_code).await?;
delete_function_service(&client, &mut configs, function, environment).await?;

Ok(())
Expand Down Expand Up @@ -65,41 +66,6 @@ fn find_function_by_identifier<'a>(
}
}

async fn validate_two_factor_if_enabled(client: &reqwest::Client, configs: &Configs) -> Result<()> {
let is_two_factor_enabled = check_two_factor_status(client, configs).await?;

if is_two_factor_enabled {
validate_two_factor_code(client, configs).await?;
}

Ok(())
}

async fn check_two_factor_status(client: &reqwest::Client, configs: &Configs) -> Result<bool> {
let vars = queries::two_factor_info::Variables {};
let info = post_graphql::<queries::TwoFactorInfo, _>(client, configs.get_backboard(), vars)
.await?
.two_factor_info;

Ok(info.is_verified)
}

async fn validate_two_factor_code(client: &reqwest::Client, configs: &Configs) -> Result<()> {
let token = prompt_text("Enter your 2FA code")?;
let vars = mutations::validate_two_factor::Variables { token };

let valid =
post_graphql::<mutations::ValidateTwoFactor, _>(client, configs.get_backboard(), vars)
.await?
.two_factor_info_validate;

if !valid {
return Err(RailwayError::InvalidTwoFactorCode.into());
}

Ok(())
}

async fn delete_function_service(
client: &reqwest::Client,
configs: &mut Configs,
Expand Down
6 changes: 5 additions & 1 deletion src/commands/functions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ structstruck::strike! {

/// Skip confirmation for deleting
#[clap(long, short, action = clap::ArgAction::Set, num_args = 0..=1, default_missing_value = "true")]
yes: Option<bool>
yes: Option<bool>,

/// 2FA code for verification (required if 2FA is enabled in non-interactive mode)
#[clap(long = "2fa-code")]
two_factor_code: Option<String>
}),

/// Push a new change to the function
Expand Down
46 changes: 8 additions & 38 deletions src/commands/volume.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{
util::{
progress::create_spinner,
prompt::{fake_select, prompt_confirm_with_default, prompt_options, prompt_text},
two_factor::validate_two_factor_if_enabled,
},
};
use anyhow::{anyhow, bail};
Expand Down Expand Up @@ -63,13 +64,13 @@ structstruck::strike! {
#[clap(short = 'y', long = "yes")]
yes: bool,

/// 2FA code for verification (required if 2FA is enabled in non-interactive mode)
#[clap(long = "2fa-code")]
two_factor_code: Option<String>,

/// Output in JSON format
#[clap(long)]
json: bool,

/// 2FA code for verification (required if 2FA is enabled in non-interactive mode)
#[clap(long = "2fa-code")]
two_factor_code: Option<String>,
}),

/// Update a volume
Expand Down Expand Up @@ -147,8 +148,8 @@ pub async fn command(args: Args) -> Result<()> {
d.volume,
project,
d.yes,
d.two_factor_code,
d.json,
d.two_factor_code,
)
.await?
}
Expand Down Expand Up @@ -401,8 +402,8 @@ async fn delete(
volume: Option<String>,
project: ProjectProject,
yes: bool,
two_factor_code: Option<String>,
json: bool,
two_factor_code: Option<String>,
) -> Result<()> {
let configs = Configs::new()?;
let client = GQLClient::new_authorized(&configs)?;
Expand All @@ -426,39 +427,8 @@ async fn delete(
);
};
if confirm {
let is_two_factor_enabled = {
let vars = queries::two_factor_info::Variables {};

let info =
post_graphql::<queries::TwoFactorInfo, _>(&client, configs.get_backboard(), vars)
.await?
.two_factor_info;

info.is_verified
};

if is_two_factor_enabled {
let token = if let Some(code) = two_factor_code {
code
} else if is_terminal {
prompt_text("Enter your 2FA code")?
} else {
return Err(RailwayError::TwoFactorRequiresInteractive.into());
};
let vars = mutations::validate_two_factor::Variables { token };

let valid = post_graphql::<mutations::ValidateTwoFactor, _>(
&client,
configs.get_backboard(),
vars,
)
.await?
.two_factor_info_validate;
validate_two_factor_if_enabled(&client, &configs, is_terminal, two_factor_code).await?;

if !valid {
return Err(RailwayError::InvalidTwoFactorCode.into());
}
}
let volume_id = volume.0.volume.id.clone();
let p = post_graphql::<mutations::VolumeDelete, _>(
&client,
Expand Down
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,13 @@ impl Configs {
std::env::var(consts::RAILWAY_API_TOKEN_ENV).ok()
}

/// Returns true if using token-based auth (RAILWAY_TOKEN or RAILWAY_API_TOKEN)
/// rather than session-based auth from `railway login`.
/// Token-based auth bypasses 2FA on the backend, so client-side 2FA checks are unnecessary.
pub fn is_using_token_auth() -> bool {
Self::get_railway_token().is_some() || Self::get_railway_api_token().is_some()
}

pub fn env_is_ci() -> bool {
std::env::var("CI")
.map(|val| val.trim().to_lowercase() == "true")
Expand Down
5 changes: 0 additions & 5 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,6 @@ pub enum RailwayError {
#[error("2FA code is incorrect. Please try again.")]
InvalidTwoFactorCode,

#[error(
"2FA is enabled. Use --2fa-code <CODE> to provide your verification code in non-interactive mode."
)]
TwoFactorRequiresInteractive,

#[error("Two-factor authentication is required for workspace \"{0}\".\nEnable 2FA at: {1}")]
TwoFactorEnforcementRequired(String, String),

Expand Down
1 change: 1 addition & 0 deletions src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pub mod progress;
pub mod prompt;
pub mod retry;
pub mod time;
pub mod two_factor;
pub mod watcher;
57 changes: 57 additions & 0 deletions src/util/two_factor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use anyhow::{Result, bail};

use crate::{
Configs,
client::post_graphql,
errors::RailwayError,
gql::{mutations, queries},
util::prompt::prompt_text,
};

/// Validates 2FA if enabled for the current user.
/// Skips check entirely for token-based auth (API tokens bypass 2FA on the backend).
/// For session-based auth, prompts for 2FA code if enabled, or uses provided code.
pub async fn validate_two_factor_if_enabled(
client: &reqwest::Client,
configs: &Configs,
is_terminal: bool,
two_factor_code: Option<String>,
) -> Result<()> {
// Skip 2FA check for token-based auth (API tokens bypass 2FA on the backend)
if Configs::is_using_token_auth() {
return Ok(());
}

let is_two_factor_enabled = {
let vars = queries::two_factor_info::Variables {};
let info = post_graphql::<queries::TwoFactorInfo, _>(client, configs.get_backboard(), vars)
.await?
.two_factor_info;
info.is_verified
};

if is_two_factor_enabled {
let token = if let Some(code) = two_factor_code {
code
} else if is_terminal {
prompt_text("Enter your 2FA code")?
} else {
bail!(
"2FA is enabled and requires interactive mode. Use --2fa-code <CODE> or an API token for non-interactive operations."
);
};

let vars = mutations::validate_two_factor::Variables { token };

let valid =
post_graphql::<mutations::ValidateTwoFactor, _>(client, configs.get_backboard(), vars)
.await?
.two_factor_info_validate;

if !valid {
return Err(RailwayError::InvalidTwoFactorCode.into());
}
}

Ok(())
}