use std::time::SystemTime; use axum::{ body::Body, extract::{Path, State}, http::{HeaderMap, StatusCode, Uri}, response::{IntoResponse, Response}, routing::get, Router, }; use axum_extra::extract::CookieJar; use maud::html; use tap::Pipe; use crate::server::{store::Store, Handoffs, Nevermind}; use super::{account_auth, render_html, ApiState}; pub fn bind(app: Router) -> Router { app.route("/", get(get_panel)) .route("/scopes/:token", get(get_scopes)) .route("/handoff", get(get_handoff)) } #[axum::debug_handler(state = ApiState)] async fn get_panel(jar: CookieJar, State(store): State) -> Result { Ok(account_auth(&jar, &store) .await .prompt_login()? .pipe(render_normal_panel)) } fn render_normal_panel(name: String) -> Response { render_html( html!(title { "dissociate" }), html! { p { "currently logged in as " (name) } a href="/logout" { button { "log out" } } }, ) } #[axum::debug_handler(state = ApiState)] async fn get_scopes( Path(token): Path, State(store): State, ) -> Result { let error_response = || { Response::builder() .status(StatusCode::BAD_REQUEST) .body(Body::empty()) .unwrap() }; let mut parts = Vec::new(); let (name, expires) = store.check_token(&token).await.ok_or_else(error_response)?; parts.push( expires .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs() .to_string(), ); { let account = store.get_account(&name).await.ok_or_else(error_response)?; parts.extend(account.scopes.iter().cloned()); }; let body = parts.join("\n"); Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", "text/plain") .body(body.into()) .unwrap()) } #[axum::debug_handler(state = ApiState)] async fn get_handoff( jar: CookieJar, State(Handoffs(handoffs)): State, State(store): State, headers: HeaderMap, ) -> Response { let Some(origin_header) = headers.get("Origin") else { return (StatusCode::BAD_REQUEST, "Missing Origin header").into_response(); }; let Some(origin) = origin_header .to_str() .ok() .and_then(|origin| origin.parse::().ok()) .and_then(|origin| origin.host().map(ToString::to_string)) else { return (StatusCode::BAD_REQUEST, "Malformed Origin header").into_response(); }; if !handoffs.contains(&origin) { return (StatusCode::FORBIDDEN, "Origin not registered for handoff").into_response(); } let builder = Response::builder() .header("Access-Control-Allow-Credentials", "true") .header("Access-Control-Allow-Origin", origin_header); if let Some(token) = jar.get("dissociate-token") { if let Some((_, expires)) = store.check_token(token.value()).await { let expires_in = expires .duration_since(SystemTime::now()) .unwrap_or_default() .as_secs(); return builder .status(StatusCode::OK) .body(Body::from(format!( r#"{{"token":"{}","expiresIn":{}}}"#, token.value().to_string(), expires_in, ))) .unwrap(); } } builder .status(StatusCode::UNAUTHORIZED) .body(Body::empty()) .unwrap() }