diff --git a/src/cli/auth.rs b/src/cli/auth.rs index ddea6e2..437055e 100644 --- a/src/cli/auth.rs +++ b/src/cli/auth.rs @@ -64,7 +64,7 @@ pub fn get_token_login( ) -> anyhow::Result { let response = reqwest .get("https://api.github.com/user") - .header("Authorization", format!("Bearer {access_token}")) + .header("Authorization", access_token) .send() .context("failed to send user request")? .error_for_status() diff --git a/src/cli/commands/auth/login.rs b/src/cli/commands/auth/login.rs index ebc6992..ffe729b 100644 --- a/src/cli/commands/auth/login.rs +++ b/src/cli/commands/auth/login.rs @@ -1,17 +1,19 @@ -use crate::cli::{ - auth::{get_token_login, set_token}, - config::read_config, -}; use anyhow::Context; use clap::Args; use colored::Colorize; +use serde::Deserialize; +use url::Url; + use pesde::{ errors::ManifestReadError, source::{pesde::PesdePackageSource, traits::PackageSource}, Project, }; -use serde::Deserialize; -use url::Url; + +use crate::cli::{ + auth::{get_token_login, set_token}, + config::read_config, +}; #[derive(Debug, Args)] pub struct LoginCommand { @@ -19,6 +21,10 @@ pub struct LoginCommand { #[arg(short, long)] index: Option, + /// Whether to not prefix the token with `Bearer ` + #[arg(short, long, conflicts_with = "token")] + no_bearer: bool, + /// The token to use for authentication, skipping login #[arg(short, long, conflicts_with = "index")] token: Option, @@ -184,7 +190,15 @@ impl LoginCommand { None => self.authenticate_device_flow(&project, &reqwest)?, }; - println!("logged in as {}", get_token_login(&reqwest, &token)?.bold()); + let token = if self.no_bearer { + println!("set token"); + token + } else { + let token = format!("Bearer {token}"); + println!("logged in as {}", get_token_login(&reqwest, &token)?.bold()); + + token + }; set_token(Some(&token))?; diff --git a/src/cli/commands/auth/mod.rs b/src/cli/commands/auth/mod.rs index d32cd46..f5fb0ca 100644 --- a/src/cli/commands/auth/mod.rs +++ b/src/cli/commands/auth/mod.rs @@ -3,6 +3,7 @@ use pesde::Project; mod login; mod logout; +mod set_token_override; mod whoami; #[derive(Debug, Subcommand)] @@ -14,6 +15,8 @@ pub enum AuthCommands { /// Prints the username of the currently logged-in user #[clap(name = "whoami")] WhoAmI(whoami::WhoAmICommand), + /// Sets a token override for a specific repository + SetTokenOverride(set_token_override::SetTokenOverrideCommand), } impl AuthCommands { @@ -22,6 +25,7 @@ impl AuthCommands { AuthCommands::Login(login) => login.run(project, reqwest), AuthCommands::Logout(logout) => logout.run(), AuthCommands::WhoAmI(whoami) => whoami.run(reqwest), + AuthCommands::SetTokenOverride(set_token_override) => set_token_override.run(), } } } diff --git a/src/cli/commands/auth/set_token_override.rs b/src/cli/commands/auth/set_token_override.rs new file mode 100644 index 0000000..b2de68e --- /dev/null +++ b/src/cli/commands/auth/set_token_override.rs @@ -0,0 +1,31 @@ +use crate::cli::config::{read_config, write_config}; +use clap::Args; + +#[derive(Debug, Args)] +pub struct SetTokenOverrideCommand { + /// The repository to add the token to + #[arg(index = 1, value_parser = crate::cli::parse_gix_url)] + repository: gix::Url, + + /// The token to set + #[arg(index = 2)] + token: Option, +} + +impl SetTokenOverrideCommand { + pub fn run(self) -> anyhow::Result<()> { + let mut config = read_config()?; + + if let Some(token) = self.token { + println!("set token for {}", self.repository); + config.token_overrides.insert(self.repository, token); + } else { + println!("removed token for {}", self.repository); + config.token_overrides.remove(&self.repository); + } + + write_config(&config)?; + + Ok(()) + } +} diff --git a/src/cli/config.rs b/src/cli/config.rs index 3da76a1..675b306 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -1,3 +1,5 @@ +use std::collections::BTreeMap; + use anyhow::Context; use serde::{Deserialize, Serialize}; @@ -15,8 +17,16 @@ pub struct CliConfig { deserialize_with = "crate::util::deserialize_gix_url" )] pub scripts_repo: gix::Url, + #[serde(default, skip_serializing_if = "Option::is_none")] pub token: Option, + #[serde( + default, + skip_serializing_if = "BTreeMap::is_empty", + serialize_with = "crate::cli::serialize_string_url_map", + deserialize_with = "crate::cli::deserialize_string_url_map" + )] + pub token_overrides: BTreeMap, #[serde(default, skip_serializing_if = "Option::is_none")] pub last_checked_updates: Option<(chrono::DateTime, semver::Version)>, @@ -31,7 +41,9 @@ impl Default for CliConfig { scripts_repo: "https://github.com/daimond113/pesde-scripts" .try_into() .unwrap(), + token: None, + token_overrides: Default::default(), last_checked_updates: None, } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index cad8a2d..e2e9882 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,10 +1,14 @@ -use std::{collections::HashSet, fs::create_dir_all, str::FromStr}; - use anyhow::Context; - +use gix::bstr::BStr; use pesde::{ lockfile::DownloadedGraph, names::PackageNames, source::version_id::VersionId, Project, }; +use serde::{ser::SerializeMap, Deserialize, Deserializer, Serializer}; +use std::{ + collections::{BTreeMap, HashSet}, + fs::create_dir_all, + str::FromStr, +}; use crate::cli::auth::get_token; @@ -168,3 +172,27 @@ impl, E: Into, N: FromStr, F: Into Result { s.try_into() } + +pub fn serialize_string_url_map( + url: &BTreeMap, + serializer: S, +) -> Result { + let mut map = serializer.serialize_map(Some(url.len()))?; + for (k, v) in url { + map.serialize_entry(&k.to_bstring().to_string(), v)?; + } + map.end() +} + +pub fn deserialize_string_url_map<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + BTreeMap::::deserialize(deserializer)? + .into_iter() + .map(|(k, v)| { + gix::Url::from_bytes(BStr::new(&k)) + .map(|k| (k, v)) + .map_err(serde::de::Error::custom) + }) + .collect() +} diff --git a/src/lib.rs b/src/lib.rs index 5b3aaee..74d3bfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,11 @@ compile_error!("at least one of the features `roblox`, `lune`, or `luau` must be enabled"); use crate::lockfile::Lockfile; -use std::path::{Path, PathBuf}; +use gix::sec::identity::Account; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; /// Downloading packages pub mod download; @@ -43,8 +47,9 @@ pub(crate) const LINK_LIB_NO_FILE_FOUND: &str = "____pesde_no_export_file_found" /// Struct containing the authentication configuration #[derive(Debug, Default, Clone)] pub struct AuthConfig { - github_token: Option, - git_credentials: Option, + default_token: Option, + token_overrides: HashMap, + git_credentials: Option, } impl AuthConfig { @@ -53,30 +58,51 @@ impl AuthConfig { AuthConfig::default() } - /// Access the GitHub token - pub fn github_token(&self) -> Option<&str> { - self.github_token.as_deref() + /// Sets the default token + pub fn with_default_token>(mut self, token: Option) -> Self { + self.default_token = token.map(|s| s.as_ref().to_string()); + self } - /// Access the git credentials - pub fn git_credentials(&self) -> Option<&gix::sec::identity::Account> { - self.git_credentials.as_ref() - } - - /// Set the GitHub token - pub fn with_github_token>(mut self, token: Option) -> Self { - self.github_token = token.map(|s| s.as_ref().to_string()); + /// Set the token overrides + pub fn with_token_overrides, S: AsRef>( + mut self, + tokens: I, + ) -> Self { + self.token_overrides = tokens + .into_iter() + .map(|(url, s)| (url, s.as_ref().to_string())) + .collect(); self } /// Set the git credentials - pub fn with_git_credentials( - mut self, - git_credentials: Option, - ) -> Self { + pub fn with_git_credentials(mut self, git_credentials: Option) -> Self { self.git_credentials = git_credentials; self } + + /// Get the default token + pub fn default_token(&self) -> Option<&str> { + self.default_token.as_deref() + } + + /// Get the token overrides + pub fn token_overrides(&self) -> &HashMap { + &self.token_overrides + } + + /// Get the git credentials + pub fn git_credentials(&self) -> Option<&Account> { + self.git_credentials.as_ref() + } + + pub(crate) fn get_token(&self, url: &gix::Url) -> Option<&str> { + self.token_overrides + .get(url) + .map(|s| s.as_str()) + .or(self.default_token.as_deref()) + } } /// The main struct of the pesde library, representing a project diff --git a/src/main.rs b/src/main.rs index 67d32d1..32a0dce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use pesde::{AuthConfig, Project, MANIFEST_FILE_NAME}; use crate::cli::{ auth::get_token, + config::read_config, home_dir, scripts::update_scripts_folder, version::{check_for_updates, current_version, get_or_download_version, max_installed_version}, @@ -140,7 +141,9 @@ fn run() -> anyhow::Result<()> { project_root_dir, data_dir, cas_dir, - AuthConfig::new().with_github_token(token.as_ref()), + AuthConfig::new() + .with_default_token(token.clone()) + .with_token_overrides(read_config()?.token_overrides), ); let reqwest = { diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index e5cd2d7..9080d3a 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -277,9 +277,9 @@ impl PackageSource for PesdePackageSource { let mut response = reqwest.get(url).header(ACCEPT, "application/octet-stream"); - if let Some(token) = &project.auth_config.github_token { + if let Some(token) = project.auth_config.get_token(&self.repo_url) { log::debug!("using token for pesde package download"); - response = response.header("Authorization", format!("Bearer {token}")); + response = response.header("Authorization", token); } let response = response.send()?.error_for_status()?; diff --git a/src/source/wally/mod.rs b/src/source/wally/mod.rs index 6f23e68..d77c946 100644 --- a/src/source/wally/mod.rs +++ b/src/source/wally/mod.rs @@ -184,9 +184,9 @@ impl PackageSource for WallyPackageSource { .unwrap_or("0.3.2"), ); - if let Some(token) = &project.auth_config.github_token { + if let Some(token) = project.auth_config.get_token(&self.repo_url) { log::debug!("using token for wally package download"); - response = response.header("Authorization", format!("Bearer {token}")); + response = response.header("Authorization", token); } let response = response.send()?.error_for_status()?; diff --git a/src/util.rs b/src/util.rs index 228e4e9..9e62d20 100644 --- a/src/util.rs +++ b/src/util.rs @@ -30,6 +30,13 @@ pub fn serialize_gix_url(url: &gix::Url, serializer: S) -> Result serializer.serialize_str(&url.to_bstring().to_string()) } +pub fn deserialize_gix_url<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result { + let s = String::deserialize(deserializer)?; + gix::Url::from_bytes(BStr::new(&s)).map_err(serde::de::Error::custom) +} + pub fn serialize_gix_url_map( url: &BTreeMap, serializer: S, @@ -41,13 +48,6 @@ pub fn serialize_gix_url_map( map.end() } -pub fn deserialize_gix_url<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result { - let s = String::deserialize(deserializer)?; - gix::Url::from_bytes(BStr::new(&s)).map_err(serde::de::Error::custom) -} - pub fn deserialize_gix_url_map<'de, D: Deserializer<'de>>( deserializer: D, ) -> Result, D::Error> {