summaryrefslogtreecommitdiff
path: root/web/index.hoon
diff options
context:
space:
mode:
Diffstat (limited to 'web/index.hoon')
-rw-r--r--web/index.hoon465
1 files changed, 465 insertions, 0 deletions
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 "<svg width='10' height='10' viewBox='0 0 10 10' xmlns='http://www.w3.org/2000/svg'>"
+ "<circle r='3.09' cx='5' cy='5' /></svg>"
+++ 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();
+ }
+'''
+--