mod migration;
mod tokens;
pub mod types;
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
#[allow(unused_imports)] pub use types::{App, OAuth1Token, OAuth2Token, Token, TokenType};
use crate::error::{Result, XurlError};
pub struct TokenStore {
pub apps: BTreeMap<String, App>,
pub default_app: String,
pub file_path: PathBuf,
}
impl Default for TokenStore {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)] impl TokenStore {
#[must_use]
pub fn new() -> Self {
Self::with_credentials("", "")
}
#[must_use]
pub fn with_credentials(client_id: &str, client_secret: &str) -> Self {
let home_dir = dirs::home_dir().unwrap_or_else(|| PathBuf::from("."));
let file_path = home_dir.join(".xurl");
let mut store = TokenStore {
apps: BTreeMap::new(),
default_app: String::new(),
file_path,
};
if let Ok(data) = fs::read(&store.file_path) {
store.load_from_data(&data);
}
if !client_id.is_empty() || !client_secret.is_empty() {
let mut dirty = false;
for app in store.apps.values_mut() {
if app.has_tokens() {
if app.client_id.is_empty() && !client_id.is_empty() {
app.client_id = client_id.to_string();
dirty = true;
}
if app.client_secret.is_empty() && !client_secret.is_empty() {
app.client_secret = client_secret.to_string();
dirty = true;
}
}
}
if dirty {
let _ = store.save_to_file();
}
}
if store.apps.is_empty() {
store.default_app = "default".to_string();
store.apps.insert("default".to_string(), App::new());
}
let needs_import = match store.active_app() {
None => true,
Some(app) => app.oauth1_token.is_none() || app.bearer_token.is_none(),
};
if needs_import {
let twurlrc_path = home_dir.join(".twurlrc");
if twurlrc_path.exists()
&& let Err(e) = store.import_from_twurlrc(&twurlrc_path)
{
eprintln!("Error importing from .twurlrc: {e}");
}
}
store
}
#[must_use]
pub fn new_with_path(path: &str) -> Self {
let file_path = PathBuf::from(path);
let mut store = TokenStore {
apps: BTreeMap::new(),
default_app: String::new(),
file_path,
};
if let Ok(data) = fs::read(&store.file_path) {
store.load_from_data(&data);
}
if store.apps.is_empty() {
store.apps.insert("default".to_string(), App::new());
store.default_app = "default".to_string();
}
store
}
#[must_use]
pub fn new_with_credentials_and_path(client_id: &str, client_secret: &str, path: &str) -> Self {
let mut store = Self::new_with_path(path);
if !client_id.is_empty() || !client_secret.is_empty() {
for app in store.apps.values_mut() {
if app.has_tokens() || app.client_id.is_empty() {
if app.client_id.is_empty() && !client_id.is_empty() {
app.client_id = client_id.to_string();
}
if app.client_secret.is_empty() && !client_secret.is_empty() {
app.client_secret = client_secret.to_string();
}
}
}
let _ = store.save_to_file();
}
store
}
#[must_use]
pub fn new_with_home(home: &str) -> Self {
let home_path = PathBuf::from(home);
let file_path = home_path.join(".xurl");
let mut store = TokenStore {
apps: BTreeMap::new(),
default_app: String::new(),
file_path,
};
if let Ok(data) = fs::read(&store.file_path) {
store.load_from_data(&data);
}
if store.apps.is_empty() {
store.apps.insert("default".to_string(), App::new());
store.default_app = "default".to_string();
}
let needs_import = match store.active_app() {
None => true,
Some(app) => app.oauth1_token.is_none(),
};
if needs_import {
let twurlrc_path = home_path.join(".twurlrc");
if twurlrc_path.exists() {
let _ = store.import_from_twurlrc(&twurlrc_path);
}
}
store
}
#[must_use]
pub fn load_from_path(path: &str) -> Self {
Self::new_with_path(path)
}
pub fn add_app(&mut self, name: &str, client_id: &str, client_secret: &str) -> Result<()> {
if self.apps.contains_key(name) {
return Err(XurlError::token_store(format!(
"app {name:?} already exists"
)));
}
self.apps.insert(
name.to_string(),
App::with_credentials(client_id, client_secret),
);
if self.apps.len() == 1 {
self.default_app = name.to_string();
}
self.save_to_file()
}
pub fn update_app(&mut self, name: &str, client_id: &str, client_secret: &str) -> Result<()> {
let app = self
.apps
.get_mut(name)
.ok_or_else(|| XurlError::token_store(format!("app {name:?} not found")))?;
if !client_id.is_empty() {
app.client_id = client_id.to_string();
}
if !client_secret.is_empty() {
app.client_secret = client_secret.to_string();
}
self.save_to_file()
}
pub fn remove_app(&mut self, name: &str) -> Result<()> {
if !self.apps.contains_key(name) {
return Err(XurlError::token_store(format!("app {name:?} not found")));
}
self.apps.remove(name);
if self.default_app == name {
self.default_app = self.apps.keys().next().cloned().unwrap_or_default();
}
self.save_to_file()
}
pub fn set_default_app(&mut self, name: &str) -> Result<()> {
if !self.apps.contains_key(name) {
return Err(XurlError::token_store(format!("app {name:?} not found")));
}
self.default_app = name.to_string();
self.save_to_file()
}
#[must_use]
pub fn list_apps(&self) -> Vec<String> {
self.apps.keys().cloned().collect()
}
#[must_use]
pub fn get_app(&self, name: &str) -> Option<&App> {
self.apps.get(name)
}
pub fn set_default_user(&mut self, app_name: &str, username: &str) -> Result<()> {
let app = self.resolve_app_mut(app_name);
if !app.oauth2_tokens.contains_key(username) {
return Err(XurlError::token_store(format!(
"user {username:?} not found in app"
)));
}
app.default_user = username.to_string();
self.save_to_file()
}
#[must_use]
pub fn get_default_user(&self, app_name: &str) -> &str {
let app = self.resolve_app(app_name);
&app.default_user
}
#[must_use]
pub fn get_default_app(&self) -> &str {
&self.default_app
}
#[must_use]
pub fn get_active_app_name<'a>(&'a self, explicit: &'a str) -> &'a str {
if explicit.is_empty() {
&self.default_app
} else {
explicit
}
}
pub(crate) fn active_app(&self) -> Option<&App> {
self.apps.get(&self.default_app)
}
pub(crate) fn active_app_or_create(&mut self) -> &mut App {
if !self.apps.contains_key(&self.default_app) {
self.apps.insert("default".to_string(), App::new());
if self.default_app.is_empty() {
self.default_app = "default".to_string();
}
}
let key = if self.apps.contains_key(&self.default_app) {
self.default_app.clone()
} else {
"default".to_string()
};
self.apps.get_mut(&key).expect("just inserted")
}
#[must_use]
pub fn resolve_app(&self, name: &str) -> &App {
if !name.is_empty()
&& let Some(app) = self.apps.get(name)
{
return app;
}
self.apps.get(&self.default_app).unwrap_or_else(|| {
static EMPTY: std::sync::LazyLock<App> = std::sync::LazyLock::new(App::new);
&EMPTY
})
}
pub fn resolve_app_mut(&mut self, name: &str) -> &mut App {
if !name.is_empty() && self.apps.contains_key(name) {
return self.apps.get_mut(name).expect("just checked");
}
self.active_app_or_create()
}
pub(crate) fn save_to_file(&self) -> Result<()> {
let sf = types::StoreFile {
apps: self.apps.clone(),
default_app: self.default_app.clone(),
};
let data = serde_yaml::to_string(&sf).map_err(|e| XurlError::Json(e.to_string()))?;
fs::write(&self.file_path, data)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
fs::set_permissions(&self.file_path, perms)?;
}
Ok(())
}
}