From 6dccba9bb5100329209ad01732f9d63f4c4fb43b Mon Sep 17 00:00:00 2001 From: polwex Date: Sun, 22 Jun 2025 06:14:42 +0700 Subject: metamask login getting there --- web/index.hoon | 465 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 465 insertions(+) create mode 100644 web/index.hoon (limited to 'web/index.hoon') diff --git a/web/index.hoon b/web/index.hoon new file mode 100644 index 0000000..9aede2e --- /dev/null +++ b/web/index.hoon @@ -0,0 +1,465 @@ +|_ 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:"Login" + ;form#form(action "/~/login", method "POST") + ;h2.tc: Urbit OS (Azimuth via Arvo) + ;input.mono(type "text") + =name "name" + =id "name" + =placeholder "~sampel-palnet" + =required "true" + =minlength "4" + =maxlength "14" + =pattern "~((([a-z]\{6})\{1,2}-\{0,2})+|[a-z]\{3})"; + ;input(type "hidden", name "redirect", value redirect-str); + ;button(name "eauth", type "submit"):"Login via Ship »" + == + ;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 one for free from Red Horizon." + == + == + == +== +++ 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" + }]; + + window.addEventListener("DOMContentLoaded", () => { + + const spinner = document.getElementById("spinner"); + const pointsDiv = document.getElementById("wallet-points"); + const metamaskButton = document.getElementById("mauth"); + 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 => { + const div = document.createElement("div"); + div.innerText = `${point}`; + div.addEventListener("click", e => { + if (spinner.innerText) return + spinner.innerText = "Logging in..." + metamaskLogin(provider.address, point).then(res => { + spinner.innerText = `${res}` + }) + }) + pointsDiv.appendChild(div); + }) + // why doesn't this work wtf + // for (const points of points){ + // console.log({point}) + // } + + }).catch(e => { + metamaskButton.disabled = false; + }) + }) + }); + }); + + async function fetchSecret() { + try { + const response = await fetch('/forum/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]; + 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('/forum/auth', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + who: point, + secret: secret, + address: account, + signature: signature + }), + }); + + if (response.ok) { + // location.reload(); + // window.location.replace('/forum'); + return "done!" + } 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(); + } +''' +-- -- cgit v1.2.3