|_ bowl:gall ++ auth-styling ''' @import url("https://rsms.me/inter/inter.css"); @font-face { font-family: "Source Code Pro"; src: url("https://storage.googleapis.com/media.urbit.org/fonts/scp-regular.woff"); font-weight: 400; font-display: swap; } :root { --red-soft: #FFEFEC; --red: #FF6240; --gray-100: #E5E5E5; --gray-400: #999999; --gray-800: #333333; --white: #FFFFFF; } html { font-family: Inter, sans-serif; height: 100%; margin: 0; width: 100%; background: var(--white); color: var(--gray-800); -webkit-font-smoothing: antialiased; line-height: 1.5; font-size: 16px; font-weight: 600; display: flex; flex-flow: row nowrap; justify-content: center; } body { display: flex; flex-flow: column nowrap; justify-content: center; max-width: 300px; padding: 1rem; width: 100%; } body.local #eauth, body.eauth #local { display: none; min-height: 100%; } #eauth input { /*NOTE dumb hack to get approx equal height with #local */ margin-bottom: 15px; } body nav { background: var(--gray-100); border-radius: 2rem; display: flex; justify-content: space-around; overflow: hidden; margin-bottom: 1rem; } body nav div { width: 50%; padding: 0.5rem 1rem; text-align: center; cursor: pointer; } body.local nav div.local, body.eauth nav div.eauth { background: var(--gray-800); color: var(--white); cursor: default; } nav div.local { border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; } nav div.eauth { border-left: none; border-top-left-radius: 0; border-bottom-left-radius: 0; } body > *, form > input { width: 100%; } form { display: flex; flex-flow: column; align-items: flex-start; } input { background: var(--gray-100); border: 2px solid transparent; padding: 0.5rem; border-radius: 0.5rem; font-size: inherit; color: var(--gray-800); box-shadow: none; width: 100%; } input:disabled { background: var(--gray-100); color: var(--gray-400); } input:focus { outline: none; background: var(--white); border-color: var(--gray-400); } input:invalid:not(:focus) { background: var(--red-soft); border-color: var(--red); outline: none; color: var(--red); } button[type=submit] { margin-top: 1rem; } button[type=submit], a.button { font-size: 1rem; padding: 0.5rem 1rem; border-radius: 0.5rem; background: var(--gray-800); color: var(--white); border: none; font-weight: 600; text-decoration: none; } input:invalid ~ button[type=submit] { border-color: currentColor; background: var(--gray-100); color: var(--gray-400); pointer-events: none; } span.guest, span.guest a { color: var(--gray-400); } span.failed { display: flex; flex-flow: row nowrap; height: 1rem; align-items: center; margin-top: 0.875rem; color: var(--red); } span.failed svg { height: 1rem; margin-right: 0.25rem; } span.failed path { fill: transparent; stroke-width: 2px; stroke-linecap: round; stroke: currentColor; } .mono { font-family: 'Source Code Pro', monospace; } @media all and (prefers-color-scheme: dark) { :root { --white: #000000; --gray-800: #E5E5E5; --gray-400: #808080; --gray-100: #333333; --red-soft: #7F1D1D; } } @media screen and (min-width: 30em) { html { font-size: 14px; } } ''' ++ favicon %+ weld "" "" ++ landscape ^- manx =/ eauth=(unit ?) [~ %|] =/ failed=? .n =/ identity=identity:eyre [%ours ~] =/ redirect-url=(unit @t) ~ =/ redirect-str ?~(redirect-url "" (trip u.redirect-url)) ;html ;head ;meta(charset "utf-8"); ;meta(name "viewport", content "width=device-width, initial-scale=1, shrink-to-fit=no"); ;link(rel "icon", type "image/svg+xml", href (weld "data:image/svg+xml;utf8," favicon)); ;title:"Urbit" ;style:"{(trip auth-styling)}" ;style:"{?^(eauth "" "nav \{ display: none; }")}" ;script:"our = '{(scow %p our)}';" ;script:''' let name, pass; function setup(isEauth) { name = document.getElementById('name'); pass = document.getElementById('pass'); if (isEauth) goEauth(); else goLocal(); } function goLocal() { document.body.className = 'local'; pass.focus(); } function goEauth() { document.body.className = 'eauth'; name.focus(); } function doEauth() { if (name.value == our) { event.preventDefault(); goLocal(); } } ''' == ;body =class "{?:(=(`& eauth) "eauth" "local")}" =onload "setup({?:(=(`& eauth) "true" "false")})" ;div#local ;p:"Urbit ID" ;input(value "{(scow %p our)}", disabled "true", class "mono"); ;+ ?: =(%ours -.identity) ;div ;p:"Already authenticated" ;a.button/"{(trip (fall redirect-url '/'))}":"Continue" == ;form(action "/~/login", method "post", enctype "application/x-www-form-urlencoded") ;p:"Access Key" ;input =type "password" =name "password" =id "pass" =placeholder "sampel-ticlyt-migfun-falmel" =class "mono" =required "true" =minlength "27" =maxlength "27" =pattern "((?:[a-z]\{6}-)\{3}(?:[a-z]\{6}))"; ;input(type "hidden", name "redirect", value redirect-str); ;+ ?. failed ;span; ;span.failed ;svg(xmlns "http://www.w3.org/2000/svg", viewBox "0 0 16 16") ;path(d "m8 8 4-4M8 8 4 4m4 4-4 4m4-4 4 4"); == Key is incorrect == ;button(type "submit"):"Continue" == == ;div#eauth ;form(action "/~/login", method "post", onsubmit "return doEauth()") ;p:"Urbit ID Metamask Login" ;input.mono =name "name" =id "name" =placeholder "{(scow %p our)}" =required "true" =minlength "4" =maxlength "57" =pattern "~((([a-z]\{6})\{1,2}-\{0,2})+|[a-z]\{3})"; ;p ; You will be redirected to your own web interface to authorize ; logging in to ;span.mono:"{(scow %p our)}" ; . == ;input(type "hidden", name "redirect", value redirect-str); ;button(name "eauth", type "submit"):"Continue" == == ;* ?: ?=(%ours -.identity) ~ =+ as="proceed as{?:(?=(%fake -.identity) " guest" "")}" ;+ ;span.guest.mono ; Or try to ;a/"{(trip (fall redirect-url '/'))}":"{as}" ; . == == ;script:''' var failSpan = document.querySelector('.failed'); if (failSpan) { document.querySelector("input[type=password]") .addEventListener('keyup', function (event) { failSpan.style.display = 'none'; }); } ''' == ++ $ =/ redirect-str "/forum" ;html ;head ;meta(charset "utf-8"); ;meta(name "viewport", content "width=device-width, initial-scale=1, shrink-to-fit=no"); ;link(rel "icon", type "image/svg+xml", href (weld "data:image/svg+xml;utf8," favicon)); == ;body ;main#login-page.white ;h1.tc:"Zodiac Login" ;button(id "mauth"):"Login via 🦊MetaMask »" ;script(type "module"):"{metamask-script}" :: ;script(type "importmap"):"{import-script}" ;div(id "wallet-points"); ;div(id "spinner"); ;h2.tc: Join the Urbit Network ;div.tc.nudge ;a.button/"https://redhorizon.com/join/2d55b768-a5f4-45cf-a4e5-a4302e05a1f9":"Get Urbit ID »" ;p:"If you don't have an Urbit ID, get a fucking $URBIT token." == == == == ++ import-script ^~ %- trip ''' {"imports": { "ethers": "/node_modules/ethers/" }} ''' ++ metamask-script ^~ %- trip ''' import { ethers } from "https://cdnjs.cloudflare.com/ajax/libs/ethers/6.7.0/ethers.min.js"; const AZIMUTH_ADDRESS = "0x223c067F8CF28ae173EE5CafEa60cA44C335fecB"; const AZIMUTH_ABI_MINI = [{ "constant": true, "inputs": [{ "name": "_whose", "type": "address" }], "name": "getOwnedPoints", "outputs": [{ "name": "ownedPoints", "type": "uint32[]" }], "payable": false, "stateMutability": "view", "type": "function" }]; const spinner = document.getElementById("spinner"); const pointsDiv = document.getElementById("wallet-points"); const metamaskButton = document.getElementById("mauth"); let ethAddress; window.addEventListener("DOMContentLoaded", () => { metamaskButton.addEventListener("click", (event) => { event.preventDefault(); // Prevent the form from submitting the default way metamaskButton.disabled = true; getProvider().then(provider => { getPoints(provider).then(points => { console.log({points}) points.forEach(point => { insertSigil(point); }) // why doesn't this work wtf // for (const points of points){ // console.log({point}) // } }).catch(e => { metamaskButton.disabled = false; }) }) }); }); async function insertSigil(point){ const div = document.createElement("div"); div.style.cursor = "pointer"; const sigilDiv = await fetchSigil(point); div.innerHTML = sigilDiv; div.addEventListener("click", e => { if (spinner.innerText) return spinner.innerText = "Logging in..." metamaskLogin(ethAddress, point) }) pointsDiv.appendChild(div); } async function fetchSigil(ship) { try { const response = await fetch('/zodiac/sigil/' + ship); if (response.ok) { const data = await response.text(); return data; } else { throw new Error('Failed to fetch sigil'); } } catch (error) { console.error('Error fetching sigil :', error); } } async function fetchSecret() { try { const response = await fetch('/zodiac/metamask'); if (response.ok) { const data = await response.json(); return data.challenge; } else { throw new Error('Failed to retrieve secret'); } } catch (error) { console.error('Error fetching secret:', error); } } async function getProvider(){ if (typeof window.ethereum !== 'undefined') { try { const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); const account = accounts[0]; ethAddress = account; console.log("logged with metamask on account", account); const provider = new ethers.BrowserProvider(window.ethereum) const t1 = await provider.getBalance(account) console.log({t1}) const signer = await provider.getSigner(); console.log({provider, signer}) return signer } catch (error) { alert("MetaMask initialization failed"); } } else { alert("MetaMask is not installed. Please install it to continue."); } } async function metamaskLogin(account, point){ // Fetch the secret from the server const secret = await fetchSecret(); console.log({secret}); const signature = await window.ethereum.request({ method: "personal_sign", params: [secret, account], }); const response = await fetch('/zodiac/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ who: Number(point), secret: secret, address: account, signature: signature }), }); if (response.ok) { // location.reload(); window.location.replace('/zodiac'); } else { alert("Login failed. Please try again."); } } async function getPoints(provider){ const address = provider.address; const contract = new ethers.Contract( AZIMUTH_ADDRESS, AZIMUTH_ABI_MINI, provider, ); const res = await contract.getOwnedPoints(address); return res.toArray(); } ''' --