~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
125 lines · 4.2 KB Raw History
import { useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { Heading } from '#/components/atoms/heading.jsx';
import { Panel } from '#/components/atoms/panel.jsx';
import { ErrorNotice } from '#/components/molecules/error-notice.jsx';
import { TextField } from '#/components/molecules/text-field.jsx';
import { b64, unb64 } from '#/lib/base64.js';

export function SignInPage({ hostname, account = null, error = '', passkeys }) {
  const [err, setErr] = useState(error);
  const [busy, setBusy] = useState(false);

  const passkeySignIn = async () => {
    setErr('');
    setBusy(true);
    try {
      if (!window.PublicKeyCredential) {
        throw new Error('This browser has no passkey support.');
      }
      const start = await fetch('/account/passkey/challenge');
      const { challenge, rpId, error: startError } = await start.json();
      if (!start.ok) {
        throw new Error(startError || 'Could not start a passkey sign-in.');
      }
      const assertion = await navigator.credentials.get({
        publicKey: {
          challenge: unb64(challenge),
          rpId,
          userVerification: 'preferred',
          timeout: 120000,
        },
      });
      const response = await fetch('/account/passkey/sign-in', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          credentialId: b64(assertion.rawId),
          authenticatorData: b64(assertion.response.authenticatorData),
          clientDataJSON: b64(assertion.response.clientDataJSON),
          signature: b64(assertion.response.signature),
        }),
      });
      const result = await response.json();
      if (!response.ok)
        throw new Error(result.error || 'That passkey was refused.');
      window.location.href = '/account';
    } catch (caught) {
      setErr(
        caught?.name === 'NotAllowedError'
          ? 'Cancelled.'
          : String(caught?.message || caught),
      );
      setBusy(false);
    }
  };

  return (
    <div className="mx-auto flex min-h-svh max-w-md flex-col justify-center px-4 py-10">
      <Panel>
        <Heading>Sign in</Heading>
        <p className="mt-2 text-[13px] text-muted-foreground leading-relaxed">
          Sign in to the account hosted by{' '}
          <span className="text-foreground">{hostname}</span>.
        </p>

        <ErrorNotice error={err} className="mt-4" />

        <form
          method="POST"
          action="/account/sign-in"
          className="mt-4 flex flex-col gap-4"
        >
          {account ? (
            <div>
              <div className="text-[13px] text-muted-foreground">Account</div>
              <div className="mt-1 text-[14px] text-foreground">{account}</div>
              <input type="hidden" name="username" value={account} />
            </div>
          ) : (
            <TextField
              name="username"
              label="Identifier"
              placeholder="handle or DID"
              autoCapitalize="none"
              autoCorrect="off"
              spellCheck="false"
              autoComplete="username"
              required
              // biome-ignore lint/a11y/noAutofocus: the only editable field on a dedicated sign-in screen
              autoFocus
            />
          )}
          <TextField
            name="password"
            label="Password"
            type="password"
            autoComplete="current-password"
            required
            // biome-ignore lint/a11y/noAutofocus: with the account fixed, the password is the only field
            autoFocus={!!account}
          />
          <div className="rounded-xl border border-border bg-muted px-3.5 py-3 text-[12.5px] text-muted-foreground leading-relaxed">
            Verify the website address before entering your password. Only sign
            in on sites you recognize and trust.
          </div>
          <Button type="submit" size="block">
            Sign in
          </Button>
        </form>

        {passkeys && (
          <Button
            variant="outline"
            size="block"
            className="mt-3"
            disabled={busy}
            onClick={passkeySignIn}
          >
            Use a passkey
          </Button>
        )}
      </Panel>
    </div>
  );
}