APIs for links and references in the ATmosphere

wip: better pages and errors

+38 -66
+1 -1
who-am-i/readme.md
··· 43 43 44 44 it's still nice to have an explicit opt-in on a per-demo basis for microcosm so it will be used for that. it's allow-listed for the microcosm domain however (so not deployed on any adversarial hosting pages), so it's simultaenously overkill and restrictive. 45 45 46 - i will get back to oauth eventually and hopefully roll out a microcosm service to make it easy for clients, but there are a few more things in the pipeline to get to first. 46 + i will get back to oauth eventually and hopefully roll out a microcosm service to make it easy for clients (and demos), but there are a few more things in the pipeline to get to first.
+37 -12
who-am-i/src/server.rs
··· 4 4 extract::{FromRef, Query, State}, 5 5 http::{ 6 6 StatusCode, 7 - header::{HeaderMap, REFERER}, 7 + header::{CONTENT_TYPE, HeaderMap, REFERER}, 8 8 }, 9 - response::{Html, IntoResponse, Json, Redirect, Response}, 9 + response::{IntoResponse, Json, Redirect, Response}, 10 10 routing::get, 11 11 }; 12 12 use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar}; ··· 25 25 use crate::{ExpiringTaskMap, OAuth, OAuthCallbackParams, OAuthCompleteError, ResolveHandleError}; 26 26 27 27 const FAVICON: &[u8] = include_bytes!("../static/favicon.ico"); 28 - const INDEX_HTML: &str = include_str!("../static/index.html"); 28 + const STYLE_CSS: &str = include_str!("../static/style.css"); 29 29 30 30 const DID_COOKIE_KEY: &str = "did"; 31 31 32 32 type AppEngine = Engine<Handlebars<'static>>; 33 + type Rendered = RenderHtml<&'static str, AppEngine, Value>; 33 34 34 35 #[derive(Clone)] 35 36 struct AppState { ··· 76 77 }; 77 78 78 79 let app = Router::new() 79 - .route("/", get(|| async { Html(INDEX_HTML) })) 80 - .route("/favicon.ico", get(|| async { FAVICON })) // todo MIME 80 + .route("/", get(hello)) 81 + .route("/favicon.ico", get(favicon)) // todo MIME 82 + .route("/style.css", get(css)) 81 83 .route("/prompt", get(prompt)) 82 84 .route("/user-info", get(user_info)) 83 85 .route("/auth", get(start_oauth)) ··· 94 96 .unwrap(); 95 97 } 96 98 99 + async fn hello(State(AppState { engine, .. }): State<AppState>) -> Rendered { 100 + RenderHtml("hello", engine, json!({})) 101 + } 102 + 103 + async fn css() -> impl IntoResponse { 104 + let headers = [ 105 + (CONTENT_TYPE, "text/css"), 106 + // (CACHE_CONTROL, "") // TODO 107 + ]; 108 + (headers, STYLE_CSS) 109 + } 110 + 111 + async fn favicon() -> impl IntoResponse { 112 + ([(CONTENT_TYPE, "image/x-icon")], FAVICON) 113 + } 114 + 97 115 async fn prompt( 98 116 State(AppState { 99 117 allowed_hosts, ··· 106 124 jar: SignedCookieJar, 107 125 headers: HeaderMap, 108 126 ) -> impl IntoResponse { 127 + let err = |reason, check_frame| { 128 + let info = json!({ 129 + "reason": reason, 130 + "check_frame": check_frame, 131 + }); 132 + RenderHtml("prompt-error", engine.clone(), info).into_response() 133 + }; 134 + 109 135 let Some(referrer) = headers.get(REFERER) else { 110 - return Html::<&'static str>("missing referrer, sorry").into_response(); 136 + return err("Missing referer", true); 111 137 }; 112 138 let Ok(referrer) = referrer.to_str() else { 113 - return "referer contained opaque bytes".into_response(); 139 + return err("Unreadable referer", true); 114 140 }; 115 141 let Ok(url) = Url::parse(referrer) else { 116 - return "referrer was not a url".into_response(); 142 + return err("Bad referer", true); 117 143 }; 118 144 let Some(parent_host) = url.host_str() else { 119 - return "could nto get host from url".into_response(); 145 + return err("Referer missing host", true); 120 146 }; 121 147 if !allowed_hosts.contains(parent_host) { 122 - return format!("host {parent_host:?} not in allowed_hosts, disallowing for now") 123 - .into_response(); 148 + return err("Login is not allowed on this page", false); 124 149 } 125 150 if let Some(did) = jar.get(DID_COOKIE_KEY) { 126 151 let Ok(did) = Did::new(did.value_trimmed().to_string()) else { 127 - return "did from cookie failed to parse".into_response(); 152 + return err("Bad cookie", false); 128 153 }; 129 154 130 155 let fetch_key = resolve_handles.dispatch(
-53
who-am-i/static/index.html
··· 1 - <!doctype html> 2 - <html lang="en"> 3 - <head> 4 - <meta charset="utf-8" /> 5 - <title>Who-am-i documentation</title> 6 - <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 - <meta name="description" content="API Documentation who-am-i, a read-only atproto identity verifier" /> 8 - <style> 9 - .custom-header { 10 - height: 42px; 11 - background-color: #221828; 12 - box-shadow: inset 0 -1px 0 var(--scalar-border-color); 13 - color: var(--scalar-color-1); 14 - font-size: var(--scalar-font-size-3); 15 - font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif; 16 - padding: 0 18px; 17 - justify-content: space-between; 18 - } 19 - .custom-header, 20 - .custom-header nav { 21 - display: flex; 22 - align-items: center; 23 - gap: 18px; 24 - } 25 - .custom-header a:hover { 26 - color: var(--scalar-color-2); 27 - } 28 - </style> 29 - </head> 30 - <body> 31 - <header class="custom-header scalar-app"> 32 - <p> 33 - <a href="https://ufos.microcosm.blue">Launch who-am-i [todo]</a> 34 - </p> 35 - <nav> 36 - <b>a <a href="https://microcosm.blue">microcosm</a> project</b> 37 - <a href="https://bsky.app/profile/microcosm.blue">@microcosm.blue</a> 38 - <a href="https://github.com/at-microcosm">github</a> 39 - </nav> 40 - </header> 41 - 42 - <script id="api-reference" type="application/json" data-url="/openapi"></script> 43 - 44 - <script> 45 - var configuration = { 46 - theme: 'purple', 47 - } 48 - document.getElementById('api-reference').dataset.configuration = JSON.stringify(configuration) 49 - </script> 50 - 51 - <script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script> 52 - </body> 53 - </html>