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

fix up known-user flow

Changed files
+25 -238
who-am-i
+1 -1
who-am-i/src/server.rs
···
);
RenderHtml(
-
"prompt-known",
+
"prompt",
engine,
json!({
"did": did,
+1 -1
who-am-i/static/style.css
···
margin: 0 0 1rem;
}
-
input#handle {
+
input.handle {
border: none;
border-bottom: 1px dashed #aaa;
background: transparent;
-167
who-am-i/templates/prompt-base.hbs
···
-
<!doctype html>
-
-
<style>
-
body {
-
color: #434;
-
font-family: 'Iowan Old Style', 'Palatino Linotype', 'URW Palladio L', P052, serif;
-
margin: 0;
-
min-height: 100vh;
-
padding: 0;
-
}
-
.wrap {
-
border: 2px solid #221828;
-
border-radius: 0.5rem;
-
box-sizing: border-box;
-
overflow: hidden;
-
display: flex;
-
flex-direction: column;
-
height: 100vh;
-
}
-
header {
-
background: #221828;
-
display: flex;
-
justify-content: space-between;
-
padding: 0 0.25rem;
-
color: #c9b;
-
display: flex;
-
gap: 0.5rem;
-
align-items: baseline;
-
}
-
header > * {
-
flex-basis: 33%;
-
}
-
header > .empty {
-
font-size: 0.8rem;
-
opacity: 0.5;
-
}
-
header > .title {
-
text-align: center;
-
}
-
header > a.micro {
-
text-decoration: none;
-
font-size: 0.8rem;
-
text-align: right;
-
opacity: 0.5;
-
}
-
header > a.micro:hover {
-
opacity: 1;
-
}
-
main {
-
background: #ccc;
-
display: flex;
-
flex-direction: column;
-
flex-grow: 1;
-
padding: 0.25rem 0.5rem;
-
}
-
p {
-
margin: 1rem 0 0;
-
text-align: center;
-
}
-
p.detail {
-
font-size: 0.8rem;
-
}
-
.parent-host {
-
font-weight: bold;
-
color: #48c;
-
display: inline-block;
-
padding: 0 0.125rem;
-
border-radius: 0.25rem;
-
border: 1px solid #aaa;
-
font-size: 0.8rem;
-
}
-
-
#loader {
-
display: flex;
-
flex-grow: 1;
-
justify-content: center;
-
align-items: center;
-
}
-
.spinner {
-
animation: rotation 1.618s ease-in-out infinite;
-
border-radius: 50%;
-
border: 3px dashed #434;
-
box-sizing: border-box;
-
display: inline-block;
-
height: 1.5em;
-
width: 1.5em;
-
}
-
@keyframes rotation {
-
0% { transform: rotate(0deg) }
-
100% { transform: rotate(360deg) }
-
}
-
-
#user-info {
-
flex-grow: 1;
-
display: flex;
-
flex-direction: column;
-
justify-content: center;
-
}
-
#action {
-
background: #eee;
-
display: flex;
-
justify-content: space-between;
-
padding: 0.5rem 0.25rem 0.5rem 0.5rem;
-
font-size: 0.8rem;
-
align-items: baseline;
-
border-radius: 0.5rem;
-
border: 1px solid #bbb;
-
cursor: pointer;
-
}
-
#action:hover {
-
background: #fff;
-
}
-
#allow {
-
background: transparent;
-
border: none;
-
border-left: 1px solid #bbb;
-
padding: 0 0.5rem;
-
color: #375;
-
font: inherit;
-
cursor: pointer;
-
}
-
#action:hover #allow {
-
color: #285;
-
}
-
-
#or {
-
font-size: 0.8rem;
-
text-align: center;
-
}
-
#or p {
-
margin: 0 0 1rem;
-
}
-
-
input#handle {
-
border: none;
-
border-bottom: 1px dashed #aaa;
-
background: transparent;
-
}
-
-
.hidden {
-
display: none !important;
-
}
-
-
</style>
-
-
<div class="wrap">
-
<header>
-
<div class="empty">🔒</div>
-
<code class="title" style="font-family: monospace;"
-
>who-am-i</code>
-
<a href="https://microcosm.blue" target="_blank" class="micro"
-
><span style="color: #f396a9">m</span
-
><span style="color: #f49c5c">i</span
-
><span style="color: #c7b04c">c</span
-
><span style="color: #92be4c">r</span
-
><span style="color: #4ec688">o</span
-
><span style="color: #51c2b6">c</span
-
><span style="color: #54bed7">o</span
-
><span style="color: #8fb1f1">s</span
-
><span style="color: #ce9df1">m</span
-
></a>
-
</header>
-
-
<main>
-
{{> main}}
-
</main>
-
</div>
-59
who-am-i/templates/prompt-known.hbs
···
-
{{#*inline "main"}}
-
<p>
-
Share your identity with
-
<span class="parent-host">{{ parent_host }}</span>?
-
</p>
-
<div id="loader">
-
<span class="spinner"></span>
-
</div>
-
<div id="user-info" class="hidden">
-
<div id="action">
-
<span id="handle"></span>
-
<button id="allow">Allow</button>
-
</div>
-
</div>
-
<div id="or">
-
<p>or, <a id="switch" href="#">use another account</a></p>
-
</div>
-
-
<script>
-
var loaderEl = document.getElementById('loader');
-
var infoEl = document.getElementById('user-info');
-
var actionEl = document.getElementById('action');
-
var handleEl = document.getElementById('handle');
-
var allowEl = document.getElementById('allow');
-
var switchEl = document.getElementById('switch');
-
-
switchEl.addEventListener('click', e => {
-
e.preventDefault();
-
console.log('switch plz');
-
});
-
-
var DID = {{{json did}}};
-
let user_info = new URL('/user-info', window.location);
-
user_info.searchParams.set('fetch-key', {{{json fetch_key}}});
-
fetch(user_info)
-
.then(resp => {
-
if (!resp.ok) throw new Error('request failed');
-
return resp.json();
-
})
-
.then(
-
({ handle }) => {
-
loaderEl.remove();
-
handleEl.textContent = `@${handle}`;
-
infoEl.classList.remove('hidden');
-
actionEl.addEventListener('click', () => share(handle));
-
},
-
err => {
-
infoEl.textContent = 'ohno';
-
console.error(err);
-
},
-
);
-
-
function share(handle) {
-
top.postMessage({ source: 'whoami', handle }, '*'); // TODO: pass the referrer back from server
-
}
-
</script>
-
{{/inline}}
-
-
{{#> prompt-base}}{{/prompt-base}}
+23 -10
who-am-i/templates/prompt.hbs
···
<div id="user-info">
<form id="form-action" action="/auth" method="GET" target="_blank" class="action {{#if did}}hidden{{/if}}">
<label>
-
@<input id="handle" name="handle" placeholder="example.bsky.social" />
+
@<input id="handle-input" class="handle" name="handle" placeholder="example.bsky.social" />
</label>
<button id="connect" type="submit">connect</button>
</form>
<div id="handle-action" class="action">
-
<span id="handle"></span>
+
<span id="handle-view" class="handle"></span>
<button id="allow">Allow</button>
</div>
</div>
···
const promptEl = document.getElementById('prompt');
const loaderEl = document.getElementById('loader');
const infoEl = document.getElementById('user-info');
-
const handleEl = document.getElementById('handle');
+
const handleInputEl = document.getElementById('handle-input');
+
const handleViewEl = document.getElementById('handle-view');
const formEl = document.getElementById('form-action'); // for anon
-
const allowEl = document.getElementById('allow'); // for known-did
+
const allowEl = document.getElementById('handle-action'); // for known-did
const connectEl = document.getElementById('connect'); // for anon
function err(e, msg) {
···
throw new Error(e);
}
-
formEl && (formEl.onsubmit = e => {
+
// already-known user
+
({{{json did}}}) && (async () => {
+
+
const handle = await lookUp({{{json fetch_key}}});
+
console.log('got handle', handle);
+
+
loaderEl.classList.add('hidden');
+
handleViewEl.textContent = `@${handle}`;
+
allowEl.addEventListener('click', () => shareAllow(handle));
+
})();
+
+
// anon user
+
formEl.onsubmit = e => {
e.preventDefault();
loaderEl.classList.remove('hidden');
// TODO: include expected referer! (..this system is probably bad)
// maybe a random localstorage key that we specifically listen for?
const url = new URL('/auth', window.location);
-
url.searchParams.set('handle', handleEl.value);
+
url.searchParams.set('handle', handleInputEl.value);
window.open(url, '_blank');
-
});
+
};
window.addEventListener('storage', async e => {
// here's a fun minor vuln: we can't tell which flow triggers the storage event.
···
const fail = (e, msg) => {
loaderEl.classList.add('hidden');
formEl.classList.remove('hidden');
-
handleEl.focus();
-
handleEl.select();
+
handleInputEl.focus();
+
handleInputEl.select();
err(e, msg);
}
···
shareAllow(handle);
});
-
const lookUp = async fetch_key => {
+
async function lookUp(fetch_key) {
const user_info = new URL('/user-info', window.location);
user_info.searchParams.set('fetch-key', fetch_key);
let info;