Skip to main content

cadmus_core/github/
device_flow.rs

1use crate::device::CURRENT_DEVICE;
2use secrecy::{ExposeSecret, SecretString};
3use std::fs::{self, File};
4use std::io::{Read, Write};
5use std::path::PathBuf;
6
7const TOKEN_FILENAME: &str = ".github_token";
8
9/// Persists a GitHub OAuth token to disk for reuse across app restarts.
10///
11/// Writes to `<install-dir>/.github_token` with `0600` permissions.
12///
13/// # Errors
14///
15/// Returns an error string if directory creation or file write fails.
16pub fn save_token(token: &SecretString) -> Result<(), String> {
17    let path = token_path();
18    tracing::debug!(path = %path.display(), "Saving GitHub token");
19
20    if let Some(parent) = path.parent() {
21        fs::create_dir_all(parent).map_err(|e| format!("Failed to create token dir: {}", e))?;
22    }
23
24    let mut file =
25        File::create(&path).map_err(|e| format!("Failed to create token file: {}", e))?;
26    file.write_all(token.expose_secret().as_bytes())
27        .map_err(|e| format!("Failed to write token: {}", e))?;
28
29    #[cfg(unix)]
30    {
31        use std::os::unix::fs::PermissionsExt;
32        fs::set_permissions(&path, fs::Permissions::from_mode(0o600))
33            .map_err(|e| format!("Failed to set token file permissions: {}", e))?;
34    }
35
36    tracing::info!("GitHub token saved");
37    Ok(())
38}
39
40/// Loads a previously saved GitHub OAuth token from disk.
41///
42/// Returns `None` if no token file exists (first-time setup).
43///
44/// # Errors
45///
46/// Returns an error string if the file exists but cannot be read.
47pub fn load_token() -> Result<Option<SecretString>, String> {
48    let path = token_path();
49    tracing::debug!(path = %path.display(), "Loading GitHub token");
50
51    if !path.exists() {
52        tracing::debug!("No saved token found");
53        return Ok(None);
54    }
55
56    let mut contents = String::new();
57    File::open(&path)
58        .map_err(|e| format!("Failed to open token file: {}", e))?
59        .read_to_string(&mut contents)
60        .map_err(|e| format!("Failed to read token file: {}", e))?;
61
62    let token = contents.trim().to_owned();
63    if token.is_empty() {
64        tracing::warn!("Token file exists but is empty");
65        return Ok(None);
66    }
67
68    tracing::info!("GitHub token loaded from disk");
69    Ok(Some(SecretString::from(token)))
70}
71
72/// Deletes the saved GitHub OAuth token from disk.
73///
74/// Called when a token is found to be invalid or revoked, so the next
75/// authentication attempt starts fresh via device flow.
76///
77/// Returns `Ok(())` if the file was deleted or did not exist.
78///
79/// # Errors
80///
81/// Returns an error string if the file exists but cannot be removed.
82pub fn delete_token() -> Result<(), String> {
83    let path = token_path();
84    tracing::debug!(path = %path.display(), "Deleting GitHub token");
85
86    if !path.exists() {
87        return Ok(());
88    }
89
90    fs::remove_file(&path).map_err(|e| format!("Failed to delete token file: {}", e))?;
91    tracing::info!("GitHub token deleted");
92    Ok(())
93}
94
95fn token_path() -> PathBuf {
96    CURRENT_DEVICE.install_path(TOKEN_FILENAME)
97}