lust/packages/
credentials.rs1use dirs::home_dir;
2use std::{
3 fs,
4 io::{self, Read, Write},
5 path::PathBuf,
6};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
10pub enum CredentialsError {
11 #[error("unable to determine user home directory")]
12 HomeDirUnavailable,
13
14 #[error("failed to access credentials at {path}: {source}")]
15 Io {
16 path: PathBuf,
17 #[source]
18 source: io::Error,
19 },
20}
21
22#[derive(Debug, Clone)]
23pub struct Credentials {
24 token: String,
25}
26
27impl Credentials {
28 pub fn new(token: impl Into<String>) -> Self {
29 Self {
30 token: token.into(),
31 }
32 }
33
34 pub fn token(&self) -> &str {
35 &self.token
36 }
37}
38
39pub fn credentials_file() -> Result<PathBuf, CredentialsError> {
40 let home = home_dir().ok_or(CredentialsError::HomeDirUnavailable)?;
41 let dir = home.join(".lust");
42 Ok(dir.join("credentials"))
43}
44
45pub fn load_credentials() -> Result<Option<Credentials>, CredentialsError> {
46 let path = credentials_file()?;
47 match fs::File::open(&path) {
48 Ok(mut file) => {
49 let mut buf = String::new();
50 file.read_to_string(&mut buf)
51 .map_err(|source| CredentialsError::Io {
52 path: path.clone(),
53 source,
54 })?;
55 let token = buf.trim().to_string();
56 if token.is_empty() {
57 Ok(None)
58 } else {
59 Ok(Some(Credentials::new(token)))
60 }
61 }
62 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
63 Err(source) => Err(CredentialsError::Io { path, source }),
64 }
65}
66
67pub fn save_credentials(token: &str) -> Result<(), CredentialsError> {
68 let path = credentials_file()?;
69 if let Some(parent) = path.parent() {
70 fs::create_dir_all(parent).map_err(|source| CredentialsError::Io {
71 path: parent.to_path_buf(),
72 source,
73 })?;
74 }
75 let mut file = fs::File::create(&path).map_err(|source| CredentialsError::Io {
76 path: path.clone(),
77 source,
78 })?;
79 file.write_all(token.as_bytes())
80 .and_then(|_| file.write_all(b"\n"))
81 .map_err(|source| CredentialsError::Io { path, source })
82}
83
84pub fn clear_credentials() -> Result<(), CredentialsError> {
85 let path = credentials_file()?;
86 match fs::remove_file(&path) {
87 Ok(()) => Ok(()),
88 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
89 Err(source) => Err(CredentialsError::Io { path, source }),
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use std::env;
97 use tempfile::tempdir;
98
99 #[test]
100 fn save_and_load_credentials() {
101 let dir = tempdir().unwrap();
102 let original_home = env::var("HOME").ok();
103 let original_userprofile = env::var("USERPROFILE").ok();
104 env::set_var("HOME", dir.path());
105 env::set_var("USERPROFILE", dir.path());
106
107 save_credentials("secret-token").unwrap();
108 let creds = load_credentials().unwrap().unwrap();
109 assert_eq!(creds.token(), "secret-token");
110
111 let path = credentials_file().unwrap();
112 assert!(path.exists());
113
114 env::remove_var("HOME");
115 env::remove_var("USERPROFILE");
116 if let Some(home) = original_home {
117 env::set_var("HOME", home);
118 }
119 if let Some(userprofile) = original_userprofile {
120 env::set_var("USERPROFILE", userprofile);
121 }
122 }
123}