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