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        let creds_path = Self::path()?;
43        if !creds_path.exists() {
44            return Ok(Self::default());
45        }
46
47        let content =
48            std::fs::read_to_string(creds_path).context("Could not read credentials file")?;
49        toml::from_str(&content).context("Could not load credentials file")
50    }
51
52    pub fn save(&self) -> Result<()> {
53        use std::io::Write;
54
55        let content =
56            toml::to_string_pretty(self).context("Could not serialize credentials data")?;
57
58        let creds_path = Self::path()?;
59
60        let mut file = {
61            use std::fs::OpenOptions;
62
63            let mut options = OpenOptions::new();
64            options.write(true).create(true).truncate(true);
65
66            #[cfg(unix)]
67            {
68                use std::os::unix::fs::OpenOptionsExt;
69
70                // Set credentials file permissions to 0600 (u=rw-,g=,o=)
71                // so only the user has access.
72                options.mode(0o0600);
73            }
74
75            options.open(creds_path)?
76        };
77
78        file.write_all(content.as_bytes())
79            .context("Could not write credentials data")
80    }
81
82    pub fn get(&self, profile: &Option<String>) -> Option<Credentials> {
83        match profile {
84            Some(profile) => self.profiles.get(profile).cloned(),
85            None => match (self.api_token.clone(), self.url.clone()) {
86                (Some(api_token), Some(url)) => Some(Credentials {
87                    api_token,
88                    url: Some(url),
89                }),
90                (Some(api_token), _) => Some(Credentials {
91                    api_token,
92                    url: Some("https://api.edgee.app".to_string()),
93                }),
94                _ => None,
95            },
96        }
97    }
98
99    pub fn set(&mut self, profile: Option<String>, creds: Credentials) {
100        match profile {
101            Some(profile) => {
102                self.profiles.insert(profile, creds);
103            }
104            None => {
105                self.api_token = Some(creds.api_token);
106                self.url = creds.url;
107            }
108        }
109    }
110}
111
112impl Credentials {
113    pub fn check_api_token(&self) -> Result<()> {
114        // TODO: Check API token is valid using the API
115        Ok(())
116    }
117}
118
119impl<S: State> ConnectBuilder<S> {
120    pub fn credentials(self, creds: &Credentials) -> ConnectBuilder<SetApiToken<SetBaseurl<S>>>
121    where
122        S::ApiToken: IsUnset,
123        S::Baseurl: IsUnset,
124    {
125        let api_token = creds.api_token.clone();
126        let url = creds.url.clone();
127        self.baseurl(url.unwrap_or("https://api.edgee.app".to_string()))
128            .api_token(api_token)
129    }
130}