OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / lib / web / pages / devices.js

93 lines · javascript ref:
/**
 * Device management — `/devices`.
 *
 * Web UI for the device-attestation surface that was previously CLI-only
 * (`pgh device invite/list/revoke/label`). Lists the devices linked to
 * your identity, mints link invites, and revokes/labels devices.
 *
 * The `join` side (accepting an invite on a NEW device) stays a CLI
 * ceremony (`pgh device join <code>`) — it's an interactive blind-pairing
 * over the swarm, not a web form.
 */

import { layout, escapeHtml } from '../helpers.js'

export async function renderDevices (svc, ctx) {
  const prefix = (ctx && ctx.prefix) || ''
  let devices = []
  try {
    const r = await svc.dispatch('list-my-devices', {}, ctx)
    devices = (r && r.devices) || []
  } catch (err) {
    if (err && (err.status === 401 || err.status === 403)) {
      return layout('devices', '/devices', `
<div class="card"><h2>Devices</h2>
  <div class="meta">Sign in to manage the devices linked to your identity.</div>
</div>`, ctx)
    }
  }

  const fmt = (t) => t ? new Date(t).toISOString().slice(0, 10) : '—'
  const body = `
<div class="card">
  <h2>Your devices</h2>
  <div class="meta" style="margin-bottom:0.5rem;font-size:0.8rem">
    Devices attested to your identity chain. Each can act as you on this forge. Revoke any you no longer control.
  </div>
  ${devices.length === 0 ? '<div class="empty">(no devices linked yet)</div>' : `
  <div class="member-list">
    ${devices.map(d => {
      const pk = String(d.publicKey || '')
      const short = pk.slice(0, 24)
      const badge = d.revoked
        ? '<span class="badge badge-closed">revoked</span>'
        : '<span class="badge badge-open">active</span>'
      return `
    <div class="member-row" style="display:flex;align-items:flex-start;gap:0.75rem;padding:0.5rem 0;border-bottom:1px solid var(--border,#222)">
      <div style="flex:1">
        <div>${badge} ${d.label ? '<strong>' + escapeHtml(d.label) + '</strong>' : '<span class="meta">(unlabeled)</span>'}</div>
        <div class="mono" style="font-size:0.7rem">${escapeHtml(short)}${pk.length > 24 ? '…' : ''}</div>
        <div class="meta" style="font-size:0.7rem">method: ${escapeHtml(String(d.method || 'unknown'))} · linked: ${fmt(d.firstLinkedAt)} · last seen: ${fmt(d.lastSeenAt)}</div>
      </div>
      ${d.revoked ? '' : `
      <div style="display:flex;flex-direction:column;gap:0.3rem;align-items:flex-end">
        <form action="${prefix}/devices/label" method="POST" style="margin:0;display:flex;gap:0.3rem">
          <input type="hidden" name="devicePubkey" value="${escapeHtml(pk)}">
          <input type="text" name="label" placeholder="label" value="${escapeHtml(String(d.label || ''))}" style="font-size:0.7rem;width:8rem">
          <button class="btn-secondary" type="submit" style="font-size:0.7rem;padding:0.2rem 0.5rem">rename</button>
        </form>
        <form action="${prefix}/devices/revoke" method="POST" style="margin:0" onsubmit="return confirm('Revoke device ${escapeHtml(short)}…? It will no longer be able to act as you.')">
          <input type="hidden" name="devicePubkey" value="${escapeHtml(pk)}">
          <button class="btn-secondary" type="submit" style="font-size:0.7rem;padding:0.2rem 0.5rem">revoke</button>
        </form>
      </div>`}
    </div>`
    }).join('')}
  </div>`}
</div>

<div class="card">
  <h2>Link a new device</h2>
  <div class="meta" style="margin-bottom:0.5rem;font-size:0.8rem">
    Generates a one-time invite code. On the new device run <code>pgh device join &lt;code&gt;</code> — it pairs over the swarm and is attested to your identity chain. A <em>mirror</em> invite also carries the deployment bundle so a sister daemon joins the same autobases (HA).
  </div>
  <form action="${prefix}/devices/invite" method="POST">
    <label style="font-size:0.8rem"><input type="checkbox" name="mirror" value="1"> mirror invite (sister daemon / HA)</label>
    <div style="margin-top:0.5rem"><button type="submit">Generate device invite</button></div>
  </form>
</div>`
  return layout('devices', '/devices', body, ctx)
}

export function renderDeviceInviteResult (inviteHex, mirror, ctx) {
  const prefix = (ctx && ctx.prefix) || ''
  const body = `
<h2><a href="${prefix}/devices">&larr; devices</a> / invite</h2>
<div class="card">
  <h2 style="margin-top:0">Device invite created${mirror ? ' (mirror)' : ''}</h2>
  <div class="meta" style="margin-bottom:0.5rem">On the new device, run <code>pgh device join &lt;code&gt;</code>. The code is one-time and expires in 10 minutes.</div>
  <textarea readonly rows="4" style="width:100%;font-family:monospace;font-size:0.8rem" onclick="this.select()">${escapeHtml(inviteHex)}</textarea>
</div>`
  return layout('device invite', '/devices', body, ctx)
}