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
Binary file modified .DS_Store
Binary file not shown.
44 changes: 44 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ strip-ansi-escapes = "0.2" # Strip ANSI codes for testing

# Dockerfile linting (hadolint-rs)
nom = "7" # Parser combinators for Dockerfile parsing
anyhow = "1.0.100"
open = "5.3.3"

[dev-dependencies]
assert_cmd = "2"
Expand Down
117 changes: 117 additions & 0 deletions src/auth/credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
//! Credential storage and retrieval for Syncable authentication
//!
//! Stores authentication tokens in ~/.syncable.toml

use crate::config::{load_config, save_global_config, types::SyncableAuth};
use anyhow::Result;
use std::time::{SystemTime, UNIX_EPOCH};

/// Save credentials to global config file
pub fn save_credentials(
access_token: &str,
refresh_token: Option<&str>,
user_email: Option<&str>,
expires_in_secs: Option<u64>,
) -> Result<()> {
let mut config = load_config(None).unwrap_or_default();

let expires_at = expires_in_secs.map(|secs| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
+ secs
});

config.syncable_auth = SyncableAuth {
access_token: Some(access_token.to_string()),
refresh_token: refresh_token.map(|s| s.to_string()),
expires_at,
user_email: user_email.map(|s| s.to_string()),
};

save_global_config(&config)?;
Ok(())
}

/// Get the current access token if valid
pub fn get_access_token() -> Option<String> {
let config = load_config(None).ok()?;

// Check expiry
if let Some(expires_at) = config.syncable_auth.expires_at {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()?
.as_secs();
if now > expires_at {
return None; // Token expired
}
}

config.syncable_auth.access_token
}

/// Get the authenticated user's email
pub fn get_user_email() -> Option<String> {
let config = load_config(None).ok()?;
config.syncable_auth.user_email
}

/// Check if the user is currently authenticated with a valid token
pub fn is_authenticated() -> bool {
get_access_token().is_some()
}

/// Get authentication status including expiry info
pub fn get_auth_status() -> AuthStatus {
let config = match load_config(None) {
Ok(c) => c,
Err(_) => return AuthStatus::NotAuthenticated,
};

match &config.syncable_auth.access_token {
None => AuthStatus::NotAuthenticated,
Some(_) => {
if let Some(expires_at) = config.syncable_auth.expires_at {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);

if now > expires_at {
return AuthStatus::Expired;
}

AuthStatus::Authenticated {
email: config.syncable_auth.user_email.clone(),
expires_at: Some(expires_at),
}
} else {
AuthStatus::Authenticated {
email: config.syncable_auth.user_email.clone(),
expires_at: None,
}
}
}
}
}

/// Clear stored credentials (logout)
pub fn clear_credentials() -> Result<()> {
let mut config = load_config(None).unwrap_or_default();
config.syncable_auth = SyncableAuth::default();
save_global_config(&config)?;
Ok(())
}

/// Authentication status enum
#[derive(Debug)]
pub enum AuthStatus {
NotAuthenticated,
Expired,
Authenticated {
email: Option<String>,
expires_at: Option<u64>,
},
}
Loading
Loading