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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
108 lines · 3.8 KB Raw History
import { useQuery } from '@tanstack/react-query';
import { KeySquareIcon } from 'lucide-react';
import { useState } from 'react';
import { Badge } from '#/components/atoms/badge.jsx';
import { Button } from '#/components/atoms/button.jsx';
import { Panel } from '#/components/atoms/panel.jsx';
import { RowIcon } from '#/components/atoms/row-icon.jsx';
import { CheckboxField } from '#/components/molecules/checkbox-field.jsx';
import { ErrorNotice } from '#/components/molecules/error-notice.jsx';
import { FormShell } from '#/components/molecules/form-shell.jsx';
import { ListRow } from '#/components/molecules/list-row.jsx';
import { Section } from '#/components/molecules/section.jsx';
import { TextField } from '#/components/molecules/text-field.jsx';
import { get, post } from '#/lib/api.js';
import { day } from '#/lib/format.js';
import { queryKeys, useApiMutation, useInvalidate } from '#/lib/query.js';

export function AppPasswordsCard() {
  const { data, error } = useQuery({
    queryKey: queryKeys.appPasswords,
    queryFn: () => get('/app-passwords'),
  });
  const action = useApiMutation([queryKeys.appPasswords]);
  const invalidate = useInvalidate();
  const [created, setCreated] = useState(null);

  const revoke = (name) =>
    action.run(async () => {
      await post('/app-passwords/revoke', { name });
      setCreated(null);
    });

  return (
    <Panel>
      <Section
        title="App passwords"
        description="Credentials you hand to a client instead of your account password."
      >
        <ErrorNotice error={error?.message || action.error} className="mb-4" />
        {created && (
          <div className="mb-4 rounded-2xl border border-border bg-muted p-4">
            <div className="text-[13px] text-muted-foreground">
              New app password <b className="text-foreground">{created.name}</b>
            </div>
            <code className="my-1.5 block break-all font-mono text-[19px] text-foreground tracking-wide">
              {created.password}
            </code>
            <div className="text-[12px] text-faint">
              Copy it now. Only its hash is stored, so it cannot be shown again.
            </div>
          </div>
        )}
        {data && data.passwords.length > 0 && (
          <div className="mb-4 flex flex-col gap-1">
            {data.passwords.map((entry) => (
              <ListRow
                key={entry.name}
                variant="plain"
                media={
                  <RowIcon>
                    <KeySquareIcon />
                  </RowIcon>
                }
                title={
                  <>
                    {entry.name}
                    {entry.privileged && <Badge>privileged</Badge>}
                  </>
                }
                description={`Created ${day(entry.createdAt)}`}
                actions={
                  <Button
                    variant="outline"
                    size="sm"
                    disabled={action.busy}
                    onClick={() => revoke(entry.name)}
                  >
                    Revoke
                  </Button>
                }
              />
            ))}
          </div>
        )}
        <FormShell
          submitLabel="Create app password"
          onSubmit={async (values) => {
            const result = await post('/app-passwords/create', {
              name: values.name,
              privileged: values.privileged != null,
            });
            setCreated(result);
            invalidate([queryKeys.appPasswords]);
          }}
        >
          <TextField
            name="name"
            label="Name"
            required
            maxLength={64}
            placeholder="e.g. phone client"
          />
          <CheckboxField name="privileged" label="Grant privileged access" />
        </FormShell>
      </Section>
    </Panel>
  );
}