+34
-7
who-am-i/src/server.rs
+34
-7
who-am-i/src/server.rs
···
7
7
header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, HeaderMap, REFERER, X_FRAME_OPTIONS},
8
8
},
9
9
response::{IntoResponse, Json, Redirect, Response},
10
-
routing::get,
10
+
routing::{get, post},
11
11
};
12
12
use axum_extra::extract::cookie::{Cookie, Key, SameSite, SignedCookieJar};
13
13
use axum_template::{RenderHtml, engine::Engine};
···
85
85
.route("/user-info", get(user_info))
86
86
.route("/auth", get(start_oauth))
87
87
.route("/authorized", get(complete_oauth))
88
+
.route("/disconnect", post(disconnect))
88
89
.with_state(state);
89
90
90
91
let listener = TcpListener::bind("0.0.0.0:9997")
···
98
99
}
99
100
100
101
async fn hello(
101
-
State(AppState { engine, .. }): State<AppState>,
102
+
State(AppState {
103
+
engine,
104
+
resolve_handles,
105
+
shutdown,
106
+
oauth,
107
+
..
108
+
}): State<AppState>,
102
109
mut jar: SignedCookieJar,
103
110
) -> Response {
104
-
// push expiry (or clean up) the current cookie
105
-
if let Some(did) = jar.get(DID_COOKIE_KEY) {
111
+
let info = if let Some(did) = jar.get(DID_COOKIE_KEY) {
106
112
if let Ok(did) = Did::new(did.value_trimmed().to_string()) {
113
+
// push cookie expiry
107
114
jar = jar.add(cookie(&did));
115
+
let fetch_key = resolve_handles.dispatch(
116
+
{
117
+
let oauth = oauth.clone();
118
+
let did = did.clone();
119
+
async move { oauth.resolve_handle(did.clone()).await }
120
+
},
121
+
shutdown.child_token(),
122
+
);
123
+
json!({
124
+
"did": did,
125
+
"fetch_key": fetch_key,
126
+
})
108
127
} else {
109
128
jar = jar.remove(DID_COOKIE_KEY);
129
+
json!({})
110
130
}
111
-
}
131
+
} else {
132
+
json!({})
133
+
};
112
134
let frame_headers = [
113
135
(X_FRAME_OPTIONS, "deny"),
114
136
(CONTENT_SECURITY_POLICY, "frame-ancestors 'none'"),
115
137
];
116
-
(frame_headers, jar, RenderHtml("hello", engine, json!({}))).into_response()
138
+
(frame_headers, jar, RenderHtml("hello", engine, info)).into_response()
117
139
}
118
140
119
141
async fn css() -> impl IntoResponse {
···
179
201
(X_FRAME_OPTIONS, format!("allow-from {parent_origin}")),
180
202
(
181
203
CONTENT_SECURITY_POLICY,
182
-
format!("frame-ancestors {parent_host}"),
204
+
format!("frame-ancestors {parent_origin}"),
183
205
),
184
206
];
185
207
···
362
384
});
363
385
(jar, RenderHtml("authorized", engine, info)).into_response()
364
386
}
387
+
388
+
async fn disconnect(jar: SignedCookieJar) -> impl IntoResponse {
389
+
let jar = jar.remove(DID_COOKIE_KEY);
390
+
(jar, Json(json!({ "ok": true })))
391
+
}
+5
-1
who-am-i/static/style.css
+5
-1
who-am-i/static/style.css
···
136
136
}
137
137
138
138
#connect,
139
-
#allow {
139
+
#allow,
140
+
#revoke {
140
141
background: transparent;
141
142
border: none;
142
143
border-left: 1px solid #bbb;
···
144
145
color: #375;
145
146
font: inherit;
146
147
cursor: pointer;
148
+
}
149
+
#revoke {
150
+
color: #a31;
147
151
}
148
152
#action:hover #allow {
149
153
color: #285;
+68
-3
who-am-i/templates/hello.hbs
+68
-3
who-am-i/templates/hello.hbs
···
1
1
{{#*inline "description"}}A little identity-verifying auth service for microcosm demos{{/inline}}
2
2
3
3
{{#*inline "main"}}
4
-
<div class="mini-content">
5
-
This is a little identity-verifying service for microcosm demos.
6
-
</div>
4
+
<div class="mini-content">
5
+
This is a little identity-verifying service for microcosm demos.
6
+
7
+
{{#if did}}
8
+
<p id="error-message" class="hidden"></p>
9
+
10
+
<p id="prompt" class="detail">
11
+
Connected identity:
12
+
</p>
13
+
14
+
<div id="loader">
15
+
<span class="spinner"></span>
16
+
</div>
17
+
18
+
<div id="user-info">
19
+
<div id="handle-action" class="action">
20
+
<span id="handle-view" class="handle"></span>
21
+
<button id="revoke">disconnect</button>
22
+
</div>
23
+
</div>
24
+
<script>
25
+
const errorEl = document.getElementById('error-message');
26
+
const loaderEl = document.getElementById('loader');
27
+
const handleViewEl = document.getElementById('handle-view');
28
+
const revokeEl = document.getElementById('revoke'); // for known-did
29
+
30
+
function err(e, msg) {
31
+
loaderEl.classList.add('hidden');
32
+
errorEl.classList.remove('hidden');
33
+
errorEl.textContent = msg || e;
34
+
throw new Error(e);
35
+
}
36
+
37
+
// already-known user
38
+
({{{json did}}}) && (async () => {
39
+
40
+
const handle = await lookUp({{{json fetch_key}}});
41
+
console.log('got handle', handle);
42
+
43
+
loaderEl.classList.add('hidden');
44
+
handleViewEl.textContent = `@${handle}`;
45
+
revokeEl.addEventListener('click', async () => {
46
+
try {
47
+
let res = await fetch('/disconnect', { method: 'POST', credentials: 'include' });
48
+
if (!res.ok) throw res;
49
+
} catch (e) {
50
+
err(e, 'failed to clear session, sorry');
51
+
}
52
+
window.location.reload();
53
+
});
54
+
})();
55
+
56
+
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
+
let info;
60
+
try {
61
+
const resp = await fetch(user_info);
62
+
if (!resp.ok) throw resp;
63
+
info = await resp.json();
64
+
} catch (e) {
65
+
err(e, 'failed to resolve handle from DID')
66
+
}
67
+
return info.handle;
68
+
}
69
+
</script>
70
+
{{/if}}
71
+
</div>
7
72
{{/inline}}
8
73
9
74
{{#> base-full}}{{/base-full}}