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

wip, prompting flow with cookie

Changed files
+70 -11
who-am-i
+18
Cargo.lock
···
"axum",
"axum-core",
"bytes",
"futures-util",
"headers",
"http",
···
"tower-http",
"tungstenite 0.26.2",
"zstd",
]
[[package]]
···
"atrium-identity",
"atrium-oauth",
"axum",
"clap",
"hickory-resolver",
"metrics",
···
"axum",
"axum-core",
"bytes",
+
"cookie",
"futures-util",
"headers",
"http",
···
"tower-http",
"tungstenite 0.26.2",
"zstd",
+
]
+
+
[[package]]
+
name = "cookie"
+
version = "0.18.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+
dependencies = [
+
"base64 0.22.1",
+
"hmac",
+
"percent-encoding",
+
"rand 0.8.5",
+
"sha2",
+
"subtle",
+
"time",
+
"version_check",
]
[[package]]
···
"atrium-identity",
"atrium-oauth",
"axum",
+
"axum-extra",
"clap",
"hickory-resolver",
"metrics",
+1
who-am-i/Cargo.toml
···
atrium-identity = "0.1.5"
atrium-oauth = "0.1.3"
axum = "0.8.4"
clap = { version = "4.5.40", features = ["derive"] }
hickory-resolver = "0.25.2"
metrics = "0.24.2"
···
atrium-identity = "0.1.5"
atrium-oauth = "0.1.3"
axum = "0.8.4"
+
axum-extra = { version = "0.10.1", features = ["cookie-signed", "typed-header"] }
clap = { version = "4.5.40", features = ["derive"] }
hickory-resolver = "0.25.2"
metrics = "0.24.2"
+34 -11
who-am-i/src/server.rs
···
use atrium_oauth::CallbackParams;
use axum::{
Router,
-
extract::{Query, State},
response::{Html, Redirect},
routing::get,
};
use serde::Deserialize;
use std::sync::Arc;
···
use crate::{Client, authorize, client};
const INDEX_HTML: &str = include_str!("../static/index.html");
-
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
pub async fn serve(shutdown: CancellationToken) {
let state = AppState {
client: Arc::new(client()),
};
let app = Router::new()
.route("/", get(|| async { Html(INDEX_HTML) }))
.route("/favicon.ico", get(|| async { FAVICON })) // todo MIME
.route("/auth", get(start_oauth))
.route("/authorized", get(complete_oauth))
.with_state(state);
···
#[derive(Clone)]
struct AppState {
pub client: Arc<Client>,
}
#[derive(Debug, Deserialize)]
struct BeginOauthParams {
handle: String,
···
async fn start_oauth(
State(state): State<AppState>,
Query(params): Query<BeginOauthParams>,
-
) -> Redirect {
-
let AppState { client } = state;
-
let BeginOauthParams { handle } = params;
-
let auth_url = authorize(&client, &handle).await;
-
Redirect::to(&auth_url)
}
async fn complete_oauth(
State(state): State<AppState>,
Query(params): Query<CallbackParams>,
-
) -> Html<String> {
-
let AppState { client } = state;
-
let Ok((oauth_session, _)) = client.callback(params).await else {
panic!("failed to do client callback");
};
let did = oauth_session.did().await.expect("a did to be present");
-
Html(format!("sup: {did:?}"))
}
···
use atrium_oauth::CallbackParams;
use axum::{
Router,
+
extract::{FromRef, Query, State},
response::{Html, Redirect},
routing::get,
};
+
use axum_extra::extract::cookie::{Cookie, Key, SignedCookieJar};
use serde::Deserialize;
use std::sync::Arc;
···
use crate::{Client, authorize, client};
+
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
const INDEX_HTML: &str = include_str!("../static/index.html");
+
const LOGIN_HTML: &str = include_str!("../static/login.html");
pub async fn serve(shutdown: CancellationToken) {
let state = AppState {
+
key: Key::generate(), // TODO: via config
client: Arc::new(client()),
};
let app = Router::new()
.route("/", get(|| async { Html(INDEX_HTML) }))
.route("/favicon.ico", get(|| async { FAVICON })) // todo MIME
+
.route("/prompt", get(prompt))
.route("/auth", get(start_oauth))
.route("/authorized", get(complete_oauth))
.with_state(state);
···
#[derive(Clone)]
struct AppState {
+
pub key: Key,
pub client: Arc<Client>,
}
+
impl FromRef<AppState> for Key {
+
fn from_ref(state: &AppState) -> Self {
+
state.key.clone()
+
}
+
}
+
+
async fn prompt(jar: SignedCookieJar) -> (SignedCookieJar, Html<String>) {
+
let m = if let Some(did) = jar.get("did") {
+
format!("oh i know you: {did}")
+
} else {
+
LOGIN_HTML.into()
+
};
+
(jar, Html(m))
+
}
+
#[derive(Debug, Deserialize)]
struct BeginOauthParams {
handle: String,
···
async fn start_oauth(
State(state): State<AppState>,
Query(params): Query<BeginOauthParams>,
+
jar: SignedCookieJar,
+
) -> (SignedCookieJar, Redirect) {
+
// if any existing session was active, clear it first
+
let jar = jar.remove("did");
+
+
let auth_url = authorize(&state.client, &params.handle).await;
+
(jar, Redirect::to(&auth_url))
}
async fn complete_oauth(
State(state): State<AppState>,
Query(params): Query<CallbackParams>,
+
jar: SignedCookieJar,
+
) -> (SignedCookieJar, Html<String>) {
+
let Ok((oauth_session, _)) = state.client.callback(params).await else {
panic!("failed to do client callback");
};
let did = oauth_session.did().await.expect("a did to be present");
+
let jar = jar.add(Cookie::new("did", did.to_string()));
+
(jar, Html(format!("sup: {did:?}")))
}
+17
who-am-i/static/login.html
···
···
+
<!doctype html>
+
<html lang="en">
+
<head>
+
<meta charset="utf-8" />
+
<title>Who-am-i</title>
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
+
<meta name="description" content="Log in" />
+
</head>
+
<body>
+
<form action="/auth" method="GET">
+
<label>
+
@<input name="handle" placeholder="example.bsky.social" />
+
</label>
+
<button type="submit">log in</button>
+
</form>
+
</body>
+
</html>