edgee_api_client/
auth.rs

1use std::path::PathBuf;
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7    connect_builder::{IsUnset, SetApiToken, SetBaseurl, State},
8    ConnectBuilder,
9};
10
11#[derive(Debug, Deserialize, Default, Serialize, Clone)]
12pub struct Config {
13    #[serde(default)]
14    api_token: Option<String>,
15    #[serde(default)]
16    url: Option<String>,
17
18    #[serde(flatten)]
19    profiles: std::collections::HashMap<String, Credentials>,
20}
21
22#[derive(Debug, Deserialize, Default, Serialize, Clone)]
23pub struct Credentials {
24    pub api_token: String,
25    #[serde(default)]
26    pub url: Option<String>,
27}
28
29impl Config {
30    pub fn path() -> Result<PathBuf> {
31        let config_dir = dirs::config_dir()
32            .ok_or_else(|| anyhow::anyhow!("Could not get user config directory"))?
33            .join("edgee");
34        if !config_dir.exists() {
35            std::fs::create_dir_all(&config_dir).context("Could not create Edgee config dir")?;
36        }
37
38        Ok(config_dir.join("credentials.toml"))
39    }
40
41    pub fn load() -> Result<Self> {
42        // if EDGEE_API_URL and EDGEE_API_TOKEN are set, use them
43        // skip using the credentials file
44        if let Ok(api_token) = std::env::var("EDGEE_API_TOKEN") {
45            return Ok(Self {
46                api_token: Some(api_token),
47                url: Some(
48                    std::env::var("EDGEE_API_URL").unwrap_or("https://api.edgee.app".to_string()),
49                ),
50                ..Default::default()
51            });
52        };
53
54        let creds_path = Self::path()?;
55        if !creds_path.exists() {
56            return Ok(Self::default());
57        }
58
59        let content =
60            std::fs::read_to_string(creds_path).context("Could not read credentials file")?;
61        toml::from_str(&content).context("Could not load credentials file")
62    }
63
64    pub fn save(&self) -> Result<()> {
65        use std::io::Write;
66
67        let content =
68            toml::to_string_pretty(self).context("Could not serialize credentials data")?;
69
70        let creds_path = Self::path()?;
71
72        let mut file = {
73            use std::fs::OpenOptions;
74
75            let mut options = OpenOptions::new();
76            options.write(true).create(true).truncate(true);
77
78            #[cfg(unix)]
79            {
80                use std::os::unix::fs::OpenOptionsExt;
81
82                // Set credentials file permissions to 0600 (u=rw-,g=,o=)
83                // so only the user has access.
84                options.mode(0o0600);
85            }
86
87            options.open(creds_path)?
88        };
89
90        file.write_all(content.as_bytes())
91            .context("Could not write credentials data")
92    }
93
94    pub fn get(&self, profile: &Option<String>) -> Option<Credentials> {
95        match profile {
96            Some(profile) => self.profiles.get(profile).cloned(),
97            None => match (self.api_token.clone(), self.url.clone()) {
98                (Some(api_token), Some(url)) => Some(Credentials {
99                    api_token,
100                    url: Some(url),
101                }),
102                (Some(api_token), _) => Some(Credentials {
103                    api_token,
104                    url: Some("https://api.edgee.app".to_string()),
105                }),
106                _ => None,
107            },
108        }
109    }
110
111    pub fn set(&mut self, profile: Option<String>, creds: Credentials) {
112        match profile {
113            Some(profile) => {
114                self.profiles.insert(profile, creds);
115            }
116            None => {
117                self.api_token = Some(creds.api_token);
118                self.url = creds.url;
119            }
120        }
121    }
122}
123
124impl Credentials {
125    pub fn check_api_token(&self) -> Result<()> {
126        // TODO: Check API token is valid using the API
127        Ok(())
128    }
129}
130
131impl<S: State> ConnectBuilder<S> {
132    pub fn credentials(self, creds: &Credentials) -> ConnectBuilder<SetApiToken<SetBaseurl<S>>>
133    where
134        S::ApiToken: IsUnset,
135        S::Baseurl: IsUnset,
136    {
137        let api_token = creds.api_token.clone();
138        let url = creds.url.clone();
139        self.baseurl(url.unwrap_or("https://api.edgee.app".to_string()))
140            .api_token(api_token)
141    }
142}