+9
-4
who-am-i/src/expiring_task_map.rs
+9
-4
who-am-i/src/expiring_task_map.rs
···
49
49
.run_until_cancelled(sleep(expiration))
50
50
.await
51
51
.is_some()
52
+
// the (sleep) task completed first
52
53
{
53
-
// is Some if the (sleep) task completed first
54
54
map.remove(&k);
55
55
cancel.cancel();
56
56
metrics::counter!("whoami_task_map_completions", "result" => "expired")
···
62
62
}
63
63
64
64
pub fn take(&self, key: &str) -> Option<JoinHandle<T>> {
65
-
metrics::counter!("whoami_task_map_completions", "result" => "retrieved").increment(1);
66
-
// when the _guard drops, the token gets cancelled for us
67
-
self.0.map.remove(key).map(|(_, (_guard, handle))| handle)
65
+
if let Some((_key, (_guard, handle))) = self.0.map.remove(key) {
66
+
// when the _guard drops, it cancels the token for us
67
+
metrics::counter!("whoami_task_map_completions", "result" => "retrieved").increment(1);
68
+
Some(handle)
69
+
} else {
70
+
metrics::counter!("whoami_task_map_gones").increment(1);
71
+
None
72
+
}
68
73
}
69
74
}
70
75
+7
-16
who-am-i/src/server.rs
+7
-16
who-am-i/src/server.rs
···
1
1
use atrium_api::types::string::Did;
2
2
use axum::{
3
3
Router,
4
-
extract::{FromRef, Query, State},
4
+
extract::{FromRef, Json as ExtractJson, Query, State},
5
5
http::{
6
6
StatusCode,
7
-
header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER, X_FRAME_OPTIONS},
7
+
header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER},
8
8
},
9
9
response::{IntoResponse, Json, Redirect, Response},
10
10
routing::{get, post},
···
87
87
.route("/favicon.ico", get(favicon)) // todo MIME
88
88
.route("/style.css", get(css))
89
89
.route("/prompt", get(prompt))
90
-
.route("/user-info", get(user_info))
90
+
.route("/user-info", post(user_info))
91
91
.route("/auth", get(start_oauth))
92
92
.route("/authorized", get(complete_oauth))
93
93
.route("/disconnect", post(disconnect))
···
137
137
} else {
138
138
json!({})
139
139
};
140
-
let frame_headers = [
141
-
(X_FRAME_OPTIONS, "deny"),
142
-
(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'"),
143
-
];
140
+
let frame_headers = [(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'")];
144
141
(frame_headers, jar, RenderHtml("hello", engine, info)).into_response()
145
142
}
146
143
···
205
202
return err("Referer origin is opaque", true);
206
203
}
207
204
208
-
let frame_headers = [
209
-
(X_FRAME_OPTIONS, format!("allow-from {parent_origin}")),
210
-
(
211
-
CONTENT_SECURITY_POLICY,
212
-
format!("frame-ancestors {parent_origin}"),
213
-
),
214
-
];
205
+
let csp = format!("frame-ancestors {parent_origin}");
206
+
let frame_headers = [(CONTENT_SECURITY_POLICY, &csp)];
215
207
216
208
if let Some(did) = jar.get(DID_COOKIE_KEY) {
217
209
let Ok(did) = Did::new(did.value_trimmed().to_string()) else {
···
258
250
}
259
251
260
252
#[derive(Debug, Deserialize)]
261
-
#[serde(rename_all = "kebab-case")]
262
253
struct UserInfoParams {
263
254
fetch_key: String,
264
255
}
···
266
257
State(AppState {
267
258
resolve_handles, ..
268
259
}): State<AppState>,
269
-
Query(params): Query<UserInfoParams>,
260
+
ExtractJson(params): ExtractJson<UserInfoParams>,
270
261
) -> impl IntoResponse {
271
262
let err = |status, reason: &str| {
272
263
metrics::counter!("whoami_user_info", "found" => "false", "reason" => reason.to_string())
+5
-4
who-am-i/templates/hello.hbs
+5
-4
who-am-i/templates/hello.hbs
···
38
38
({{{json did}}}) && (async () => {
39
39
40
40
const handle = await lookUp({{{json fetch_key}}});
41
-
console.log('got handle', handle);
42
41
43
42
loaderEl.classList.add('hidden');
44
43
handleViewEl.textContent = `@${handle}`;
···
54
53
})();
55
54
56
55
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
56
let info;
60
57
try {
61
-
const resp = await fetch(user_info);
58
+
const resp = await fetch('/user-info', {
59
+
method: 'POST',
60
+
headers: {'Content-Type': 'application/json'},
61
+
body: JSON.stringify({ fetch_key }),
62
+
});
62
63
if (!resp.ok) throw resp;
63
64
info = await resp.json();
64
65
} catch (e) {
+22
-19
who-am-i/templates/prompt.hbs
+22
-19
who-am-i/templates/prompt.hbs
···
49
49
50
50
// already-known user
51
51
({{{json did}}}) && (async () => {
52
-
53
52
const handle = await lookUp({{{json fetch_key}}});
54
-
console.log('got handle', handle);
55
-
56
53
loaderEl.classList.add('hidden');
57
54
handleViewEl.textContent = `@${handle}`;
58
55
allowEl.addEventListener('click', () => shareAllow(handle, {{{json token}}}));
···
74
71
// so if you have two flows going, it grants for both (or the first responder?) if you grant for either.
75
72
// (letting this slide while parent pages are allowlisted to microcosm only)
76
73
77
-
const fail = (e, msg) => {
78
-
loaderEl.classList.add('hidden');
79
-
formEl.classList.remove('hidden');
80
-
handleInputEl.focus();
81
-
handleInputEl.select();
82
-
err(e, msg);
83
-
}
74
+
if (e.key !== 'who-am-i') return;
75
+
if (e.newValue === null) return;
84
76
85
-
const details = localStorage.getItem("who-am-i");
77
+
const details = e.newValue;
86
78
if (!details) {
87
-
console.error("hmm, heard from localstorage but did not get DID");
88
-
return;
79
+
console.error("hmm, heard from localstorage but did not get DID", details, e);
80
+
err('sorry, something went wrong getting your details');
89
81
}
90
-
localStorage.removeItem("who-am-i");
82
+
localStorage.removeItem(e.key);
91
83
92
84
let parsed;
93
85
try {
···
96
88
err(e, "something went wrong getting the details back");
97
89
}
98
90
91
+
const fail = (e, msg) => {
92
+
loaderEl.classList.add('hidden');
93
+
formEl.classList.remove('hidden');
94
+
handleInputEl.focus();
95
+
handleInputEl.select();
96
+
err(e, msg);
97
+
}
98
+
99
99
if (parsed.result === "fail") {
100
100
fail(`uh oh: ${parsed.reason}`);
101
101
}
···
108
108
109
109
const handle = await lookUp(parsed.fetch_key);
110
110
111
-
shareAllow(handle, token);
111
+
shareAllow(handle, parsed.token);
112
112
});
113
113
114
114
async function lookUp(fetch_key) {
115
-
const user_info = new URL('/user-info', window.location);
116
-
user_info.searchParams.set('fetch-key', fetch_key);
117
115
let info;
118
116
try {
119
-
const resp = await fetch(user_info);
117
+
const resp = await fetch('/user-info', {
118
+
method: 'POST',
119
+
headers: { 'Content-Type': 'application/json' },
120
+
body: JSON.stringify({ fetch_key }),
121
+
});
120
122
if (!resp.ok) throw resp;
121
123
info = await resp.json();
122
124
} catch (e) {
123
-
err(e, 'failed to resolve handle from DID')
125
+
err(e, `failed to resolve handle from DID with ${fetch_key}`);
124
126
}
125
127
return info.handle;
126
128
}
···
130
132
{ action: "allow", handle, token },
131
133
{{{json parent_origin}}},
132
134
);
135
+
promptEl.textContent = '✔️ shared';
133
136
}
134
137
135
138
const shareDeny = reason => {