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
··· 164 164 ); 165 165 166 166 RenderHtml( 167 - "prompt-known", 167 + "prompt", 168 168 engine, 169 169 json!({ 170 170 "did": did,
+1 -1
who-am-i/static/style.css
··· 157 157 margin: 0 0 1rem; 158 158 } 159 159 160 - input#handle { 160 + input.handle { 161 161 border: none; 162 162 border-bottom: 1px dashed #aaa; 163 163 background: transparent;
-167
who-am-i/templates/prompt-base.hbs
··· 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 > .empty { 34 - font-size: 0.8rem; 35 - opacity: 0.5; 36 - } 37 - header > .title { 38 - text-align: center; 39 - } 40 - header > a.micro { 41 - text-decoration: none; 42 - font-size: 0.8rem; 43 - text-align: right; 44 - opacity: 0.5; 45 - } 46 - header > a.micro:hover { 47 - opacity: 1; 48 - } 49 - main { 50 - background: #ccc; 51 - display: flex; 52 - flex-direction: column; 53 - flex-grow: 1; 54 - padding: 0.25rem 0.5rem; 55 - } 56 - p { 57 - margin: 1rem 0 0; 58 - text-align: center; 59 - } 60 - p.detail { 61 - font-size: 0.8rem; 62 - } 63 - .parent-host { 64 - font-weight: bold; 65 - color: #48c; 66 - display: inline-block; 67 - padding: 0 0.125rem; 68 - border-radius: 0.25rem; 69 - border: 1px solid #aaa; 70 - font-size: 0.8rem; 71 - } 72 - 73 - #loader { 74 - display: flex; 75 - flex-grow: 1; 76 - justify-content: center; 77 - align-items: center; 78 - } 79 - .spinner { 80 - animation: rotation 1.618s ease-in-out infinite; 81 - border-radius: 50%; 82 - border: 3px dashed #434; 83 - box-sizing: border-box; 84 - display: inline-block; 85 - height: 1.5em; 86 - width: 1.5em; 87 - } 88 - @keyframes rotation { 89 - 0% { transform: rotate(0deg) } 90 - 100% { transform: rotate(360deg) } 91 - } 92 - 93 - #user-info { 94 - flex-grow: 1; 95 - display: flex; 96 - flex-direction: column; 97 - justify-content: center; 98 - } 99 - #action { 100 - background: #eee; 101 - display: flex; 102 - justify-content: space-between; 103 - padding: 0.5rem 0.25rem 0.5rem 0.5rem; 104 - font-size: 0.8rem; 105 - align-items: baseline; 106 - border-radius: 0.5rem; 107 - border: 1px solid #bbb; 108 - cursor: pointer; 109 - } 110 - #action:hover { 111 - background: #fff; 112 - } 113 - #allow { 114 - background: transparent; 115 - border: none; 116 - border-left: 1px solid #bbb; 117 - padding: 0 0.5rem; 118 - color: #375; 119 - font: inherit; 120 - cursor: pointer; 121 - } 122 - #action:hover #allow { 123 - color: #285; 124 - } 125 - 126 - #or { 127 - font-size: 0.8rem; 128 - text-align: center; 129 - } 130 - #or p { 131 - margin: 0 0 1rem; 132 - } 133 - 134 - input#handle { 135 - border: none; 136 - border-bottom: 1px dashed #aaa; 137 - background: transparent; 138 - } 139 - 140 - .hidden { 141 - display: none !important; 142 - } 143 - 144 - </style> 145 - 146 - <div class="wrap"> 147 - <header> 148 - <div class="empty">🔒</div> 149 - <code class="title" style="font-family: monospace;" 150 - >who-am-i</code> 151 - <a href="https://microcosm.blue" target="_blank" class="micro" 152 - ><span style="color: #f396a9">m</span 153 - ><span style="color: #f49c5c">i</span 154 - ><span style="color: #c7b04c">c</span 155 - ><span style="color: #92be4c">r</span 156 - ><span style="color: #4ec688">o</span 157 - ><span style="color: #51c2b6">c</span 158 - ><span style="color: #54bed7">o</span 159 - ><span style="color: #8fb1f1">s</span 160 - ><span style="color: #ce9df1">m</span 161 - ></a> 162 - </header> 163 - 164 - <main> 165 - {{> main}} 166 - </main> 167 - </div>
-59
who-am-i/templates/prompt-known.hbs
··· 1 - {{#*inline "main"}} 2 - <p> 3 - Share your identity with 4 - <span class="parent-host">{{ parent_host }}</span>? 5 - </p> 6 - <div id="loader"> 7 - <span class="spinner"></span> 8 - </div> 9 - <div id="user-info" class="hidden"> 10 - <div id="action"> 11 - <span id="handle"></span> 12 - <button id="allow">Allow</button> 13 - </div> 14 - </div> 15 - <div id="or"> 16 - <p>or, <a id="switch" href="#">use another account</a></p> 17 - </div> 18 - 19 - <script> 20 - var loaderEl = document.getElementById('loader'); 21 - var infoEl = document.getElementById('user-info'); 22 - var actionEl = document.getElementById('action'); 23 - var handleEl = document.getElementById('handle'); 24 - var allowEl = document.getElementById('allow'); 25 - var switchEl = document.getElementById('switch'); 26 - 27 - switchEl.addEventListener('click', e => { 28 - e.preventDefault(); 29 - console.log('switch plz'); 30 - }); 31 - 32 - var DID = {{{json did}}}; 33 - let user_info = new URL('/user-info', window.location); 34 - user_info.searchParams.set('fetch-key', {{{json fetch_key}}}); 35 - fetch(user_info) 36 - .then(resp => { 37 - if (!resp.ok) throw new Error('request failed'); 38 - return resp.json(); 39 - }) 40 - .then( 41 - ({ handle }) => { 42 - loaderEl.remove(); 43 - handleEl.textContent = `@${handle}`; 44 - infoEl.classList.remove('hidden'); 45 - actionEl.addEventListener('click', () => share(handle)); 46 - }, 47 - err => { 48 - infoEl.textContent = 'ohno'; 49 - console.error(err); 50 - }, 51 - ); 52 - 53 - function share(handle) { 54 - top.postMessage({ source: 'whoami', handle }, '*'); // TODO: pass the referrer back from server 55 - } 56 - </script> 57 - {{/inline}} 58 - 59 - {{#> prompt-base}}{{/prompt-base}}
+23 -10
who-am-i/templates/prompt.hbs
··· 16 16 <div id="user-info"> 17 17 <form id="form-action" action="/auth" method="GET" target="_blank" class="action {{#if did}}hidden{{/if}}"> 18 18 <label> 19 - @<input id="handle" name="handle" placeholder="example.bsky.social" /> 19 + @<input id="handle-input" class="handle" name="handle" placeholder="example.bsky.social" /> 20 20 </label> 21 21 <button id="connect" type="submit">connect</button> 22 22 </form> 23 23 24 24 <div id="handle-action" class="action"> 25 - <span id="handle"></span> 25 + <span id="handle-view" class="handle"></span> 26 26 <button id="allow">Allow</button> 27 27 </div> 28 28 </div> ··· 34 34 const promptEl = document.getElementById('prompt'); 35 35 const loaderEl = document.getElementById('loader'); 36 36 const infoEl = document.getElementById('user-info'); 37 - const handleEl = document.getElementById('handle'); 37 + const handleInputEl = document.getElementById('handle-input'); 38 + const handleViewEl = document.getElementById('handle-view'); 38 39 const formEl = document.getElementById('form-action'); // for anon 39 - const allowEl = document.getElementById('allow'); // for known-did 40 + const allowEl = document.getElementById('handle-action'); // for known-did 40 41 const connectEl = document.getElementById('connect'); // for anon 41 42 42 43 function err(e, msg) { ··· 46 47 throw new Error(e); 47 48 } 48 49 49 - formEl && (formEl.onsubmit = e => { 50 + // already-known user 51 + ({{{json did}}}) && (async () => { 52 + 53 + const handle = await lookUp({{{json fetch_key}}}); 54 + console.log('got handle', handle); 55 + 56 + loaderEl.classList.add('hidden'); 57 + handleViewEl.textContent = `@${handle}`; 58 + allowEl.addEventListener('click', () => shareAllow(handle)); 59 + })(); 60 + 61 + // anon user 62 + formEl.onsubmit = e => { 50 63 e.preventDefault(); 51 64 loaderEl.classList.remove('hidden'); 52 65 // TODO: include expected referer! (..this system is probably bad) 53 66 // maybe a random localstorage key that we specifically listen for? 54 67 const url = new URL('/auth', window.location); 55 - url.searchParams.set('handle', handleEl.value); 68 + url.searchParams.set('handle', handleInputEl.value); 56 69 window.open(url, '_blank'); 57 - }); 70 + }; 58 71 59 72 window.addEventListener('storage', async e => { 60 73 // here's a fun minor vuln: we can't tell which flow triggers the storage event. ··· 64 77 const fail = (e, msg) => { 65 78 loaderEl.classList.add('hidden'); 66 79 formEl.classList.remove('hidden'); 67 - handleEl.focus(); 68 - handleEl.select(); 80 + handleInputEl.focus(); 81 + handleInputEl.select(); 69 82 err(e, msg); 70 83 } 71 84 ··· 98 111 shareAllow(handle); 99 112 }); 100 113 101 - const lookUp = async fetch_key => { 114 + async function lookUp(fetch_key) { 102 115 const user_info = new URL('/user-info', window.location); 103 116 user_info.searchParams.set('fetch-key', fetch_key); 104 117 let info;