tangled
alpha
login
or
join now
bad-example.com
/
microcosm-links
7
fork
atom
APIs for links and references in the ATmosphere
7
fork
atom
overview
issues
pulls
pipelines
wip, prompting flow with cookie
bad-example.com
8 months ago
b9be4800
56812cb0
+70
-11
4 changed files
expand all
collapse all
unified
split
Cargo.lock
who-am-i
Cargo.toml
src
server.rs
static
login.html
+18
Cargo.lock
···
469
469
"axum",
470
470
"axum-core",
471
471
"bytes",
472
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
922
+
]
923
923
+
924
924
+
[[package]]
925
925
+
name = "cookie"
926
926
+
version = "0.18.1"
927
927
+
source = "registry+https://github.com/rust-lang/crates.io-index"
928
928
+
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
929
929
+
dependencies = [
930
930
+
"base64 0.22.1",
931
931
+
"hmac",
932
932
+
"percent-encoding",
933
933
+
"rand 0.8.5",
934
934
+
"sha2",
935
935
+
"subtle",
936
936
+
"time",
937
937
+
"version_check",
921
938
]
922
939
923
940
[[package]]
···
4851
4868
"atrium-identity",
4852
4869
"atrium-oauth",
4853
4870
"axum",
4871
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
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
5
-
extract::{Query, State},
5
5
+
extract::{FromRef, Query, State},
6
6
response::{Html, Redirect},
7
7
routing::get,
8
8
};
9
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
18
+
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
17
19
const INDEX_HTML: &str = include_str!("../static/index.html");
18
18
-
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
20
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
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
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
48
+
pub key: Key,
44
49
pub client: Arc<Client>,
45
50
}
46
51
52
52
+
impl FromRef<AppState> for Key {
53
53
+
fn from_ref(state: &AppState) -> Self {
54
54
+
state.key.clone()
55
55
+
}
56
56
+
}
57
57
+
58
58
+
async fn prompt(jar: SignedCookieJar) -> (SignedCookieJar, Html<String>) {
59
59
+
let m = if let Some(did) = jar.get("did") {
60
60
+
format!("oh i know you: {did}")
61
61
+
} else {
62
62
+
LOGIN_HTML.into()
63
63
+
};
64
64
+
(jar, Html(m))
65
65
+
}
66
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
54
-
) -> Redirect {
55
55
-
let AppState { client } = state;
56
56
-
let BeginOauthParams { handle } = params;
57
57
-
let auth_url = authorize(&client, &handle).await;
58
58
-
Redirect::to(&auth_url)
74
74
+
jar: SignedCookieJar,
75
75
+
) -> (SignedCookieJar, Redirect) {
76
76
+
// if any existing session was active, clear it first
77
77
+
let jar = jar.remove("did");
78
78
+
79
79
+
let auth_url = authorize(&state.client, ¶ms.handle).await;
80
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
64
-
) -> Html<String> {
65
65
-
let AppState { client } = state;
66
66
-
let Ok((oauth_session, _)) = client.callback(params).await else {
86
86
+
jar: SignedCookieJar,
87
87
+
) -> (SignedCookieJar, Html<String>) {
88
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
70
-
Html(format!("sup: {did:?}"))
92
92
+
let jar = jar.add(Cookie::new("did", did.to_string()));
93
93
+
(jar, Html(format!("sup: {did:?}")))
71
94
}
+17
who-am-i/static/login.html
···
1
1
+
<!doctype html>
2
2
+
<html lang="en">
3
3
+
<head>
4
4
+
<meta charset="utf-8" />
5
5
+
<title>Who-am-i</title>
6
6
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7
7
+
<meta name="description" content="Log in" />
8
8
+
</head>
9
9
+
<body>
10
10
+
<form action="/auth" method="GET">
11
11
+
<label>
12
12
+
@<input name="handle" placeholder="example.bsky.social" />
13
13
+
</label>
14
14
+
<button type="submit">log in</button>
15
15
+
</form>
16
16
+
</body>
17
17
+
</html>