APIs for links and references in the ATmosphere

wip, prompting flow with cookie

+70 -11
+18
Cargo.lock
··· 469 469 "axum", 470 470 "axum-core", 471 471 "bytes", 472 + "cookie", 472 473 "futures-util", 473 474 "headers", 474 475 "http", ··· 918 919 "tower-http", 919 920 "tungstenite 0.26.2", 920 921 "zstd", 922 + ] 923 + 924 + [[package]] 925 + name = "cookie" 926 + version = "0.18.1" 927 + source = "registry+https://github.com/rust-lang/crates.io-index" 928 + checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" 929 + dependencies = [ 930 + "base64 0.22.1", 931 + "hmac", 932 + "percent-encoding", 933 + "rand 0.8.5", 934 + "sha2", 935 + "subtle", 936 + "time", 937 + "version_check", 921 938 ] 922 939 923 940 [[package]] ··· 4851 4868 "atrium-identity", 4852 4869 "atrium-oauth", 4853 4870 "axum", 4871 + "axum-extra", 4854 4872 "clap", 4855 4873 "hickory-resolver", 4856 4874 "metrics",
+1
who-am-i/Cargo.toml
··· 8 8 atrium-identity = "0.1.5" 9 9 atrium-oauth = "0.1.3" 10 10 axum = "0.8.4" 11 + axum-extra = { version = "0.10.1", features = ["cookie-signed", "typed-header"] } 11 12 clap = { version = "4.5.40", features = ["derive"] } 12 13 hickory-resolver = "0.25.2" 13 14 metrics = "0.24.2"
+34 -11
who-am-i/src/server.rs
··· 2 2 use atrium_oauth::CallbackParams; 3 3 use axum::{ 4 4 Router, 5 - extract::{Query, State}, 5 + extract::{FromRef, Query, State}, 6 6 response::{Html, Redirect}, 7 7 routing::get, 8 8 }; 9 + use axum_extra::extract::cookie::{Cookie, Key, SignedCookieJar}; 9 10 10 11 use serde::Deserialize; 11 12 use std::sync::Arc; ··· 14 15 15 16 use crate::{Client, authorize, client}; 16 17 18 + const FAVICON: &[u8] = include_bytes!("../static/favicon.ico"); 17 19 const INDEX_HTML: &str = include_str!("../static/index.html"); 18 - const FAVICON: &[u8] = include_bytes!("../static/favicon.ico"); 20 + const LOGIN_HTML: &str = include_str!("../static/login.html"); 19 21 20 22 pub async fn serve(shutdown: CancellationToken) { 21 23 let state = AppState { 24 + key: Key::generate(), // TODO: via config 22 25 client: Arc::new(client()), 23 26 }; 24 27 25 28 let app = Router::new() 26 29 .route("/", get(|| async { Html(INDEX_HTML) })) 27 30 .route("/favicon.ico", get(|| async { FAVICON })) // todo MIME 31 + .route("/prompt", get(prompt)) 28 32 .route("/auth", get(start_oauth)) 29 33 .route("/authorized", get(complete_oauth)) 30 34 .with_state(state); ··· 41 45 42 46 #[derive(Clone)] 43 47 struct AppState { 48 + pub key: Key, 44 49 pub client: Arc<Client>, 45 50 } 46 51 52 + impl FromRef<AppState> for Key { 53 + fn from_ref(state: &AppState) -> Self { 54 + state.key.clone() 55 + } 56 + } 57 + 58 + async fn prompt(jar: SignedCookieJar) -> (SignedCookieJar, Html<String>) { 59 + let m = if let Some(did) = jar.get("did") { 60 + format!("oh i know you: {did}") 61 + } else { 62 + LOGIN_HTML.into() 63 + }; 64 + (jar, Html(m)) 65 + } 66 + 47 67 #[derive(Debug, Deserialize)] 48 68 struct BeginOauthParams { 49 69 handle: String, ··· 51 71 async fn start_oauth( 52 72 State(state): State<AppState>, 53 73 Query(params): Query<BeginOauthParams>, 54 - ) -> Redirect { 55 - let AppState { client } = state; 56 - let BeginOauthParams { handle } = params; 57 - let auth_url = authorize(&client, &handle).await; 58 - Redirect::to(&auth_url) 74 + jar: SignedCookieJar, 75 + ) -> (SignedCookieJar, Redirect) { 76 + // if any existing session was active, clear it first 77 + let jar = jar.remove("did"); 78 + 79 + let auth_url = authorize(&state.client, &params.handle).await; 80 + (jar, Redirect::to(&auth_url)) 59 81 } 60 82 61 83 async fn complete_oauth( 62 84 State(state): State<AppState>, 63 85 Query(params): Query<CallbackParams>, 64 - ) -> Html<String> { 65 - let AppState { client } = state; 66 - let Ok((oauth_session, _)) = client.callback(params).await else { 86 + jar: SignedCookieJar, 87 + ) -> (SignedCookieJar, Html<String>) { 88 + let Ok((oauth_session, _)) = state.client.callback(params).await else { 67 89 panic!("failed to do client callback"); 68 90 }; 69 91 let did = oauth_session.did().await.expect("a did to be present"); 70 - Html(format!("sup: {did:?}")) 92 + let jar = jar.add(Cookie::new("did", did.to_string())); 93 + (jar, Html(format!("sup: {did:?}"))) 71 94 }
+17
who-am-i/static/login.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="utf-8" /> 5 + <title>Who-am-i</title> 6 + <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 + <meta name="description" content="Log in" /> 8 + </head> 9 + <body> 10 + <form action="/auth" method="GET"> 11 + <label> 12 + @<input name="handle" placeholder="example.bsky.social" /> 13 + </label> 14 + <button type="submit">log in</button> 15 + </form> 16 + </body> 17 + </html>