use super::{Cua, run};
use crate::{CuaError, Result};
use std::sync::Arc;
#[uniffi::export]
pub fn may_have_session() -> bool {
cua_auth::may_have_session()
}
impl From<cua_auth::Error> for CuaError {
fn from(e: cua_auth::Error) -> Self {
use cua_auth::Error as E;
let m = e.to_string();
match e {
E::NotLoggedIn | E::Unauthenticated(_) => CuaError::Unauthenticated(m),
E::Http(_) => CuaError::Http(m),
E::Timeout(_) => CuaError::Timeout(m),
E::Unsupported(_) => CuaError::Unsupported(m),
E::InvalidArgument(_) => CuaError::InvalidArgument(m),
E::Store(_) => CuaError::Internal(m),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, uniffi::Record)]
pub struct AuthIdentity {
pub username: Option<String>,
pub email: Option<String>,
pub name: Option<String>,
pub subject: Option<String>,
pub display: Option<String>,
}
impl From<cua_auth::Identity> for AuthIdentity {
fn from(i: cua_auth::Identity) -> Self {
AuthIdentity {
display: i.display(),
username: i.username,
email: i.email,
name: i.name,
subject: i.subject,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, uniffi::Record)]
pub struct AuthStatus {
pub logged_in: bool,
pub identity: Option<AuthIdentity>,
pub expires_at: Option<String>,
pub store: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
pub enum LoginMethod {
Browser,
Device,
}
#[derive(uniffi::Object)]
pub struct LoginAttempt {
method: LoginMethod,
url: String,
user_code: Option<String>,
note: Option<String>,
session: Arc<cua_auth::Session>,
pending: tokio::sync::Mutex<Option<cua_auth::PendingLogin>>,
}
#[uniffi::export]
impl LoginAttempt {
pub fn method(&self) -> LoginMethod {
self.method
}
pub fn url(&self) -> String {
self.url.clone()
}
pub fn user_code(&self) -> Option<String> {
self.user_code.clone()
}
pub fn note(&self) -> Option<String> {
self.note.clone()
}
pub async fn wait(self: Arc<Self>) -> Result<AuthIdentity> {
run(async move {
let pending = self
.pending
.lock()
.await
.take()
.ok_or_else(|| CuaError::Closed("this sign-in already finished".into()))?;
let creds = pending.complete().await?;
let who = self.session.install(creds).await?;
enroll_after_sign_in(&self.session).await;
#[cfg(feature = "host")]
super::telemetry::sync_account(&self.session).await;
Ok(who.into())
})
.await
}
}
#[cfg(feature = "host")]
async fn enroll_after_sign_in(session: &Arc<cua_auth::Session>) {
let home = cua_daemon::cua_home();
let Ok(auth) = cua_host::DeviceAuth::new(
&cua_host::relay_url_for(None, &home),
Arc::new(super::host::SessionTokens(session.clone())),
Arc::new(session.store().clone()),
cua_host::device_name(),
) else {
return;
};
let auth = auth.with_pending_file(cua_host::device_pending_file(&home));
let r = tokio::time::timeout(
std::time::Duration::from_secs(15),
auth.enroll_after_sign_in(),
)
.await;
record_sign_in_enrollment(&r);
}
#[cfg(feature = "host")]
pub(crate) fn record_sign_in_enrollment<E>(
r: &std::result::Result<std::result::Result<cua_host::relay::Enrollment, cua_host::Error>, E>,
) {
use cua_telemetry::Outcome;
let (method, outcome) = match r {
Ok(Ok(e)) if e.device.state == cua_host::relay::DeviceState::Enrolled => (
if e.superseded.is_empty() {
"sign_in"
} else {
"rekey"
},
Outcome::Ok,
),
Ok(Ok(_)) => return,
_ => ("sign_in", Outcome::Error),
};
if let Some(e) = cua_telemetry::events::device_enroll(method, outcome) {
cua_telemetry::capture(e);
}
}
#[cfg(not(feature = "host"))]
async fn enroll_after_sign_in(_session: &Arc<cua_auth::Session>) {}
#[derive(uniffi::Object)]
pub struct Auth {
session: Arc<cua_auth::Session>,
}
impl Auth {
pub fn with_session(session: cua_auth::Session) -> Arc<Self> {
let session = Arc::new(session);
session.spawn_keep_alive();
Arc::new(Auth { session })
}
pub fn session(&self) -> Arc<cua_auth::Session> {
self.session.clone()
}
}
#[uniffi::export]
impl Cua {
pub fn auth(&self) -> Arc<Auth> {
Auth::with_session(cua_auth::Session::from_env())
}
}
#[uniffi::export]
impl Auth {
pub fn status(&self) -> Result<AuthStatus> {
let c = self.session.credentials()?;
Ok(AuthStatus {
logged_in: c.is_some(),
identity: c.as_ref().map(|c| c.identity().into()),
expires_at: c.map(|c| c.expires_at),
store: self.session.store().describe(),
})
}
pub async fn begin_login(&self, flow: Option<String>) -> Result<Arc<LoginAttempt>> {
let session = self.session.clone();
run(async move {
let flow = cua_auth::Flow::parse(flow.as_deref().unwrap_or("auto"))?;
let p = session.begin_login(flow).await?;
Ok(Arc::new(LoginAttempt {
method: match p.method {
cua_auth::Method::Browser => LoginMethod::Browser,
cua_auth::Method::Device => LoginMethod::Device,
},
url: p.url.clone(),
user_code: p.user_code.clone(),
note: p.note.clone(),
session,
pending: tokio::sync::Mutex::new(Some(p)),
}))
})
.await
}
pub async fn access_token(&self, force: bool) -> Result<String> {
let session = self.session.clone();
run(async move { Ok(session.access_token(force).await?) }).await
}
pub async fn logout(&self) -> Result<bool> {
let session = self.session.clone();
let existed = run(async move { Ok(session.logout().await?.0) }).await?;
let _ = cua_telemetry::global().forget_account();
Ok(existed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn status_and_errors_map_without_touching_the_keychain() {
let d = tempfile::tempdir().unwrap();
let store = cua_auth::Store::File(d.path().join("credentials.json"));
let a = Auth::with_session(cua_auth::Session::new(
cua_auth::Oidc::new("http://127.0.0.1:9", "cua-cli"),
store.clone(),
));
let s = a.status().unwrap();
assert!(!s.logged_in && s.identity.is_none());
assert!(s.store.contains("credentials.json"));
assert!(matches!(
a.access_token(false).await.unwrap_err(),
CuaError::Unauthenticated(_)
));
assert!(matches!(
a.begin_login(Some("bogus".into())).await.err().unwrap(),
CuaError::InvalidArgument(_)
));
assert!(matches!(
a.begin_login(None).await.err().unwrap(),
CuaError::Http(_)
));
store
.save(&cua_auth::Credentials::from_token_response(&serde_json::json!({"access_token": "a.eyJlbWFpbCI6ImFAYi5jIn0.s", "expires_in": 3600})).unwrap())
.unwrap();
let s = a.status().unwrap();
assert!(s.logged_in);
assert_eq!(s.identity.unwrap().display.as_deref(), Some("a@b.c"));
assert_eq!(
a.access_token(false).await.unwrap(),
"a.eyJlbWFpbCI6ImFAYi5jIn0.s"
);
assert!(a.logout().await.unwrap());
assert!(!a.status().unwrap().logged_in);
}
}