chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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>
);
}