Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm
75
fork

Configure Feed

Select the types of activity you want to include in your feed.

show current session at hello and allow revoking

do we need csrf here or...

phil cbd1e7fb 2e5e17ad

+107 -11
+34 -7
who-am-i/src/server.rs
··· 7 7 header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER, X_FRAME_OPTIONS}, 8 8 }, 9 9 response::{IntoResponse, Json, Redirect, Response}, 10 - routing::get, 10 + routing::{get, post}, 11 11 }; 12 12 use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar}; 13 13 use axum_template::{RenderHtml, engine::Engine}; ··· 85 85 .route("/user-info", get(user_info)) 86 86 .route("/auth", get(start_oauth)) 87 87 .route("/authorized", get(complete_oauth)) 88 + .route("/disconnect", post(disconnect)) 88 89 .with_state(state); 89 90 90 91 let listener = TcpListener::bind("0.0.0.0:9997") ··· 98 99 } 99 100 100 101 async fn hello( 101 - State(AppState { engine, .. }): State<AppState>, 102 + State(AppState { 103 + engine, 104 + resolve_handles, 105 + shutdown, 106 + oauth, 107 + .. 108 + }): State<AppState>, 102 109 mut jar: SignedCookieJar, 103 110 ) -> Response { 104 - // push expiry (or clean up) the current cookie 105 - if let Some(did) = jar.get(DID_COOKIE_KEY) { 111 + let info = if let Some(did) = jar.get(DID_COOKIE_KEY) { 106 112 if let Ok(did) = Did::new(did.value_trimmed().to_string()) { 113 + // push cookie expiry 107 114 jar = jar.add(cookie(&did)); 115 + let fetch_key = resolve_handles.dispatch( 116 + { 117 + let oauth = oauth.clone(); 118 + let did = did.clone(); 119 + async move { oauth.resolve_handle(did.clone()).await } 120 + }, 121 + shutdown.child_token(), 122 + ); 123 + json!({ 124 + "did": did, 125 + "fetch_key": fetch_key, 126 + }) 108 127 } else { 109 128 jar = jar.remove(DID_COOKIE_KEY); 129 + json!({}) 110 130 } 111 - } 131 + } else { 132 + json!({}) 133 + }; 112 134 let frame_headers = [ 113 135 (X_FRAME_OPTIONS, "deny"), 114 136 (CONTENT_SECURITY_POLICY, "frame-ancestors 'none'"), 115 137 ]; 116 - (frame_headers, jar, RenderHtml("hello", engine, json!({}))).into_response() 138 + (frame_headers, jar, RenderHtml("hello", engine, info)).into_response() 117 139 } 118 140 119 141 async fn css() -> impl IntoResponse { ··· 179 201 (X_FRAME_OPTIONS, format!("allow-from {parent_origin}")), 180 202 ( 181 203 CONTENT_SECURITY_POLICY, 182 - format!("frame-ancestors {parent_host}"), 204 + format!("frame-ancestors {parent_origin}"), 183 205 ), 184 206 ]; 185 207 ··· 362 384 }); 363 385 (jar, RenderHtml("authorized", engine, info)).into_response() 364 386 } 387 + 388 + async fn disconnect(jar: SignedCookieJar) -> impl IntoResponse { 389 + let jar = jar.remove(DID_COOKIE_KEY); 390 + (jar, Json(json!({ "ok": true }))) 391 + }
+5 -1
who-am-i/static/style.css
··· 136 136 } 137 137 138 138 #connect, 139 - #allow { 139 + #allow, 140 + #revoke { 140 141 background: transparent; 141 142 border: none; 142 143 border-left: 1px solid #bbb; ··· 144 145 color: #375; 145 146 font: inherit; 146 147 cursor: pointer; 148 + } 149 + #revoke { 150 + color: #a31; 147 151 } 148 152 #action:hover #allow { 149 153 color: #285;
+68 -3
who-am-i/templates/hello.hbs
··· 1 1 {{#*inline "description"}}A little identity-verifying auth service for microcosm demos{{/inline}} 2 2 3 3 {{#*inline "main"}} 4 - <div class="mini-content"> 5 - This is a little identity-verifying service for microcosm demos. 6 - </div> 4 + <div class="mini-content"> 5 + This is a little identity-verifying service for microcosm demos. 6 + 7 + {{#if did}} 8 + <p id="error-message" class="hidden"></p> 9 + 10 + <p id="prompt" class="detail"> 11 + Connected identity: 12 + </p> 13 + 14 + <div id="loader"> 15 + <span class="spinner"></span> 16 + </div> 17 + 18 + <div id="user-info"> 19 + <div id="handle-action" class="action"> 20 + <span id="handle-view" class="handle"></span> 21 + <button id="revoke">disconnect</button> 22 + </div> 23 + </div> 24 + <script> 25 + const errorEl = document.getElementById('error-message'); 26 + const loaderEl = document.getElementById('loader'); 27 + const handleViewEl = document.getElementById('handle-view'); 28 + const revokeEl = document.getElementById('revoke'); // for known-did 29 + 30 + function err(e, msg) { 31 + loaderEl.classList.add('hidden'); 32 + errorEl.classList.remove('hidden'); 33 + errorEl.textContent = msg || e; 34 + throw new Error(e); 35 + } 36 + 37 + // already-known user 38 + ({{{json did}}}) && (async () => { 39 + 40 + const handle = await lookUp({{{json fetch_key}}}); 41 + console.log('got handle', handle); 42 + 43 + loaderEl.classList.add('hidden'); 44 + handleViewEl.textContent = `@${handle}`; 45 + revokeEl.addEventListener('click', async () => { 46 + try { 47 + let res = await fetch('/disconnect', { method: 'POST', credentials: 'include' }); 48 + if (!res.ok) throw res; 49 + } catch (e) { 50 + err(e, 'failed to clear session, sorry'); 51 + } 52 + window.location.reload(); 53 + }); 54 + })(); 55 + 56 + async function lookUp(fetch_key) { 57 + const user_info = new URL('/user-info', window.location); 58 + user_info.searchParams.set('fetch-key', fetch_key); 59 + let info; 60 + try { 61 + const resp = await fetch(user_info); 62 + if (!resp.ok) throw resp; 63 + info = await resp.json(); 64 + } catch (e) { 65 + err(e, 'failed to resolve handle from DID') 66 + } 67 + return info.handle; 68 + } 69 + </script> 70 + {{/if}} 71 + </div> 7 72 {{/inline}} 8 73 9 74 {{#> base-full}}{{/base-full}}