sharepoint_cli/commands/
auth.rs1use chrono::{Duration, Utc};
4
5use crate::auth::{DEFAULT_CLIENT_ID, device_code, token_cache};
6use crate::cli::{AuthCmd, Runtime};
7use crate::error::{CliError, Result};
8
9pub async fn run(rt: &Runtime, cmd: AuthCmd) -> Result<()> {
10 match cmd {
11 AuthCmd::Login => login(rt).await,
12 AuthCmd::Logout => logout(rt).await,
13 AuthCmd::Status => status(rt).await,
14 }
15}
16
17async fn login(rt: &Runtime) -> Result<()> {
18 let tenant = rt.cfg.tenant_id.clone().ok_or_else(|| {
21 CliError::Input(
22 "no tenant configured; run `sharepoint init` or pass --tenant <domain-or-guid>".into(),
23 )
24 })?;
25 let client_id = rt
26 .cfg
27 .client_id
28 .clone()
29 .unwrap_or_else(|| DEFAULT_CLIENT_ID.to_string());
30 let scope = device_code::default_scope(rt.cfg.read_only);
31
32 let http = reqwest::Client::builder()
33 .user_agent(format!("sharepoint-cli/{}", env!("CARGO_PKG_VERSION")))
34 .build()
35 .expect("reqwest");
36
37 let dc =
38 device_code::request_device_code(&http, &rt.cfg.login_endpoint, &tenant, &client_id, scope)
39 .await?;
40
41 rt.out.print_message(&format!(
42 "To sign in, open {}\nand enter code: {}",
43 dc.verification_uri, dc.user_code
44 ));
45
46 let resp = device_code::poll_for_token(
47 &http,
48 &rt.cfg.login_endpoint,
49 &tenant,
50 &client_id,
51 &dc.device_code,
52 dc.interval,
53 dc.expires_in,
54 )
55 .await?;
56
57 let claims = device_code::decode_id_token(&resp.id_token)?;
58 let key = token_cache::cache_key(&claims.tid, &client_id, &claims.oid);
59 let entry = token_cache::CacheEntry {
60 account: token_cache::Account {
61 username: claims.preferred_username.clone(),
62 name: Some(claims.name.clone()),
63 tenant_id: claims.tid.clone(),
64 oid: claims.oid.clone(),
65 },
66 access_token: resp.access_token,
67 access_token_expires_at: Utc::now() + Duration::seconds(resp.expires_in as i64),
68 refresh_token: Some(resp.refresh_token),
69 scopes: resp.scope.split(' ').map(String::from).collect(),
70 };
71 token_cache::upsert(&rt.cache_path, &key, entry)?;
72
73 rt.out
74 .print_message(&format!("Signed in as {}", claims.preferred_username));
75 if rt.out.json {
76 rt.out.print_json(&serde_json::json!({
77 "username": claims.preferred_username,
78 "name": claims.name,
79 "tenant_id": claims.tid,
80 }));
81 }
82 Ok(())
83}
84
85async fn logout(rt: &Runtime) -> Result<()> {
86 let tenant = rt
87 .cfg
88 .tenant_id
89 .clone()
90 .ok_or_else(|| CliError::Input("no tenant configured".into()))?;
91 let client_id = rt
92 .cfg
93 .client_id
94 .clone()
95 .unwrap_or_else(|| DEFAULT_CLIENT_ID.to_string());
96 let cache = token_cache::load(&rt.cache_path)?;
97 let prefix = format!("{tenant}:{client_id}:");
98 let keys: Vec<String> = cache
99 .entries
100 .keys()
101 .filter(|k| k.starts_with(&prefix))
102 .cloned()
103 .collect();
104 let mut removed = 0;
105 for k in keys {
106 if token_cache::remove(&rt.cache_path, &k)? {
107 removed += 1;
108 }
109 }
110 rt.out
111 .print_message(&format!("Removed {removed} cached account(s)"));
112 if rt.out.json {
113 rt.out.print_json(&serde_json::json!({"removed": removed}));
114 }
115 Ok(())
116}
117
118async fn status(rt: &Runtime) -> Result<()> {
119 let cache = token_cache::load(&rt.cache_path)?;
120 if rt.out.json {
121 let accounts: Vec<_> = cache
122 .entries
123 .iter()
124 .map(|(key, entry)| {
125 serde_json::json!({
126 "key": key,
127 "username": entry.account.username,
128 "name": entry.account.name,
129 "tenant_id": entry.account.tenant_id,
130 "oid": entry.account.oid,
131 "expires_at": entry.access_token_expires_at.to_rfc3339(),
132 "scopes": entry.scopes,
133 })
134 })
135 .collect();
136 rt.out
137 .print_json(&serde_json::json!({"accounts": accounts}));
138 } else if cache.entries.is_empty() {
139 rt.out
140 .print_message("No cached accounts. Run `sharepoint auth login`.");
141 } else {
142 for entry in cache.entries.values() {
143 rt.out.print_data(&format!(
144 "{:30} expires {}",
145 entry.account.username,
146 entry.access_token_expires_at.to_rfc3339()
147 ));
148 }
149 }
150 Ok(())
151}