Skip to main content

teams_cli/
config.rs

1use std::collections::BTreeMap;
2use std::path::PathBuf;
3
4use serde::{Deserialize, Serialize};
5
6use crate::error::AppError;
7
8/// The maintained, multitenant public-client registration shipped with teams-cli.
9pub const DEFAULT_CLIENT_ID: &str = "66ebad71-1604-48fc-a086-0d4caa24988b";
10pub const DEFAULT_TENANT: &str = "organizations";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Profile {
14    pub client_id: String,
15    #[serde(default = "default_tenant")]
16    pub tenant: String,
17}
18
19fn default_tenant() -> String {
20    DEFAULT_TENANT.into()
21}
22
23#[derive(Debug, Default, Serialize, Deserialize)]
24struct ConfigFile {
25    #[serde(default)]
26    active_profile: Option<String>,
27    #[serde(default)]
28    profiles: BTreeMap<String, Profile>,
29}
30
31pub fn path() -> PathBuf {
32    if let Some(base) = std::env::var_os("XDG_CONFIG_HOME").filter(|v| !v.is_empty()) {
33        PathBuf::from(base).join("teams").join("config.toml")
34    } else {
35        config_base().join("teams").join("config.toml")
36    }
37}
38
39pub fn exists() -> bool {
40    path().is_file()
41}
42
43pub fn save(profile_name: &str, profile: Profile) -> Result<PathBuf, AppError> {
44    let path = path();
45    let mut config = read_file()?;
46    config.profiles.insert(profile_name.to_string(), profile);
47    config.active_profile = Some(profile_name.to_string());
48    let parent = path
49        .parent()
50        .ok_or_else(|| AppError::Unexpected("configuration path has no parent".into()))?;
51    std::fs::create_dir_all(parent)?;
52    let body = toml::to_string_pretty(&config).map_err(|e| AppError::Unexpected(e.to_string()))?;
53    std::fs::write(&path, body)?;
54    #[cfg(unix)]
55    {
56        use std::os::unix::fs::PermissionsExt;
57        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
58    }
59    Ok(path)
60}
61
62pub fn load(requested: Option<&str>) -> Result<(String, Profile), AppError> {
63    let config = read_file()?;
64    let name = requested
65        .map(str::to_owned)
66        .or_else(|| std::env::var("TEAMS_PROFILE").ok())
67        .or(config.active_profile)
68        .unwrap_or_else(|| "default".into());
69    let stored = config.profiles.get(&name);
70    let client_id = std::env::var("TEAMS_CLIENT_ID")
71        .ok()
72        .or_else(|| stored.map(|profile| profile.client_id.clone()))
73        .ok_or_else(|| {
74            AppError::InvalidInput(format!(
75                "profile '{name}' is not configured; run `teams init` or set TEAMS_CLIENT_ID"
76            ))
77        })?;
78    let tenant = std::env::var("TEAMS_TENANT")
79        .ok()
80        .or_else(|| stored.map(|profile| profile.tenant.clone()))
81        .unwrap_or_else(default_tenant);
82    let profile = Profile { client_id, tenant };
83    Ok((name, profile))
84}
85
86#[cfg(windows)]
87fn config_base() -> PathBuf {
88    dirs::config_dir().unwrap_or_else(|| PathBuf::from(".config"))
89}
90
91#[cfg(not(windows))]
92fn config_base() -> PathBuf {
93    dirs::home_dir()
94        .map(|home| home.join(".config"))
95        .unwrap_or_else(|| PathBuf::from(".config"))
96}
97
98pub fn configured_profile(requested: Option<&str>) -> Option<(String, Profile)> {
99    load(requested).ok()
100}
101
102fn read_file() -> Result<ConfigFile, AppError> {
103    match std::fs::read_to_string(path()) {
104        Ok(body) => toml::from_str(&body)
105            .map_err(|e| AppError::InvalidInput(format!("cannot parse {}: {e}", path().display()))),
106        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(ConfigFile::default()),
107        Err(e) => Err(e.into()),
108    }
109}