bear_cli/cloudkit/
auth.rs1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::{Context, Result, anyhow};
5use serde::{Deserialize, Serialize};
6
7use crate::config::app_support_dir;
8
9const KEYCHAIN_SERVICE: &str = "bear-cli";
10const KEYCHAIN_ACCOUNT: &str = "ckWebAuthToken";
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct AuthConfig {
14 pub ck_web_auth_token: String,
15}
16
17impl AuthConfig {
18 pub fn load() -> Result<Self> {
20 if let Ok(token) = keychain_get() {
21 return Ok(Self {
22 ck_web_auth_token: token,
23 });
24 }
25 Self::load_from_file()
26 }
27
28 pub fn save(&self) -> Result<()> {
30 let _ = keychain_set(&self.ck_web_auth_token); self.save_to_file()
32 }
33
34 fn config_path() -> Result<PathBuf> {
35 Ok(app_support_dir()?.join("auth.json"))
36 }
37
38 fn load_from_file() -> Result<Self> {
39 let path = Self::config_path()?;
40 let contents = fs::read_to_string(&path).with_context(|| {
41 format!(
42 "auth token not found — run `bear auth <token>` first (checked {})",
43 path.display()
44 )
45 })?;
46 serde_json::from_str(&contents).context("invalid auth config")
47 }
48
49 fn save_to_file(&self) -> Result<()> {
50 let path = Self::config_path()?;
51 fs::create_dir_all(path.parent().unwrap())?;
52 let json = serde_json::to_string_pretty(self)?;
53 let tmp = path.with_extension("tmp");
55 fs::write(&tmp, json)?;
56 fs::rename(&tmp, &path)?;
57 Ok(())
58 }
59}
60
61fn keychain_get() -> Result<String> {
62 let output = std::process::Command::new("security")
63 .args([
64 "find-generic-password",
65 "-s",
66 KEYCHAIN_SERVICE,
67 "-a",
68 KEYCHAIN_ACCOUNT,
69 "-w",
70 ])
71 .output()?;
72 if !output.status.success() {
73 return Err(anyhow!("keychain lookup failed"));
74 }
75 Ok(String::from_utf8(output.stdout)?.trim().to_string())
76}
77
78fn keychain_set(token: &str) -> Result<()> {
79 let _ = std::process::Command::new("security")
81 .args([
82 "delete-generic-password",
83 "-s",
84 KEYCHAIN_SERVICE,
85 "-a",
86 KEYCHAIN_ACCOUNT,
87 ])
88 .output();
89
90 let status = std::process::Command::new("security")
91 .args([
92 "add-generic-password",
93 "-s",
94 KEYCHAIN_SERVICE,
95 "-a",
96 KEYCHAIN_ACCOUNT,
97 "-w",
98 token,
99 ])
100 .status()?;
101 if !status.success() {
102 return Err(anyhow!("failed to write token to Keychain"));
103 }
104 Ok(())
105}