OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web / web / static / login.js

110 lines · javascript ref:
// favela /login controller — drives the DID web sign-in DOM against the live /api/auth/* routes.
// Loaded as an ES module by the SSR /login page. All crypto is client-side (the sub-key secret never
// leaves the browser); this file is only glue between the AtprotoIdentity client and the page elements.
import { AtprotoIdentity } from './browser-identity.js'

const id = new AtprotoIdentity()
let autoSignInTried = false // pgv3-parity: attempt a silent re-sign-in once per page load
const $ = (sel) => document.querySelector(sel)
const show = (el, on) => { if (el) el.style.display = on ? '' : 'none' }
function status (msg, kind) {
  const el = $('#status')
  if (!el) return
  el.textContent = msg || ''
  el.className = kind || ''
}

async function render () {
  const subKey = id.subKeyHex()
  const skEl = $('#subkey')
  if (skEl) skEl.textContent = subKey

  let session = await id.whoami()
  const pairing = id.getPairing()

  // pgv3-parity: hold a delegation but no live session (e.g. favela-web restarted + cleared its
  // in-memory sessions)? Silently re-sign-in with the stored delegation — never fall back to
  // paste-invite. Try once per load; on failure (expired/revoked delegation) fall through to the
  // manual sign-in / pair boxes below.
  if (!session && id.isPaired() && !autoSignInTried) {
    autoSignInTried = true
    try { await id.signIn(); session = await id.whoami() } catch {}
  }

  // Signed in → show the session + a link home.
  show($('#session-box'), !!session)
  if (session) {
    const who = $('#session-who')
    if (who) who.textContent = session.handle ? `@${session.handle}` : session.did
  }

  // Paired (delegation stored) but not signed in → offer sign-in.
  show($('#signin-box'), !session && id.isPaired())
  if (!session && pairing) {
    const p = $('#paired-did')
    if (p) p.textContent = pairing.handle ? `@${pairing.handle} (${pairing.did})` : pairing.did
  }

  // Not paired → show the pairing instructions + form.
  show($('#pair-box'), !session && !id.isPaired())
}

// Pair: paste the ONE hash-URL invite the hpp minted (favela-pair). favela-web joins the tunnel, has the
// hpp delegate this browser's sub-key, and signs us in — no manual delegation shuttle.
async function onPair (e) {
  e.preventDefault()
  const field = ($('#pair-invite') && $('#pair-invite').value.trim()) || ''
  const invite = field || (location.hash || '').replace(/^.*[#?&]pair=/, '')
  try {
    status('Pairing over the tunnel (this can take a few seconds)…')
    const s = await id.pairWithInvite(invite)
    status('Signed in as ' + (s.handle ? '@' + s.handle : s.did) + '.', 'ok')
    await render()
    setTimeout(() => { window.location.href = '/' }, 600)
  } catch (err) {
    status(err.message || 'pairing failed', 'err')
    await render()
  }
}

async function onSignIn () {
  status('Signing in…')
  try {
    await id.signIn()
    status('Signed in.', 'ok')
    setTimeout(() => { window.location.href = '/' }, 400)
  } catch (err) {
    status(err.message || 'sign-in failed', 'err')
    // A stale/expired delegation can't be recovered by retrying — surface the unpair option.
    show($('#pair-box'), true)
  }
}

async function onLogout () {
  await id.logout()
  status('Signed out.')
  await render()
}

async function onUnpair () {
  id.unpair()
  status('Unpaired this browser.')
  await render()
}

async function onCopy () {
  try { await navigator.clipboard.writeText(id.subKeyHex()); status('Sub-key copied.', 'ok') } catch { status('Copy failed — select it manually.', 'err') }
}

window.addEventListener('DOMContentLoaded', () => {
  // Prefill the invite from a `…/login#pair=<invite>` link the hpp printed.
  const fromHash = (location.hash || '').replace(/^.*[#?&]pair=/, '')
  const inviteField = $('#pair-invite'); if (inviteField && fromHash && fromHash !== location.hash) inviteField.value = fromHash
  const pairForm = $('#pair-form'); if (pairForm) pairForm.addEventListener('submit', onPair)
  const signinBtn = $('#signin-btn'); if (signinBtn) signinBtn.addEventListener('click', onSignIn)
  const logoutBtn = $('#logout-btn'); if (logoutBtn) logoutBtn.addEventListener('click', onLogout)
  const unpairBtn = $('#unpair-btn'); if (unpairBtn) unpairBtn.addEventListener('click', onUnpair)
  const copyBtn = $('#copy-subkey'); if (copyBtn) copyBtn.addEventListener('click', onCopy)
  render().catch((e) => status(e.message, 'err'))
})