use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use serde_json::{json, Value};
use super::Shared;
use crate::config::Config;
#[derive(Default, Clone, serde::Serialize)]
pub(super) struct SignIn {
running: bool,
ok: Option<bool>,
message: String,
started: Option<String>,
}
fn profile() -> Option<String> {
std::env::var("AWS_PROFILE").ok().filter(|p| !p.is_empty())
}
pub(super) async fn status(State(app): State<Shared>) -> Json<Value> {
let root = app
.current
.read()
.await
.data
.root
.clone()
.unwrap_or_default();
if !root.starts_with("s3://") {
return Json(json!({ "needed": false }));
}
let (ok, message) = match tokio::task::spawn_blocking(crate::s3::check).await {
Ok(Ok(m)) => (true, m),
Ok(Err(m)) => (false, m),
Err(_) => (false, "credential check failed".to_string()),
};
let signin = !ok && message.contains("aws sso login");
let auto = Config::load().sso_auto_login == Some(true);
if signin && auto {
let recent = app.signin.lock().await.started.as_deref().is_some_and(|t| {
chrono::DateTime::parse_from_rfc3339(t)
.is_ok_and(|t| chrono::Utc::now() - t.to_utc() < chrono::Duration::minutes(10))
});
if !recent {
let _ = login(&app).await;
}
}
let run = app.signin.lock().await.clone();
Json(json!({
"needed": true,
"ok": ok,
"message": message,
"signin": signin,
"profile": profile(),
"auto": auto,
"run": run,
}))
}
pub(super) async fn start(State(app): State<Shared>) -> impl IntoResponse {
match login(&app).await {
Ok(()) => (
StatusCode::OK,
Json(json!({"ok": true, "message": "approve the sign-in in your browser"})),
),
Err(e) => (
StatusCode::BAD_REQUEST,
Json(json!({"ok": false, "message": super::error_text(&e)})),
),
}
}
async fn login(app: &Shared) -> anyhow::Result<()> {
{
let mut s = app.signin.lock().await;
anyhow::ensure!(!s.running, "a sign-in is already waiting for approval");
*s = SignIn {
running: true,
started: Some(chrono::Utc::now().to_rfc3339()),
..Default::default()
};
}
let mut cmd = tokio::process::Command::new("aws");
cmd.args(["sso", "login"]);
if let Some(p) = profile() {
cmd.args(["--profile", &p]);
}
cmd.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped());
let child = match cmd.spawn() {
Ok(c) => c,
Err(e) => {
let mut s = app.signin.lock().await;
s.running = false;
s.ok = Some(false);
s.message = "the AWS CLI (`aws`) is needed to sign in".into();
anyhow::bail!("the AWS CLI (`aws`) is needed to sign in: {e}");
}
};
let app = app.clone();
tokio::spawn(async move {
let out = child.wait_with_output().await;
let signed_in = matches!(&out, Ok(o) if o.status.success());
let session = app.current.read().await.clone();
if signed_in && session.data.problem.is_some() {
if let Some(root) = session.data.root.clone() {
let _ = app.reload(&root).await;
}
}
let mut s = app.signin.lock().await;
s.running = false;
match out {
Ok(o) if o.status.success() => {
s.ok = Some(true);
s.message = "signed in".into();
}
Ok(o) => {
s.ok = Some(false);
let err = String::from_utf8_lossy(&o.stderr);
s.message = err.lines().last().unwrap_or("sign-in failed").to_string();
}
Err(e) => {
s.ok = Some(false);
s.message = e.to_string();
}
}
});
Ok(())
}