From c3bd970efaa400b3cb92bdd3d46e277737c320a6 Mon Sep 17 00:00:00 2001 From: Raymond Kennedy Date: Sun, 25 Jan 2026 13:15:48 -0700 Subject: [PATCH 1/2] Refactor config dir to ~/.wvdsh and improve auth status Replaces use of platform-specific config directories with a unified ~/.wvdsh directory for credentials and cache. Enhances authentication logic to distinguish between environment variable and file-based credentials, updating the 'auth status' output accordingly. Also adds an AuthSource enum and removes the unused is_authenticated method. --- .claude/settings.json | 25 ++++++++++++++++++++++++ .claude/settings.local.json | 23 ++++++++++------------ Cargo.lock | 2 +- Cargo.toml | 2 +- src/auth.rs | 36 +++++++++++++++++++++++++++++++---- src/config.rs | 19 +++++++------------ src/dev/cert.rs | 3 +-- src/main.rs | 38 +++++++++++++++++++++++++------------ src/updater.rs | 5 ++--- 9 files changed, 105 insertions(+), 48 deletions(-) create mode 100644 .claude/settings.json diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..877cbef --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,25 @@ +{ + "permissions": { + "allow": [ + "Bash(doppler run:*)", + "WebSearch", + "Bash(cargo clean:*)", + "Bash(cargo tree:*)", + "WebFetch(domain:docs.rs)", + "WebFetch(domain:seanmonstar.com)", + "WebFetch(domain:github.com)", + "WebFetch(domain:crates.io)", + "WebFetch(domain:raw.githubusercontent.com)", + "Bash(dir:*)", + "Bash(cargo check:*)", + "Bash(git add:*)", + "Bash(git commit -m \"$\\(cat <<''EOF''\nfix: use ephemeral port for dev server to avoid Hyper-V conflicts\n\nPort 7777 falls within Windows'' excluded port range \\(7747-7846\\) reserved\nby Hyper-V/WSL2/Docker, causing \"os error 10013\" on many Windows machines.\nNow lets the OS pick an available port automatically.\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", + "Bash(grep:*)", + "Bash(agent-browser *)" + ] + }, + "enabledPlugins": { + "rust-skills@rust-skills": true, + "github@claude-plugins-official": true + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json index fe80515..60c9a02 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,19 +1,16 @@ { "permissions": { "allow": [ - "Bash(doppler run:*)", - "WebSearch", - "Bash(cargo clean:*)", - "Bash(cargo tree:*)", - "WebFetch(domain:docs.rs)", - "WebFetch(domain:seanmonstar.com)", - "WebFetch(domain:github.com)", - "WebFetch(domain:crates.io)", - "WebFetch(domain:raw.githubusercontent.com)", - "Bash(dir:*)", - "Bash(cargo check:*)", - "Bash(git add:*)", - "Bash(git commit -m \"$\\(cat <<''EOF''\nfix: use ephemeral port for dev server to avoid Hyper-V conflicts\n\nPort 7777 falls within Windows'' excluded port range \\(7747-7846\\) reserved\nby Hyper-V/WSL2/Docker, causing \"os error 10013\" on many Windows machines.\nNow lets the OS pick an available port automatically.\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")" + "Bash(cargo build:*)", + "Bash(./target/debug/wvdsh auth:*)", + "Bash(WVDSH_TOKEN=test-token-12345678 ./target/debug/wvdsh auth status:*)", + "WebFetch(domain:docs.github.com)", + "Bash(gh ruleset list:*)", + "Bash(gh ruleset check:*)", + "Bash(gh api:*)", + "Bash(while read id)", + "Bash(do gh api -X DELETE \"repos/wvdsh/docs/rulesets/$id\")", + "Bash(done)" ] } } diff --git a/Cargo.lock b/Cargo.lock index 0614fac..12c739f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3121,7 +3121,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wvdsh" -version = "0.1.46" +version = "0.1.47" dependencies = [ "anyhow", "axoupdater", diff --git a/Cargo.toml b/Cargo.toml index ea86738..6e70a22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wvdsh" -version = "0.1.46" +version = "0.1.47" edition = "2021" authors = ["Wavedash Team"] description = "Cross-platform CLI tool for uploading game projects to wavedash.com" diff --git a/src/auth.rs b/src/auth.rs index daa5732..d8cbaf4 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -10,6 +10,12 @@ struct Credentials { api_key: String, } +pub enum AuthSource { + Environment, + File, + None, +} + pub struct AuthManager; impl AuthManager { @@ -47,12 +53,38 @@ impl AuthManager { } pub fn get_api_key(&self) -> Option { + // First check environment variable (for CI) + if let Ok(api_key) = std::env::var("WVDSH_TOKEN") { + if !api_key.is_empty() { + return Some(api_key); + } + } + + // Fall back to file-based credentials let path = config::credentials_path().ok()?; let json = fs::read_to_string(&path).ok()?; let credentials: Credentials = serde_json::from_str(&json).ok()?; Some(credentials.api_key) } + pub fn get_auth_source(&self) -> AuthSource { + if std::env::var("WVDSH_TOKEN") + .map(|s| !s.is_empty()) + .unwrap_or(false) + { + AuthSource::Environment + } else if config::credentials_path() + .ok() + .and_then(|p| fs::read_to_string(p).ok()) + .and_then(|json| serde_json::from_str::(&json).ok()) + .is_some() + { + AuthSource::File + } else { + AuthSource::None + } + } + pub fn clear_credentials(&self) -> Result<()> { let path = config::credentials_path()?; if path.exists() { @@ -60,10 +92,6 @@ impl AuthManager { } Ok(()) } - - pub fn is_authenticated(&self) -> bool { - self.get_api_key().is_some() - } } fn find_available_port() -> Result { diff --git a/src/config.rs b/src/config.rs index 3644424..d4ed20e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,17 +1,13 @@ use anyhow::Result; -use directories::ProjectDirs; +use directories::BaseDirs; use serde::Deserialize; use std::path::PathBuf; -// Standard project directories configuration -pub const PROJECT_QUALIFIER: &str = "gg"; -pub const PROJECT_ORGANIZATION: &str = "wavedash"; -pub const PROJECT_APPLICATION: &str = "cli"; - -/// Get the project directories for this application -pub fn project_dirs() -> Result { - ProjectDirs::from(PROJECT_QUALIFIER, PROJECT_ORGANIZATION, PROJECT_APPLICATION) - .ok_or_else(|| anyhow::anyhow!("Could not determine project directories")) +/// Get the wvdsh config directory (~/.wvdsh) +pub fn wvdsh_dir() -> Result { + let base_dirs = BaseDirs::new() + .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; + Ok(base_dirs.home_dir().join(".wvdsh")) } #[derive(Deserialize)] @@ -52,8 +48,7 @@ pub fn get(key: &str) -> Result { /// Get the path to the credentials file pub fn credentials_path() -> Result { - let dirs = project_dirs()?; - Ok(dirs.config_dir().join("credentials.json")) + Ok(wvdsh_dir()?.join("credentials.json")) } /// Create an HTTP client configured with Cloudflare Access headers if needed diff --git a/src/dev/cert.rs b/src/dev/cert.rs index 6dbc1c3..bd68d21 100644 --- a/src/dev/cert.rs +++ b/src/dev/cert.rs @@ -16,8 +16,7 @@ const DEV_CERT_COMMON_NAME: &str = "wvdsh dev server"; const LINUX_CERT_INSTALL_PATH: &str = "/usr/local/share/ca-certificates/wvdsh-dev.crt"; pub async fn load_or_create_certificates() -> Result<(RustlsConfig, PathBuf, PathBuf)> { - let project_dirs = config::project_dirs()?; - let cert_dir = project_dirs.config_dir().join(DEV_CERT_SUBDIR); + let cert_dir = config::wvdsh_dir()?.join(DEV_CERT_SUBDIR); fs::create_dir_all(&cert_dir)?; let cert_path = cert_dir.join(DEV_CERT_NAME); diff --git a/src/main.rs b/src/main.rs index 068bbbc..19db1da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ mod file_staging; mod updater; use anyhow::Result; -use auth::{login_with_browser, AuthManager}; +use auth::{login_with_browser, AuthManager, AuthSource}; use builds::handle_build_push; use clap::{Parser, Subcommand}; use dev::handle_dev; @@ -111,18 +111,32 @@ async fn main() -> Result<()> { println!("✓ Successfully logged out"); } AuthCommands::Status => { - if auth_manager.is_authenticated() { - println!("✓ Authenticated"); - if let Some(api_key) = auth_manager.get_api_key() { - let preview = if api_key.len() > 10 { - format!("{}...{}", &api_key[..6], &api_key[api_key.len() - 3..]) - } else { - "***".to_string() - }; - println!("API Key: {}", preview); + match auth_manager.get_auth_source() { + AuthSource::Environment => { + println!("✓ Authenticated (via WVDSH_TOKEN environment variable)"); + if let Some(api_key) = auth_manager.get_api_key() { + let preview = if api_key.len() > 10 { + format!("{}...{}", &api_key[..6], &api_key[api_key.len() - 3..]) + } else { + "***".to_string() + }; + println!("Token: {}", preview); + } + } + AuthSource::File => { + println!("✓ Authenticated (via stored credentials)"); + if let Some(api_key) = auth_manager.get_api_key() { + let preview = if api_key.len() > 10 { + format!("{}...{}", &api_key[..6], &api_key[api_key.len() - 3..]) + } else { + "***".to_string() + }; + println!("API Key: {}", preview); + } + } + AuthSource::None => { + println!("Not authenticated. Run 'wvdsh auth login' or set WVDSH_TOKEN environment variable."); } - } else { - println!("Not authenticated. Run 'wvdsh auth login' to get started."); } } } diff --git a/src/updater.rs b/src/updater.rs index 6f6bc8d..3a482cd 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -19,9 +19,8 @@ struct UpdateCache { impl UpdateCache { fn cache_path() -> Result { - let project_dirs = config::project_dirs()?; - let cache_dir = project_dirs.config_dir(); - fs::create_dir_all(cache_dir)?; + let cache_dir = config::wvdsh_dir()?; + fs::create_dir_all(&cache_dir)?; Ok(cache_dir.join("update-cache.json")) } From 9f376ed261554c6a4908ddf4fe5acfaa69b2f9a9 Mon Sep 17 00:00:00 2001 From: Raymond Kennedy Date: Sun, 25 Jan 2026 13:48:44 -0700 Subject: [PATCH 2/2] Refactor auth info handling and token masking Introduced AuthInfo struct to encapsulate authentication source and API key. Refactored AuthManager to provide get_auth_info and improved API key retrieval logic. Added mask_token helper in main.rs to consistently mask tokens in status output. --- src/auth.rs | 56 ++++++++++++++++++++++++++++++----------------------- src/main.rs | 29 ++++++++++++++------------- 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/src/auth.rs b/src/auth.rs index d8cbaf4..68b6e81 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -16,6 +16,11 @@ pub enum AuthSource { None, } +pub struct AuthInfo { + pub source: AuthSource, + pub api_key: Option, +} + pub struct AuthManager; impl AuthManager { @@ -52,37 +57,40 @@ impl AuthManager { Ok(()) } - pub fn get_api_key(&self) -> Option { - // First check environment variable (for CI) - if let Ok(api_key) = std::env::var("WVDSH_TOKEN") { - if !api_key.is_empty() { - return Some(api_key); - } - } - - // Fall back to file-based credentials + fn read_file_credentials(&self) -> Option { let path = config::credentials_path().ok()?; let json = fs::read_to_string(&path).ok()?; let credentials: Credentials = serde_json::from_str(&json).ok()?; Some(credentials.api_key) } - pub fn get_auth_source(&self) -> AuthSource { - if std::env::var("WVDSH_TOKEN") - .map(|s| !s.is_empty()) - .unwrap_or(false) - { - AuthSource::Environment - } else if config::credentials_path() - .ok() - .and_then(|p| fs::read_to_string(p).ok()) - .and_then(|json| serde_json::from_str::(&json).ok()) - .is_some() - { - AuthSource::File - } else { - AuthSource::None + pub fn get_auth_info(&self) -> AuthInfo { + // Check environment first + if let Ok(api_key) = std::env::var("WVDSH_TOKEN") { + if !api_key.is_empty() { + return AuthInfo { + source: AuthSource::Environment, + api_key: Some(api_key), + }; + } + } + + // Check file + if let Some(api_key) = self.read_file_credentials() { + return AuthInfo { + source: AuthSource::File, + api_key: Some(api_key), + }; } + + AuthInfo { + source: AuthSource::None, + api_key: None, + } + } + + pub fn get_api_key(&self) -> Option { + self.get_auth_info().api_key } pub fn clear_credentials(&self) -> Result<()> { diff --git a/src/main.rs b/src/main.rs index 19db1da..b36cfb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,14 @@ use clap::{Parser, Subcommand}; use dev::handle_dev; use std::path::PathBuf; +fn mask_token(token: &str) -> String { + if token.len() > 10 { + format!("{}...{}", &token[..6], &token[token.len() - 3..]) + } else { + "***".to_string() + } +} + #[derive(Parser)] #[command(name = "wvdsh")] #[command(about = "Cross-platform CLI tool for uploading game projects to wavedash.com")] @@ -111,27 +119,18 @@ async fn main() -> Result<()> { println!("✓ Successfully logged out"); } AuthCommands::Status => { - match auth_manager.get_auth_source() { + let auth_info = auth_manager.get_auth_info(); + match auth_info.source { AuthSource::Environment => { println!("✓ Authenticated (via WVDSH_TOKEN environment variable)"); - if let Some(api_key) = auth_manager.get_api_key() { - let preview = if api_key.len() > 10 { - format!("{}...{}", &api_key[..6], &api_key[api_key.len() - 3..]) - } else { - "***".to_string() - }; - println!("Token: {}", preview); + if let Some(api_key) = auth_info.api_key { + println!("Token: {}", mask_token(&api_key)); } } AuthSource::File => { println!("✓ Authenticated (via stored credentials)"); - if let Some(api_key) = auth_manager.get_api_key() { - let preview = if api_key.len() > 10 { - format!("{}...{}", &api_key[..6], &api_key[api_key.len() - 3..]) - } else { - "***".to_string() - }; - println!("API Key: {}", preview); + if let Some(api_key) = auth_info.api_key { + println!("API Key: {}", mask_token(&api_key)); } } AuthSource::None => {