use chrono::{Duration, Utc};
use crate::auth::{device_code, require_client_id, token_cache};
use crate::cli::{AuthCmd, Runtime};
use crate::config;
use crate::error::{CliError, Result};
pub async fn run(rt: &Runtime, cmd: AuthCmd) -> Result<()> {
match cmd {
AuthCmd::Login => login(rt).await,
AuthCmd::Logout => logout(rt).await,
AuthCmd::Status {
limit,
page,
fields,
} => status(rt, limit, page.as_deref(), &fields).await,
}
}
async fn login(rt: &Runtime) -> Result<()> {
let tenant = rt.cfg.tenant_id.clone().ok_or_else(|| {
CliError::Input(
"no tenant configured; run `sharepoint init` or pass --tenant <domain-or-guid>".into(),
)
})?;
let client_id = require_client_id(&rt.cfg)?;
let scope = device_code::default_scope(rt.cfg.read_only);
let http = reqwest::Client::builder()
.user_agent(format!("sharepoint-cli/{}", env!("CARGO_PKG_VERSION")))
.build()
.expect("reqwest");
let dc =
device_code::request_device_code(&http, &rt.cfg.login_endpoint, &tenant, &client_id, scope)
.await?;
rt.out.print_required_prompt(&format!(
"To sign in, open {}\nand enter code: {}",
dc.verification_uri, dc.user_code
));
let resp = device_code::poll_for_token(
&http,
&rt.cfg.login_endpoint,
&tenant,
&client_id,
&dc.device_code,
dc.interval,
dc.expires_in,
)
.await?;
let claims = device_code::decode_id_token(&resp.id_token)?;
if rt.cfg.tenant_id.as_deref() != Some(claims.tid.as_str()) {
config::write_profile_tenant_id(&rt.config_path, &rt.cfg.profile_name, &claims.tid)?;
}
let key = token_cache::cache_key(&claims.tid, &client_id, &claims.oid);
let entry = token_cache::CacheEntry {
account: token_cache::Account {
username: claims.preferred_username.clone(),
name: Some(claims.name.clone()),
tenant_id: claims.tid.clone(),
oid: claims.oid.clone(),
},
access_token: resp.access_token,
access_token_expires_at: Utc::now() + Duration::seconds(resp.expires_in as i64),
refresh_token: Some(resp.refresh_token),
scopes: resp.scope.split(' ').map(String::from).collect(),
};
token_cache::upsert(&rt.cache_path, &key, entry)?;
rt.out
.print_message(&format!("Signed in as {}", claims.preferred_username));
if rt.out.json {
rt.out.print_json(&serde_json::json!({
"username": claims.preferred_username,
"name": claims.name,
"tenant_id": claims.tid,
}));
}
Ok(())
}
async fn logout(rt: &Runtime) -> Result<()> {
let tenant = rt
.cfg
.tenant_id
.clone()
.ok_or_else(|| CliError::Input("no tenant configured".into()))?;
let client_id = require_client_id(&rt.cfg)?;
let cache = token_cache::load(&rt.cache_path)?;
let prefix = format!("{tenant}:{client_id}:");
let keys: Vec<String> = cache
.entries
.keys()
.filter(|k| k.starts_with(&prefix))
.cloned()
.collect();
let mut removed = 0;
for k in keys {
if token_cache::remove(&rt.cache_path, &k)? {
removed += 1;
}
}
rt.out
.print_message(&format!("Removed {removed} cached account(s)"));
if rt.out.json {
rt.out.print_json(&serde_json::json!({"removed": removed}));
}
Ok(())
}
async fn status(rt: &Runtime, limit: usize, _page: Option<&str>, fields: &[String]) -> Result<()> {
let cache = token_cache::load(&rt.cache_path)?;
let mut all_accounts: Vec<_> = cache
.entries
.iter()
.map(|(key, entry)| {
let mut obj = serde_json::json!({
"key": key,
"username": entry.account.username,
"name": entry.account.name,
"tenant_id": entry.account.tenant_id,
"oid": entry.account.oid,
"expires_at": entry.access_token_expires_at.to_rfc3339(),
"scopes": entry.scopes,
});
if !fields.is_empty()
&& let serde_json::Value::Object(ref mut map) = obj
{
map.retain(|k, _| fields.iter().any(|f| f == k));
}
obj
})
.collect();
let total = all_accounts.len();
all_accounts.truncate(limit);
if rt.out.json {
rt.out.print_json(&serde_json::json!({
"total": total,
"next": serde_json::Value::Null,
"items": all_accounts,
}));
} else {
rt.out
.print_data(&format!("{:30} {}", "ACCOUNT", "EXPIRES"));
for obj in &all_accounts {
let username = obj["username"].as_str().unwrap_or("");
let expires = obj["expires_at"].as_str().unwrap_or("");
rt.out.print_data(&format!("{:30} {}", username, expires));
}
if total == 0 {
rt.out
.print_message("No cached accounts. Run `sharepoint auth login`.");
}
}
Ok(())
}