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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
210 lines · 7.5 KB Raw History
import { useQuery } from '@tanstack/react-query';
import { useState } from 'react';
import { Button } from '#/components/atoms/button.jsx';
import { Heading } from '#/components/atoms/heading.jsx';
import { Notice } from '#/components/atoms/notice.jsx';
import { Panel } from '#/components/atoms/panel.jsx';
import { Skeleton } from '#/components/atoms/skeleton.jsx';
import { DetailList } from '#/components/molecules/detail-list.jsx';
import { FormShell } from '#/components/molecules/form-shell.jsx';
import { Section } from '#/components/molecules/section.jsx';
import { TextField } from '#/components/molecules/text-field.jsx';
import { get, post } from '#/lib/api.js';
import { queryKeys, useInvalidate } from '#/lib/query.js';

/**
 * The domain this PDS hosts, derived from its own endpoint. A handle that is
 * this domain or a subdomain of it is verified by the server itself.
 * @param {string} endpoint
 */
function pdsDomainOf(endpoint) {
  try {
    return new URL(endpoint).hostname;
  } catch {
    return '';
  }
}

/**
 * The rename control. A username has to be a domain you own, proven with DNS —
 * this reads what you type and shows the exact record before you submit. The
 * server's own address and its subdomains are special cases it calls out.
 * @param {{did: string, pdsDomain: string, onSucceed: () => void}} props
 */
function RenameForm({ did, pdsDomain, onSucceed }) {
  const [value, setValue] = useState('');
  const typed = value.trim().toLowerCase();
  const isSelf = !!pdsDomain && typed === pdsDomain;
  const isInternal =
    !!pdsDomain && typed !== pdsDomain && typed.endsWith(`.${pdsDomain}`);
  const isExternal = typed !== '' && !isSelf && !isInternal;

  return (
    <FormShell
      className="mt-5"
      submitLabel="Change username"
      onSubmit={({ handle }) => post('/identity/handle', { handle })}
      onSucceed={() => {
        setValue('');
        onSucceed();
      }}
    >
      <TextField
        name="handle"
        label="New username"
        autoCapitalize="none"
        autoCorrect="off"
        spellCheck={false}
        required
        value={value}
        onChange={(event) => setValue(event.target.value)}
        placeholder="name.example.com"
        hint={
          pdsDomain
            ? `A domain you own, verified with DNS. This server's address is ${pdsDomain}.`
            : 'A domain you own, e.g. name.example.com.'
        }
      />

      {isSelf && (
        <p className="text-[13px] text-muted-foreground leading-relaxed">
          <span className="font-mono text-foreground">{pdsDomain}</span> is this
          server's own address — it's already your username.
        </p>
      )}

      {isInternal && (
        <div className="space-y-2 rounded-2xl border border-border bg-muted px-4 py-3.5">
          <p className="text-[13px] text-muted-foreground leading-relaxed">
            <span className="font-mono text-foreground">{typed}</span> is a
            subdomain of this server. A single-user PDS only answers at{' '}
            <span className="font-mono text-foreground">{pdsDomain}</span>, so a
            name like this won't resolve unless you separately host it. To
            change your username, use a domain you own below.
          </p>
        </div>
      )}

      {isExternal && (
        <div className="space-y-3 rounded-2xl border border-border bg-muted px-4 py-3.5">
          <p className="text-[13px] text-muted-foreground leading-relaxed">
            <span className="font-mono text-foreground">{typed}</span> is a
            domain you own. Prove it points here first, with either one of
            these, then submit:
          </p>
          <div className="space-y-1.5">
            <div className="text-[11px] text-faint uppercase tracking-wide">
              DNS TXT record
            </div>
            <div className="break-all rounded-xl border border-border bg-background px-3 py-2 font-mono text-[12.5px] text-foreground">
              _atproto.{typed} → did={did}
            </div>
          </div>
          <div className="space-y-1.5">
            <div className="text-[11px] text-faint uppercase tracking-wide">
              or serve this URL
            </div>
            <div className="break-all rounded-xl border border-border bg-background px-3 py-2 font-mono text-[12.5px] text-foreground">
              https://{typed}/.well-known/atproto-did → {did}
            </div>
          </div>
        </div>
      )}
    </FormShell>
  );
}

export function IdentityPage() {
  const { data, error } = useQuery({
    queryKey: queryKeys.identity,
    queryFn: () => get('/identity'),
  });
  const invalidate = useInvalidate();

  if (error) {
    return <Notice variant="error">{error.message}</Notice>;
  }
  if (!data) {
    return (
      <div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-[1fr_360px]">
        <div className="space-y-5">
          <Skeleton className="h-40 rounded-3xl" />
          <Skeleton className="h-40 rounded-3xl" />
        </div>
        <div className="space-y-5">
          <Skeleton className="h-32 rounded-3xl" />
          <Skeleton className="h-32 rounded-3xl" />
        </div>
      </div>
    );
  }

  return (
    <div className="grid grid-cols-1 items-start gap-5 lg:grid-cols-[1fr_360px]">
      <div className="space-y-5">
        <Panel>
          <Section title="Your username">
            <div className="rounded-2xl border border-border bg-muted px-4 py-3.5 font-mono text-[15px] text-foreground">
              {data.handle ? `@${data.handle}` : 'none'}
            </div>
            <p className="mt-4 text-[13px] text-muted-foreground leading-relaxed">
              This is the name people see in every app. It points at your
              permanent ID, so you can change it without losing anything you
              have made.
            </p>
            {!data.readOnly && (
              <RenameForm
                did={data.did}
                pdsDomain={pdsDomainOf(data.pdsEndpoint)}
                onSucceed={() =>
                  invalidate([queryKeys.identity, queryKeys.overview])
                }
              />
            )}
          </Section>
        </Panel>

        <Panel>
          <Section title="Where this account is published">
            <DetailList
              rows={[
                ['PDS endpoint', data.pdsEndpoint],
                ['Directory', data.plcUrl],
                ['Signing key', data.signingKey || 'none'],
              ]}
            />
          </Section>
        </Panel>
      </div>

      <div className="space-y-5">
        <Panel>
          <Heading>Your permanent ID</Heading>
          <p className="mt-2 text-[13px] text-muted-foreground leading-relaxed">
            Never changes, even if you rename or move hosts. Apps follow this,
            not your username.
          </p>
          <div className="mt-4 break-all rounded-2xl border border-border bg-muted px-4 py-3.5 font-mono text-[12.5px] text-muted-foreground">
            {data.did}
          </div>
        </Panel>

        <Panel>
          <Heading>Change host</Heading>
          <p className="mt-2 text-[13px] text-muted-foreground leading-relaxed">
            Move your account, everything in it and your followers to another
            host. Your username and permanent ID come with you.
          </p>
          <Button
            variant="outline"
            size="block"
            disabled
            className="mt-4 text-[13.5px] text-faint"
          >
            Not available yet
          </Button>
        </Panel>
      </div>
    </div>
  );
}