indieweb_cli_common/
token.rs1use directories::ProjectDirs;
2use secrecy::{ExposeSecret, SecretString};
3use std::fs;
4use std::io;
5use std::path::PathBuf;
6
7use crate::error::{CliError, Result};
8
9const TOKEN_FILE_PERMISSIONS: u32 = 0o600;
10
11pub struct TokenStore {
12 service_name: &'static str,
13}
14
15impl TokenStore {
16 pub fn new(service_name: &'static str) -> Self {
17 Self { service_name }
18 }
19
20 pub fn micropub() -> Self {
21 Self::new("micropub")
22 }
23
24 pub fn indieauth() -> Self {
25 Self::new("indieauth")
26 }
27
28 fn token_path(&self) -> Option<PathBuf> {
29 ProjectDirs::from("org", "indieweb", "indieweb").map(|dirs| {
30 dirs.data_local_dir()
31 .join(format!("{}-token", self.service_name))
32 })
33 }
34
35 pub fn load(&self) -> Result<Option<SecretString>> {
36 let path = self.token_path().ok_or_else(|| {
37 CliError::Config(Box::new(io::Error::new(
38 io::ErrorKind::NotFound,
39 "Could not determine token storage path",
40 )))
41 })?;
42
43 if !path.exists() {
44 return Ok(None);
45 }
46
47 let contents = fs::read_to_string(&path)?;
48 let token = contents.trim().to_string();
49
50 if token.is_empty() {
51 Ok(None)
52 } else {
53 Ok(Some(SecretString::new(token.into_boxed_str())))
54 }
55 }
56
57 pub fn save(&self, token: &SecretString) -> Result<()> {
58 let path = self.token_path().ok_or_else(|| {
59 CliError::Config(Box::new(io::Error::new(
60 io::ErrorKind::NotFound,
61 "Could not determine token storage path",
62 )))
63 })?;
64
65 if let Some(parent) = path.parent() {
66 fs::create_dir_all(parent)?;
67 }
68
69 fs::write(&path, token.expose_secret())?;
70
71 #[cfg(unix)]
72 {
73 use std::os::unix::fs::PermissionsExt;
74 fs::set_permissions(&path, fs::Permissions::from_mode(TOKEN_FILE_PERMISSIONS))?;
75 }
76
77 Ok(())
78 }
79
80 pub fn delete(&self) -> Result<()> {
81 if let Some(path) = self.token_path() {
82 if path.exists() {
83 fs::remove_file(&path)?;
84 }
85 }
86 Ok(())
87 }
88
89 pub fn exists(&self) -> bool {
90 self.token_path().map(|p| p.exists()).unwrap_or(false)
91 }
92
93 pub fn resolve_token(
94 &self,
95 cli_token: Option<&String>,
96 env_token: Option<&String>,
97 ) -> Result<Option<SecretString>> {
98 if let Some(token) = cli_token {
99 return Ok(Some(SecretString::new(token.clone().into_boxed_str())));
100 }
101
102 if let Some(token) = env_token {
103 return Ok(Some(SecretString::new(token.clone().into_boxed_str())));
104 }
105
106 self.load()
107 }
108}