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