Skip to main content

things3_cloud/
auth.rs

1use std::fs;
2
3use anyhow::{Context, Result, anyhow};
4use figment::{
5    Figment,
6    providers::{Env, Format, Json},
7};
8use serde::{Deserialize, Serialize};
9
10use crate::dirs::auth_file_path;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13struct AuthPayload {
14    email: String,
15    password: String,
16}
17
18#[derive(Debug, Clone, Deserialize)]
19struct AuthConfig {
20    email: Option<String>,
21    password: Option<String>,
22}
23
24fn load_auth_config(path: &std::path::Path) -> Result<AuthConfig> {
25    let mut figment = Figment::new();
26    if path.exists() {
27        figment = figment.merge(Json::file(path));
28    }
29    figment
30        .merge(Env::prefixed("THINGS3_"))
31        .extract()
32        .with_context(|| format!("Failed reading auth config at {}", path.display()))
33}
34
35fn validate_auth(email: &str, password: &str) -> Result<(String, String)> {
36    let email = email.trim().to_string();
37    let password = password.to_string();
38
39    if email.is_empty() {
40        return Err(anyhow!("Missing auth email."));
41    }
42    if password.is_empty() {
43        return Err(anyhow!("Missing auth password."));
44    }
45
46    Ok((email, password))
47}
48
49pub fn load_auth() -> Result<(String, String)> {
50    let path = auth_file_path();
51
52    let cfg = load_auth_config(&path)?;
53
54    let Some(email) = cfg.email else {
55        return Err(anyhow!(
56            "Missing auth email. Set THINGS3_EMAIL or run `things3 set-auth` to create {}.",
57            path.display()
58        ));
59    };
60
61    let Some(password) = cfg.password else {
62        return Err(anyhow!(
63            "Missing auth password. Set THINGS3_PASSWORD or run `things3 set-auth` to update {}.",
64            path.display()
65        ));
66    };
67
68    validate_auth(&email, &password)
69}
70
71pub fn write_auth(email: &str, password: &str) -> Result<std::path::PathBuf> {
72    let (email, password) = validate_auth(email, password)?;
73    let path = auth_file_path();
74    let parent = path
75        .parent()
76        .ok_or_else(|| anyhow!("Invalid auth file path"))?
77        .to_path_buf();
78    fs::create_dir_all(&parent).with_context(|| format!("Failed creating {}", parent.display()))?;
79
80    let payload = AuthPayload { email, password };
81    let serialized = serde_json::to_string(&payload)?;
82    let tmp_path = path.with_extension("tmp");
83    fs::write(&tmp_path, serialized)
84        .with_context(|| format!("Failed writing {}", tmp_path.display()))?;
85    fs::rename(&tmp_path, &path)
86        .with_context(|| format!("Failed finalizing {}", path.display()))?;
87
88    #[cfg(unix)]
89    {
90        use std::os::unix::fs::PermissionsExt;
91        let mut perms = fs::metadata(&path)?.permissions();
92        perms.set_mode(0o600);
93        fs::set_permissions(&path, perms)?;
94    }
95
96    Ok(path)
97}