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
look up identity from did-cookie
bad-example.com
8 months ago
8ddb11f6
08b772bf
+468
-25
9 changed files
expand all
collapse all
unified
split
Cargo.lock
who-am-i
Cargo.toml
demo
index.html
src
expiring_task_map.rs
identity_resolver.rs
lib.rs
main.rs
server.rs
templates
prompt-known.hbs
+161
Cargo.lock
···
500
]
501
502
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
503
name = "backtrace"
504
version = "0.3.74"
505
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1164
]
1165
1166
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1167
name = "digest"
1168
version = "0.10.7"
1169
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1716
"tokio",
1717
"tokio-util",
1718
"tracing",
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
1719
]
1720
1721
[[package]]
···
2879
]
2880
2881
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2882
name = "num-traits"
2883
version = "0.2.19"
2884
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3058
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
3059
3060
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
3061
name = "pin-project-lite"
3062
version = "0.2.16"
3063
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3602
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
3603
3604
[[package]]
0
0
0
0
0
0
0
0
0
3605
name = "schannel"
3606
version = "0.1.27"
3607
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4557
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
4558
4559
[[package]]
0
0
0
0
0
0
4560
name = "ufos"
4561
version = "0.1.0"
4562
dependencies = [
···
4734
]
4735
4736
[[package]]
0
0
0
0
0
0
0
0
0
0
4737
name = "want"
4738
version = "0.3.1"
4739
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4865
version = "0.1.0"
4866
dependencies = [
4867
"atrium-api 0.25.4",
0
4868
"atrium-identity",
4869
"atrium-oauth",
4870
"axum",
4871
"axum-extra",
0
4872
"clap",
0
0
0
4873
"hickory-resolver",
4874
"metrics",
0
4875
"serde",
0
4876
"tokio",
4877
"tokio-util",
0
4878
]
4879
4880
[[package]]
···
4898
version = "0.4.0"
4899
source = "registry+https://github.com/rust-lang/crates.io-index"
4900
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
0
0
0
0
0
0
0
0
0
4901
4902
[[package]]
4903
name = "winapi-x86_64-pc-windows-gnu"
···
500
]
501
502
[[package]]
503
+
name = "axum-template"
504
+
version = "3.0.0"
505
+
source = "registry+https://github.com/rust-lang/crates.io-index"
506
+
checksum = "3df50f7d669bfc3a8c348f08f536fe37e7acfbeded3cfdffd2ad3d76725fc40c"
507
+
dependencies = [
508
+
"axum",
509
+
"handlebars",
510
+
"serde",
511
+
"thiserror 2.0.12",
512
+
]
513
+
514
+
[[package]]
515
name = "backtrace"
516
version = "0.3.74"
517
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1176
]
1177
1178
[[package]]
1179
+
name = "derive_builder"
1180
+
version = "0.20.2"
1181
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1182
+
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
1183
+
dependencies = [
1184
+
"derive_builder_macro",
1185
+
]
1186
+
1187
+
[[package]]
1188
+
name = "derive_builder_core"
1189
+
version = "0.20.2"
1190
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1191
+
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
1192
+
dependencies = [
1193
+
"darling",
1194
+
"proc-macro2",
1195
+
"quote",
1196
+
"syn",
1197
+
]
1198
+
1199
+
[[package]]
1200
+
name = "derive_builder_macro"
1201
+
version = "0.20.2"
1202
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1203
+
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
1204
+
dependencies = [
1205
+
"derive_builder_core",
1206
+
"syn",
1207
+
]
1208
+
1209
+
[[package]]
1210
name = "digest"
1211
version = "0.10.7"
1212
source = "registry+https://github.com/rust-lang/crates.io-index"
···
1759
"tokio",
1760
"tokio-util",
1761
"tracing",
1762
+
]
1763
+
1764
+
[[package]]
1765
+
name = "handlebars"
1766
+
version = "6.3.2"
1767
+
source = "registry+https://github.com/rust-lang/crates.io-index"
1768
+
checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
1769
+
dependencies = [
1770
+
"derive_builder",
1771
+
"log",
1772
+
"num-order",
1773
+
"pest",
1774
+
"pest_derive",
1775
+
"serde",
1776
+
"serde_json",
1777
+
"thiserror 2.0.12",
1778
+
"walkdir",
1779
]
1780
1781
[[package]]
···
2939
]
2940
2941
[[package]]
2942
+
name = "num-modular"
2943
+
version = "0.6.1"
2944
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2945
+
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
2946
+
2947
+
[[package]]
2948
+
name = "num-order"
2949
+
version = "1.2.0"
2950
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2951
+
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
2952
+
dependencies = [
2953
+
"num-modular",
2954
+
]
2955
+
2956
+
[[package]]
2957
name = "num-traits"
2958
version = "0.2.19"
2959
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3133
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
3134
3135
[[package]]
3136
+
name = "pest"
3137
+
version = "2.8.1"
3138
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3139
+
checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323"
3140
+
dependencies = [
3141
+
"memchr",
3142
+
"thiserror 2.0.12",
3143
+
"ucd-trie",
3144
+
]
3145
+
3146
+
[[package]]
3147
+
name = "pest_derive"
3148
+
version = "2.8.1"
3149
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3150
+
checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc"
3151
+
dependencies = [
3152
+
"pest",
3153
+
"pest_generator",
3154
+
]
3155
+
3156
+
[[package]]
3157
+
name = "pest_generator"
3158
+
version = "2.8.1"
3159
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3160
+
checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966"
3161
+
dependencies = [
3162
+
"pest",
3163
+
"pest_meta",
3164
+
"proc-macro2",
3165
+
"quote",
3166
+
"syn",
3167
+
]
3168
+
3169
+
[[package]]
3170
+
name = "pest_meta"
3171
+
version = "2.8.1"
3172
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3173
+
checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5"
3174
+
dependencies = [
3175
+
"pest",
3176
+
"sha2",
3177
+
]
3178
+
3179
+
[[package]]
3180
name = "pin-project-lite"
3181
version = "0.2.16"
3182
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3721
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
3722
3723
[[package]]
3724
+
name = "same-file"
3725
+
version = "1.0.6"
3726
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3727
+
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
3728
+
dependencies = [
3729
+
"winapi-util",
3730
+
]
3731
+
3732
+
[[package]]
3733
name = "schannel"
3734
version = "0.1.27"
3735
source = "registry+https://github.com/rust-lang/crates.io-index"
···
4685
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
4686
4687
[[package]]
4688
+
name = "ucd-trie"
4689
+
version = "0.1.7"
4690
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4691
+
checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
4692
+
4693
+
[[package]]
4694
name = "ufos"
4695
version = "0.1.0"
4696
dependencies = [
···
4868
]
4869
4870
[[package]]
4871
+
name = "walkdir"
4872
+
version = "2.5.0"
4873
+
source = "registry+https://github.com/rust-lang/crates.io-index"
4874
+
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
4875
+
dependencies = [
4876
+
"same-file",
4877
+
"winapi-util",
4878
+
]
4879
+
4880
+
[[package]]
4881
name = "want"
4882
version = "0.3.1"
4883
source = "registry+https://github.com/rust-lang/crates.io-index"
···
5009
version = "0.1.0"
5010
dependencies = [
5011
"atrium-api 0.25.4",
5012
+
"atrium-common 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
5013
"atrium-identity",
5014
"atrium-oauth",
5015
"axum",
5016
"axum-extra",
5017
+
"axum-template",
5018
"clap",
5019
+
"ctrlc",
5020
+
"dashmap",
5021
+
"handlebars",
5022
"hickory-resolver",
5023
"metrics",
5024
+
"rand 0.9.1",
5025
"serde",
5026
+
"serde_json",
5027
"tokio",
5028
"tokio-util",
5029
+
"url",
5030
]
5031
5032
[[package]]
···
5050
version = "0.4.0"
5051
source = "registry+https://github.com/rust-lang/crates.io-index"
5052
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
5053
+
5054
+
[[package]]
5055
+
name = "winapi-util"
5056
+
version = "0.1.9"
5057
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5058
+
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
5059
+
dependencies = [
5060
+
"windows-sys 0.48.0",
5061
+
]
5062
5063
[[package]]
5064
name = "winapi-x86_64-pc-windows-gnu"
+8
who-am-i/Cargo.toml
···
5
6
[dependencies]
7
atrium-api = { version = "0.25.4", default-features = false }
0
8
atrium-identity = "0.1.5"
9
atrium-oauth = "0.1.3"
10
axum = "0.8.4"
11
axum-extra = { version = "0.10.1", features = ["cookie-signed", "typed-header"] }
0
12
clap = { version = "4.5.40", features = ["derive"] }
0
0
0
13
hickory-resolver = "0.25.2"
14
metrics = "0.24.2"
0
15
serde = { version = "1.0.219", features = ["derive"] }
0
16
tokio = { version = "1.45.1", features = ["full", "macros"] }
17
tokio-util = "0.7.15"
0
···
5
6
[dependencies]
7
atrium-api = { version = "0.25.4", default-features = false }
8
+
atrium-common = "0.1.2"
9
atrium-identity = "0.1.5"
10
atrium-oauth = "0.1.3"
11
axum = "0.8.4"
12
axum-extra = { version = "0.10.1", features = ["cookie-signed", "typed-header"] }
13
+
axum-template = { version = "3.0.0", features = ["handlebars"] }
14
clap = { version = "4.5.40", features = ["derive"] }
15
+
ctrlc = "3.4.7"
16
+
dashmap = "6.1.0"
17
+
handlebars = { version = "6.3.2", features = ["dir_source"] }
18
hickory-resolver = "0.25.2"
19
metrics = "0.24.2"
20
+
rand = "0.9.1"
21
serde = { version = "1.0.219", features = ["derive"] }
22
+
serde_json = "1.0.140"
23
tokio = { version = "1.45.1", features = ["full", "macros"] }
24
tokio-util = "0.7.15"
25
+
url = "2.5.4"
+1
-3
who-am-i/demo/index.html
···
9
10
<h1>hey</h1>
11
12
-
<iframe src="http://127.0.0.1:9997/prompt" style="border: 2px solid #000; border-radius: 0.5em;" />
13
-
14
-
···
9
10
<h1>hey</h1>
11
12
+
<iframe src="http://127.0.0.1:9997/prompt" style="border: none" height="140" width="280" />
0
0
+53
who-am-i/src/expiring_task_map.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use dashmap::DashMap;
2
+
use rand::{Rng, distr::Alphanumeric};
3
+
use std::sync::Arc;
4
+
use std::time::Duration;
5
+
use tokio::task::{JoinHandle, spawn};
6
+
use tokio::time::sleep; // 0.8
7
+
8
+
#[derive(Clone)]
9
+
pub struct ExpiringTaskMap<T>(Arc<TaskMap<T>>);
10
+
11
+
impl<T: Send + 'static> ExpiringTaskMap<T> {
12
+
pub fn new(expiration: Duration) -> Self {
13
+
let map = TaskMap {
14
+
map: DashMap::new(),
15
+
expiration,
16
+
};
17
+
Self(Arc::new(map))
18
+
}
19
+
20
+
pub fn dispatch(&self, task: impl Future<Output = T> + Send + 'static) -> String {
21
+
let task_key: String = rand::rng()
22
+
.sample_iter(&Alphanumeric)
23
+
.take(24)
24
+
.map(char::from)
25
+
.collect();
26
+
27
+
// spawn a tokio task and put the join handle in the map for later retrieval
28
+
self.0.map.insert(task_key.clone(), spawn(task));
29
+
30
+
// spawn a second task to clean up the map in case it doesn't get claimed
31
+
spawn({
32
+
let me = self.0.clone();
33
+
let key = task_key.clone();
34
+
async move {
35
+
sleep(me.expiration).await;
36
+
let _ = me.map.remove(&key);
37
+
// TODO: also use a cancellation token so taking and expiring can mutually cancel
38
+
}
39
+
});
40
+
41
+
task_key
42
+
}
43
+
44
+
pub fn take(&self, key: &str) -> Option<JoinHandle<T>> {
45
+
eprintln!("trying to take...");
46
+
self.0.map.remove(key).map(|(_, handle)| handle)
47
+
}
48
+
}
49
+
50
+
struct TaskMap<T> {
51
+
map: DashMap<String, JoinHandle<T>>,
52
+
expiration: Duration,
53
+
}
+20
who-am-i/src/identity_resolver.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
use atrium_api::types::string::Did;
2
+
use atrium_common::resolver::Resolver;
3
+
use atrium_identity::did::{CommonDidResolver, CommonDidResolverConfig, DEFAULT_PLC_DIRECTORY_URL};
4
+
use atrium_oauth::DefaultHttpClient;
5
+
use std::sync::Arc;
6
+
7
+
pub async fn resolve_identity(did: String) -> String {
8
+
let http_client = Arc::new(DefaultHttpClient::default());
9
+
let resolver = CommonDidResolver::new(CommonDidResolverConfig {
10
+
plc_directory_url: DEFAULT_PLC_DIRECTORY_URL.to_string(),
11
+
http_client: Arc::clone(&http_client),
12
+
});
13
+
let doc = resolver.resolve(&Did::new(did).unwrap()).await.unwrap(); // TODO: this is only half the resolution? or is atrium checking dns?
14
+
if let Some(aka) = doc.also_known_as {
15
+
if let Some(f) = aka.first() {
16
+
return f.to_string();
17
+
}
18
+
}
19
+
"who knows".to_string()
20
+
}
+4
who-am-i/src/lib.rs
···
1
mod dns_resolver;
0
0
2
mod oauth;
3
mod server;
4
5
pub use dns_resolver::HickoryDnsTxtResolver;
0
0
6
pub use oauth::{Client, authorize, client};
7
pub use server::serve;
···
1
mod dns_resolver;
2
+
mod expiring_task_map;
3
+
mod identity_resolver;
4
mod oauth;
5
mod server;
6
7
pub use dns_resolver::HickoryDnsTxtResolver;
8
+
pub use expiring_task_map::ExpiringTaskMap;
9
+
pub use identity_resolver::resolve_identity;
10
pub use oauth::{Client, authorize, client};
11
pub use server::serve;
+27
-2
who-am-i/src/main.rs
···
0
1
use tokio_util::sync::CancellationToken;
2
use who_am_i::serve;
3
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
4
#[tokio::main]
5
async fn main() {
6
-
let server_shutdown = CancellationToken::new();
7
-
serve(server_shutdown).await;
0
0
0
0
0
0
8
}
···
1
+
use clap::Parser;
2
use tokio_util::sync::CancellationToken;
3
use who_am_i::serve;
4
5
+
/// Aggregate links in the at-mosphere
6
+
#[derive(Parser, Debug, Clone)]
7
+
#[command(version, about, long_about = None)]
8
+
struct Args {
9
+
/// secret key from which the cookie-signing key is derived
10
+
///
11
+
/// must have at least 512 bits (64 bytes) of randomness
12
+
///
13
+
/// eg: `cat /dev/urandom | head -c 64 | base64`
14
+
#[arg(long)]
15
+
app_secret: String,
16
+
/// Enable dev mode
17
+
///
18
+
/// enables automatic template reloading
19
+
#[arg(long, action)]
20
+
dev: bool,
21
+
}
22
+
23
#[tokio::main]
24
async fn main() {
25
+
let shutdown = CancellationToken::new();
26
+
27
+
let ctrlc_shutdown = shutdown.clone();
28
+
ctrlc::set_handler(move || ctrlc_shutdown.cancel()).expect("failed to set ctrl-c handler");
29
+
30
+
let args = Args::parse();
31
+
32
+
serve(shutdown, args.app_secret, args.dev).await;
33
}
+98
-20
who-am-i/src/server.rs
···
3
use axum::{
4
Router,
5
extract::{FromRef, Query, State},
0
6
response::{Html, IntoResponse, Redirect},
7
routing::get,
8
};
9
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
0
0
10
11
-
use serde::Deserialize;
0
12
use std::sync::Arc;
0
13
use tokio::net::TcpListener;
14
use tokio_util::sync::CancellationToken;
0
15
16
-
use crate::{Client, authorize, client};
17
18
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
19
const INDEX_HTML: &str = include_str!("../static/index.html");
20
const LOGIN_HTML: &str = include_str!("../static/login.html");
21
22
-
pub async fn serve(shutdown: CancellationToken) {
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
23
let state = AppState {
24
-
key: Key::generate(), // TODO: via config
0
25
client: Arc::new(client()),
0
26
};
27
28
let app = Router::new()
29
.route("/", get(|| async { Html(INDEX_HTML) }))
30
.route("/favicon.ico", get(|| async { FAVICON })) // todo MIME
31
.route("/prompt", get(prompt))
0
32
.route("/auth", get(start_oauth))
33
.route("/authorized", get(complete_oauth))
34
.with_state(state);
···
43
.unwrap();
44
}
45
46
-
#[derive(Clone)]
47
-
struct AppState {
48
-
pub key: Key,
49
-
pub client: Arc<Client>,
0
50
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
51
52
-
impl FromRef<AppState> for Key {
53
-
fn from_ref(state: &AppState) -> Self {
54
-
state.key.clone()
55
-
}
0
0
0
0
0
0
0
0
0
0
56
}
57
58
-
async fn prompt(jar: SignedCookieJar) -> impl IntoResponse {
59
-
let m = if let Some(did) = jar.get("did") {
60
-
format!("oh i know you: {did}")
61
-
} else {
62
-
LOGIN_HTML.into()
0
0
0
0
0
0
0
63
};
64
-
(jar, Html(m))
0
65
}
66
67
#[derive(Debug, Deserialize)]
···
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, ¶ms.handle).await;
80
(jar, Redirect::to(&auth_url))
···
89
panic!("failed to do client callback");
90
};
91
let did = oauth_session.did().await.expect("a did to be present");
92
-
let cookie = Cookie::build(("did", did.to_string()))
93
.http_only(true)
94
.secure(true)
95
.same_site(SameSite::None)
···
3
use axum::{
4
Router,
5
extract::{FromRef, Query, State},
6
+
http::header::{HeaderMap, REFERER},
7
response::{Html, IntoResponse, Redirect},
8
routing::get,
9
};
10
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
11
+
use axum_template::{RenderHtml, engine::Engine};
12
+
use handlebars::{Handlebars, handlebars_helper};
13
14
+
use serde::{Deserialize, Serialize};
15
+
use serde_json::Value;
16
use std::sync::Arc;
17
+
use std::time::Duration;
18
use tokio::net::TcpListener;
19
use tokio_util::sync::CancellationToken;
20
+
use url::Url;
21
22
+
use crate::{Client, ExpiringTaskMap, authorize, client, resolve_identity};
23
24
const FAVICON: &[u8] = include_bytes!("../static/favicon.ico");
25
const INDEX_HTML: &str = include_str!("../static/index.html");
26
const LOGIN_HTML: &str = include_str!("../static/login.html");
27
28
+
const DID_COOKIE_KEY: &str = "did";
29
+
30
+
type AppEngine = Engine<Handlebars<'static>>;
31
+
32
+
#[derive(Clone)]
33
+
struct AppState {
34
+
pub key: Key,
35
+
pub engine: AppEngine,
36
+
pub client: Arc<Client>,
37
+
pub resolving: ExpiringTaskMap<String>,
38
+
}
39
+
40
+
impl FromRef<AppState> for Key {
41
+
fn from_ref(state: &AppState) -> Self {
42
+
state.key.clone()
43
+
}
44
+
}
45
+
46
+
pub async fn serve(shutdown: CancellationToken, app_secret: String, dev: bool) {
47
+
let mut hbs = Handlebars::new();
48
+
hbs.set_dev_mode(dev);
49
+
hbs.register_templates_directory("templates", Default::default())
50
+
.unwrap();
51
+
52
+
handlebars_helper!(json: |v: Value| serde_json::to_string(&v).unwrap());
53
+
hbs.register_helper("json", Box::new(json));
54
+
55
+
// clients have to pick up their identity-resolving tasks within this period
56
+
let task_pickup_expiration = Duration::from_secs(15);
57
+
58
let state = AppState {
59
+
engine: Engine::new(hbs),
60
+
key: Key::from(app_secret.as_bytes()), // TODO: via config
61
client: Arc::new(client()),
62
+
resolving: ExpiringTaskMap::new(task_pickup_expiration),
63
};
64
65
let app = Router::new()
66
.route("/", get(|| async { Html(INDEX_HTML) }))
67
.route("/favicon.ico", get(|| async { FAVICON })) // todo MIME
68
.route("/prompt", get(prompt))
69
+
.route("/user-info", get(user_info))
70
.route("/auth", get(start_oauth))
71
.route("/authorized", get(complete_oauth))
72
.with_state(state);
···
81
.unwrap();
82
}
83
84
+
#[derive(Debug, Serialize)]
85
+
struct Known {
86
+
did: Value,
87
+
fetch_key: Value,
88
+
parent_host: String,
89
}
90
+
async fn prompt(
91
+
State(AppState {
92
+
engine, resolving, ..
93
+
}): State<AppState>,
94
+
jar: SignedCookieJar,
95
+
headers: HeaderMap,
96
+
) -> impl IntoResponse {
97
+
let Some(referrer) = headers.get(REFERER) else {
98
+
return Html::<&'static str>("missing referrer, sorry").into_response();
99
+
};
100
+
let Ok(referrer) = referrer.to_str() else {
101
+
return "referer contained opaque bytes".into_response();
102
+
};
103
+
let Ok(url) = Url::parse(referrer) else {
104
+
return "referrer was not a url".into_response();
105
+
};
106
+
let Some(parent_host) = url.host_str() else {
107
+
return "could nto get host from url".into_response();
108
+
};
109
+
let m = if let Some(did) = jar.get(DID_COOKIE_KEY) {
110
+
let did = did.value_trimmed().to_string();
111
112
+
let fetch_key = resolving.dispatch(resolve_identity(did.clone()));
113
+
114
+
let json_did = Value::String(did);
115
+
let json_fetch_key = Value::String(fetch_key);
116
+
let known = Known {
117
+
did: json_did,
118
+
fetch_key: json_fetch_key,
119
+
parent_host: parent_host.to_string(),
120
+
};
121
+
return (jar, RenderHtml("prompt-known", engine, known)).into_response();
122
+
} else {
123
+
LOGIN_HTML.into_response()
124
+
};
125
+
(jar, Html(m)).into_response()
126
}
127
128
+
#[derive(Debug, Deserialize)]
129
+
#[serde(rename_all = "kebab-case")]
130
+
struct UserInfoParams {
131
+
fetch_key: String,
132
+
}
133
+
async fn user_info(
134
+
State(AppState { resolving, .. }): State<AppState>,
135
+
Query(params): Query<UserInfoParams>,
136
+
) -> impl IntoResponse {
137
+
// let fetch_key: [char; 16] = params.fetch_key.chars().collect::<Vec<_>>().try_into().unwrap();
138
+
let Some(handle) = resolving.take(¶ms.fetch_key) else {
139
+
return "oops, task does not exist or is gone".into_response();
140
};
141
+
let s = handle.await.unwrap();
142
+
format!("sup: {s}").into_response()
143
}
144
145
#[derive(Debug, Deserialize)]
···
152
jar: SignedCookieJar,
153
) -> (SignedCookieJar, Redirect) {
154
// if any existing session was active, clear it first
155
+
let jar = jar.remove(DID_COOKIE_KEY);
156
157
let auth_url = authorize(&state.client, ¶ms.handle).await;
158
(jar, Redirect::to(&auth_url))
···
167
panic!("failed to do client callback");
168
};
169
let did = oauth_session.did().await.expect("a did to be present");
170
+
let cookie = Cookie::build((DID_COOKIE_KEY, did.to_string()))
171
.http_only(true)
172
.secure(true)
173
.same_site(SameSite::None)
+96
who-am-i/templates/prompt-known.hbs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
<!doctype html>
2
+
3
+
<style>
4
+
body {
5
+
color: #434;
6
+
font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
7
+
margin: 0;
8
+
min-height: 100vh;
9
+
padding: 0;
10
+
}
11
+
.wrap {
12
+
border: 2px solid #221828;
13
+
border-radius: 0.5rem;
14
+
box-sizing: border-box;
15
+
overflow: hidden;
16
+
display: flex;
17
+
flex-direction: column;
18
+
height: 100vh;
19
+
}
20
+
header {
21
+
background: #221828;
22
+
display: flex;
23
+
justify-content: space-between;
24
+
padding: 0 0.25rem;
25
+
color: #c9b;
26
+
display: flex;
27
+
gap: 0.5rem;
28
+
align-items: baseline;
29
+
}
30
+
header > * {
31
+
flex-basis: 33%;
32
+
}
33
+
header > .title {
34
+
text-align: center;
35
+
}
36
+
header > a.micro {
37
+
text-decoration: none;
38
+
font-size: 0.8rem;
39
+
text-align: right;
40
+
opacity: 0.5;
41
+
}
42
+
header > a.micro:hover {
43
+
opacity: 1;
44
+
}
45
+
main {
46
+
padding: 0.25rem 0.5rem;
47
+
background: #ccc;
48
+
flex-grow: 1;
49
+
}
50
+
p {
51
+
margin: 0.5rem 0;
52
+
}
53
+
</style>
54
+
55
+
<div class="wrap">
56
+
<header>
57
+
<div class="empty"></div>
58
+
<code class="title" style="font-family: monospace;"
59
+
>who-am-i</code>
60
+
<a href="https://microcosm.blue" target="_blank" class="micro"
61
+
><span style="color: #f396a9">m</span
62
+
><span style="color: #f49c5c">i</span
63
+
><span style="color: #c7b04c">c</span
64
+
><span style="color: #92be4c">r</span
65
+
><span style="color: #4ec688">o</span
66
+
><span style="color: #51c2b6">c</span
67
+
><span style="color: #54bed7">o</span
68
+
><span style="color: #8fb1f1">s</span
69
+
><span style="color: #ce9df1">m</span
70
+
></a>
71
+
</header>
72
+
73
+
<main>
74
+
<p>Share your identity with {{ parent_host }}?</p>
75
+
<div id="user-info">Loading…</div>
76
+
</main>
77
+
</div>
78
+
79
+
80
+
<script>
81
+
const infoEl = document.getElementById('user-info');
82
+
var DID = {{{json did}}};
83
+
let user_info = new URL('/user-info', window.location);
84
+
user_info.searchParams.set('fetch-key', {{{json fetch_key}}});
85
+
fetch(user_info).then(
86
+
info => {
87
+
infoEl.textContent = 'yay';
88
+
console.log(info);
89
+
},
90
+
err => {
91
+
infoEl.textContent = 'ohno';
92
+
console.error(err);
93
+
},
94
+
);
95
+
96
+
</script>