Skip to content

Commit

Permalink
chore: move logout functionality into auth crate
Browse files Browse the repository at this point in the history
  • Loading branch information
Zertsov committed Feb 28, 2024
1 parent 2f691cf commit 626fff4
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 48 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/turborepo-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tempfile.workspace = true
thiserror = "1.0.38"
tokio.workspace = true
tracing.workspace = true
turbopath.workspace = true
turborepo-api-client = { workspace = true }
turborepo-dirs = { version = "0.1.0", path = "../turborepo-dirs" }
turborepo-ui.workspace = true
Expand Down
122 changes: 119 additions & 3 deletions crates/turborepo-auth/src/auth/logout.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,121 @@
use turborepo_ui::{cprintln, GREY, UI};
use tracing::error;
use turborepo_api_client::Client;
use turborepo_ui::{cprintln, GREY};

pub fn logout(ui: &UI) {
cprintln!(ui, GREY, ">>> Logged out");
use crate::{Error, LogoutOptions};

pub fn logout<T: Client>(options: &LogoutOptions<T>) -> Result<(), Error> {
if let Err(err) = remove_token(options) {
error!("could not logout. Something went wrong: {}", err);
return Err(err);
}

cprintln!(options.ui, GREY, ">>> Logged out");
Ok(())
}

fn remove_token<T: Client>(options: &LogoutOptions<T>) -> Result<(), Error> {
let global_config_path = options.path;
let content = global_config_path.read_to_string()?;

let mut data: serde_json::Value = serde_json::from_str(&content)?;
if data.is_object() && data.get("token").is_some() {
// Since we do the `is_object` check above, we can safely unwrap here.
data.as_object_mut().unwrap().remove("token");
}

let new_content = serde_json::to_string_pretty(&data)?;
global_config_path.create_with_contents(new_content)?;

Ok(())
}

#[cfg(test)]
mod tests {
use async_trait::async_trait;
use reqwest::{RequestBuilder, Response};
use tempfile::TempDir;
use turbopath::AbsoluteSystemPathBuf;
use turborepo_ui::UI;
use turborepo_vercel_api::{
SpacesResponse, Team, TeamsResponse, UserResponse, VerifiedSsoUser,
};
use url::Url;

use super::*;

struct MockApiClient {}

impl MockApiClient {
fn new() -> Self {
Self {}
}
}

#[async_trait]
impl Client for MockApiClient {
async fn get_user(&self, _token: &str) -> turborepo_api_client::Result<UserResponse> {
unimplemented!("get_user")
}
async fn get_teams(&self, _token: &str) -> turborepo_api_client::Result<TeamsResponse> {
unimplemented!("get_teams")
}
async fn get_team(
&self,
_token: &str,
_team_id: &str,
) -> turborepo_api_client::Result<Option<Team>> {
unimplemented!("get_team")
}
fn add_ci_header(_request_builder: RequestBuilder) -> RequestBuilder {
unimplemented!("add_ci_header")
}
async fn get_spaces(
&self,
_token: &str,
_team_id: Option<&str>,
) -> turborepo_api_client::Result<SpacesResponse> {
unimplemented!("get_spaces")
}
async fn verify_sso_token(
&self,
token: &str,
_: &str,
) -> turborepo_api_client::Result<VerifiedSsoUser> {
Ok(VerifiedSsoUser {
token: token.to_string(),
team_id: Some("team_id".to_string()),
})
}
async fn handle_403(_response: Response) -> turborepo_api_client::Error {
unimplemented!("handle_403")
}
fn make_url(&self, _endpoint: &str) -> turborepo_api_client::Result<Url> {
unimplemented!("make_url")
}
}

#[test]
fn test_remove_token() {
let path = turborepo_dirs::config_dir()
.unwrap()
.join("turborepo")
.join("config.json");
let abs_path = AbsoluteSystemPathBuf::try_from(path).expect("could not create path");
let content = r#"{"token":"some-token"}"#;
abs_path
.create_with_contents(content)
.expect("could not create file");

let options = LogoutOptions {
ui: &UI::new(false),
api_client: &MockApiClient::new(),
path: &abs_path,
};

remove_token(&options).unwrap();

let new_content = abs_path.read_to_string().unwrap();
assert_eq!(new_content, "{}");
}
}
20 changes: 12 additions & 8 deletions crates/turborepo-auth/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod sso;
pub use login::*;
pub use logout::*;
pub use sso::*;
use turbopath::AbsoluteSystemPath;
use turborepo_api_client::{CacheClient, Client, TokenClient};
use turborepo_ui::UI;

Expand All @@ -13,10 +14,7 @@ use crate::LoginServer;
const VERCEL_TOKEN_DIR: &str = "com.vercel.cli";
const VERCEL_TOKEN_FILE: &str = "auth.json";

pub struct LoginOptions<'a, T>
where
T: Client + TokenClient + CacheClient,
{
pub struct LoginOptions<'a, T: Client + TokenClient + CacheClient> {
pub ui: &'a UI,
pub login_url: &'a str,
pub api_client: &'a T,
Expand All @@ -26,10 +24,7 @@ where
pub existing_token: Option<&'a str>,
pub force: bool,
}
impl<'a, T> LoginOptions<'a, T>
where
T: Client + TokenClient + CacheClient,
{
impl<'a, T: Client + TokenClient + CacheClient> LoginOptions<'a, T> {
pub fn new(
ui: &'a UI,
login_url: &'a str,
Expand All @@ -48,6 +43,15 @@ where
}
}

/// Options for logging out.
pub struct LogoutOptions<'a, T> {
pub ui: &'a UI,
pub api_client: &'a T,

/// The path where we should look for the token to logout.
pub path: &'a AbsoluteSystemPath,
}

fn extract_vercel_token() -> Result<String, Error> {
let vercel_config_dir =
turborepo_dirs::vercel_config_dir().ok_or_else(|| Error::ConfigDirNotFound)?;
Expand Down
12 changes: 10 additions & 2 deletions crates/turborepo-auth/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::io;

use thiserror::Error;
use turbopath::AbsoluteSystemPathBuf;

#[derive(Debug, Error)]
pub enum Error {
Expand All @@ -22,16 +23,23 @@ pub enum Error {
FailedToFetchUser(#[source] turborepo_api_client::Error),
#[error("url is invalid: {0}")]
InvalidUrl(#[from] url::ParseError),

#[error("failed to validate sso token")]
FailedToValidateSSOToken(#[source] turborepo_api_client::Error),
#[error("failed to make sso token name")]
FailedToMakeSSOTokenName(#[source] io::Error),
#[error("config directory not found")]
ConfigDirNotFound,
#[error("sso team cannot be empty for login")]
EmptySSOTeam,
#[error("sso team not found: {0}")]
SSOTeamNotFound(String),
#[error("sso token expired for team: {0}")]
SSOTokenExpired(String),

#[error("config directory not found")]
ConfigDirNotFound,
#[error("failed to read auth file path: {path}")]
FailedToReadAuthFilePath {
path: AbsoluteSystemPathBuf,
error: io::Error,
},
}
3 changes: 3 additions & 0 deletions crates/turborepo-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ impl Token {
pub fn existing(token: String) -> Self {
Self::Existing(token)
}

/// Checks if the token is still valid. The checks ran are:
/// 1. If the token is active.
/// 2. If the token has access to the cache.
Expand Down Expand Up @@ -74,6 +75,7 @@ impl Token {
}
Ok(true)
}

/// This is the same as `is_valid`, but also checks if the token is valid
/// for SSO.
///
Expand Down Expand Up @@ -124,6 +126,7 @@ impl Token {
(Err(e), _) | (_, Err(e)) => Err(Error::APIError(e)),
}
}

/// Checks if the token is active. We do a few checks:
/// 1. Fetch the token metadata.
/// 2. From the metadata, check if the token is active.
Expand Down
42 changes: 8 additions & 34 deletions crates/turborepo-lib/src/commands/logout.rs
Original file line number Diff line number Diff line change
@@ -1,39 +1,13 @@
use tracing::error;
use turborepo_auth::logout as auth_logout;
use turborepo_auth::{logout as auth_logout, LogoutOptions};
use turborepo_telemetry::events::command::CommandEventBuilder;

use crate::{cli::Error, commands::CommandBase, config, rewrite_json::unset_path};
use crate::{cli::Error, commands::CommandBase};

pub fn logout(base: &mut CommandBase, _telemetry: CommandEventBuilder) -> Result<(), Error> {
if let Err(err) = remove_token(base) {
error!("could not logout. Something went wrong: {}", err);
return Err(err);
}

auth_logout(&base.ui);

Ok(())
}

fn remove_token(base: &mut CommandBase) -> Result<(), Error> {
let global_config_path = base.global_config_path()?;
let before = global_config_path
.read_existing_to_string_or(Ok("{}"))
.map_err(|e| {
Error::Config(config::Error::FailedToReadConfig {
config_path: global_config_path.clone(),
error: e,
})
})?;

if let Some(after) = unset_path(&before, &["token"], true)? {
global_config_path.create_with_contents(after).map_err(|e| {
Error::Config(config::Error::FailedToSetConfig {
config_path: global_config_path.clone(),
error: e,
})
})
} else {
Ok(())
}
auth_logout(&LogoutOptions {
ui: &base.ui,
api_client: &base.api_client()?,
path: &base.global_config_path()?,
})
.map_err(Error::from)
}
2 changes: 1 addition & 1 deletion crates/turborepo-lib/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::cell::OnceCell;

use dirs_next::config_dir;
use turbopath::{AbsoluteSystemPath, AbsoluteSystemPathBuf};
use turborepo_api_client::{APIAuth, APIClient};
use turborepo_dirs::config_dir;
use turborepo_ui::UI;

use crate::{
Expand Down
6 changes: 6 additions & 0 deletions turborepo-tests/helpers/mock_turbo_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ TMP_DIR=$(mktemp -d -t turbo-XXXXXXXXXX)
# duplicate over to XDG var so that turbo picks it up
export XDG_CONFIG_HOME=$TMP_DIR
export HOME=$TMP_DIR
export TURBO_CONFIG_DIR_PATH=$TMP_DIR
export TURBO_TELEMETRY_MESSAGE_DISABLED=1

# For Linux
Expand All @@ -21,3 +22,8 @@ echo $CONFIG > "$TMP_DIR/turborepo/config.json"
MACOS_DIR="$TMP_DIR/Library/Application Support"
mkdir -p "$MACOS_DIR/turborepo"
echo "$CONFIG" > "$MACOS_DIR/turborepo/config.json"

# For Windows
WINDOWS_DIR="$TMP_DIR\\AppData\\Roaming"
mkdir -p "$WINDOWS_DIR\\turborepo"
echo "$CONFIG" > "$WINDOWS_DIR\\turborepo\\config.json"

0 comments on commit 626fff4

Please sign in to comment.