use std::cell::RefCell;
use std::rc::Rc;
use re_async::AsyncRuntimeHandle;
use re_auth::oauth::Credentials;
use re_auth::oauth::api::{AuthenticateWithCode, Pkce, authorization_url, send_async};
use re_log::ResultExt as _;
use re_ui::UiExt as _;
use uuid::Uuid;
use wasm_bindgen::JsCast as _;
use wasm_bindgen::prelude::Closure;
type StorageEventCallback = dyn FnMut(web_sys::StorageEvent);
pub struct State {
child_window: Option<web_sys::Window>,
on_storage_event: Option<Closure<StorageEventCallback>>,
signed_in_url: String,
pkce: Rc<Pkce>,
state: String,
result: Rc<RefCell<Option<Result<Credentials, String>>>>,
}
impl Drop for State {
fn drop(&mut self) {
re_log::debug!("dropping auth state");
if let Some(child_window) = &self.child_window {
child_window.close().ok();
}
if let Some(window) = web_sys::window()
&& let Some(on_storage_event) = &self.on_storage_event
{
window
.remove_event_listener_with_callback(
"storage",
on_storage_event.as_ref().unchecked_ref(),
)
.ok();
}
}
}
#[derive(Debug, serde::Deserialize)]
struct AuthEventPayload {
#[serde(rename = "type")]
type_: String,
url: String,
}
impl State {
pub fn start(&mut self) -> Result<(), String> {
if self.child_window.is_some() {
return Ok(());
}
let parent_window = web_sys::window().expect("no window available");
let login_url = authorization_url(&self.signed_in_url, &self.state, &self.pkce);
let Some(child_window) = parent_window
.open_with_url_and_target_and_features(&login_url, "auth", "width=480,height=640")
.map_err(|err| re_web::Error::from(err).to_string())?
else {
return Err("window.open did not return a handle".into());
};
self.child_window = Some(child_window);
Ok(())
}
#[expect(clippy::unused_self)] pub fn ui(&self, ui: &mut egui::Ui) {
ui.horizontal(|ui| {
ui.loading_indicator("Waiting for web login");
ui.label("Waiting for login…");
});
}
#[expect(clippy::needless_pass_by_ref_mut)] pub fn done(&mut self) -> Result<Option<Credentials>, String> {
if let Some(credentials) = self.result.borrow_mut().take() {
return credentials.map(Some);
}
if let Some(child_window) = &self.child_window {
if child_window
.closed()
.map_err(|err| re_web::Error::from(err).to_string())
.ok_or_log_error()
.unwrap_or_default()
{
return Err("Login popup was closed before completing authentication".into());
}
}
Ok(None)
}
#[expect(clippy::unnecessary_wraps)]
pub fn open(egui_ctx: &egui::Context, signed_in_url: String) -> Result<Self, String> {
let parent_window = web_sys::window().expect("no window available");
let pkce = Rc::new(Pkce::new());
let state = Uuid::new_v4().to_string();
let result = Rc::new(RefCell::new(None));
let on_storage_event = Closure::wrap(Box::new({
let result = Rc::clone(&result);
let pkce = pkce.clone();
let stored_state = state.clone();
let egui_ctx = egui_ctx.clone();
move |e: web_sys::StorageEvent| {
AsyncRuntimeHandle::new_web().spawn_future(try_handle_storage_event(
e,
pkce.clone(),
stored_state.clone(),
result.clone(),
egui_ctx.clone(),
));
}
}) as Box<StorageEventCallback>);
parent_window
.add_event_listener_with_callback("storage", on_storage_event.as_ref().unchecked_ref())
.ok();
Ok(Self {
child_window: None,
on_storage_event: Some(on_storage_event),
signed_in_url,
pkce,
state,
result,
})
}
}
async fn try_handle_storage_event(
e: web_sys::StorageEvent,
pkce: Rc<Pkce>,
stored_state: String,
result: Rc<RefCell<Option<Result<Credentials, String>>>>,
egui_ctx: egui::Context,
) {
macro_rules! bail {
($err:expr) => {{
let err = $err.to_string();
re_log::error!("{err}");
*result.borrow_mut() = Some(Err(err));
return;
}};
}
if e.key().as_deref() != Some("_auth") {
return;
}
let Some(new_value) = e.new_value() else {
bail!("auth storage event without new value");
};
let payload = match serde_json::from_str::<AuthEventPayload>(&new_value) {
Ok(payload) => payload,
Err(err) => {
bail!(err);
}
};
if payload.type_ != "auth" {
re_log::error!("storage event payload.type != auth");
return;
}
let Some(url) = url::Url::parse(&payload.url).ok_or_log_error() else {
return;
};
let Some(code) = url.query_pairs().find(|(k, _)| k == "code").map(|(_, v)| v) else {
bail!("missing code in url");
};
let Some(state) = url
.query_pairs()
.find(|(k, _)| k == "state")
.map(|(_, v)| v)
else {
bail!("missing state in url");
};
if state != stored_state {
bail!("invalid state");
}
let res = match send_async(AuthenticateWithCode::new(&code, &pkce)).await {
Ok(res) => res,
Err(err) => {
bail!(err);
}
};
let credentials = match re_auth::oauth::Credentials::from_auth_response(res.into()) {
Ok(v) => v,
Err(err) => {
bail!(err);
}
};
let credentials = match credentials.ensure_stored() {
Ok(v) => v,
Err(err) => {
bail!(err);
}
};
*result.borrow_mut() = Some(Ok(credentials));
egui_ctx.request_repaint();
}