chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { Dialog as BaseDialog } from '@base-ui/react/dialog';
import { useQuery } from '@tanstack/react-query';
import { MonitorIcon } 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 { Dialog } from '#/components/molecules/dialog.jsx';
import { ErrorNotice } from '#/components/molecules/error-notice.jsx';
import { ListRow } from '#/components/molecules/list-row.jsx';
import { Section } from '#/components/molecules/section.jsx';
import { ShowMore } from '#/components/molecules/show-more.jsx';
import { TextLink } from '#/components/molecules/text-link.jsx';
import { get, post } from '#/lib/api.js';
import { timeAgo } from '#/lib/format.js';
import { queryKeys, useApiMutation } from '#/lib/query.js';
import { formatUserAgent } from '#/lib/userAgent.js';
const SESSIONS_SHOWN = 5;
export function SessionsCard() {
const { data, error } = useQuery({
queryKey: queryKeys.sessions,
queryFn: () => get('/sessions'),
});
const [showAll, setShowAll] = useState(false);
const action = useApiMutation([
queryKeys.sessions,
queryKeys.apps,
queryKeys.overview,
]);
const revoke = (jti) =>
action.run(async () => {
await post('/sessions/revoke', { jti });
});
const revokeAll = () =>
action.run(async () => {
await post('/sessions/revoke-all');
});
if (data && !data.supported) return null;
const sessions = data?.sessions ?? [];
const shown = showAll ? sessions : sessions.slice(0, SESSIONS_SHOWN);
return (
<Panel>
<Section
title="Where you're signed in"
description={
data ? `${data.sessions.length} logins have access right now.` : ' '
}
action={
sessions.length > 1 && (
<Dialog
title="Sign out all logins?"
trigger={<TextLink tone="danger">Sign out all</TextLink>}
>
<div className="space-y-3 text-[13px] text-muted-foreground leading-relaxed">
<p>
This ends all {sessions.length} current logins. You'll stay
signed in on this page.
</p>
<p>
It won't fully lock an app out on its own: one that still has
your account password can just sign in again, and access it
already holds can linger up to about two hours until its token
can no longer refresh.
</p>
<p className="text-foreground">
To remove an app's access for good, change your account
password first, then sign out.
</p>
</div>
<div className="mt-5 flex justify-end gap-2">
<BaseDialog.Close
render={
<Button variant="ghost" size="sm">
Cancel
</Button>
}
/>
<BaseDialog.Close
render={
<Button variant="destructive" size="sm" onClick={revokeAll}>
Sign out all
</Button>
}
/>
</div>
</Dialog>
)
}
>
<ErrorNotice error={error?.message || action.error} className="mb-4" />
<div className="flex flex-col gap-1">
{shown.map((session) => {
const device = formatUserAgent(session.userAgent);
return (
<ListRow
key={session.jti}
variant="plain"
media={
<RowIcon>
<MonitorIcon />
</RowIcon>
}
title={
<>
{session.label}
{session.appPassword && <Badge>limited</Badge>}
</>
}
description={
<>
{device && <span className="block">{device}</span>}
<span className="block">
Signed in {timeAgo(session.createdAt)}
{session.refreshedAt
? ` · renewed ${timeAgo(session.refreshedAt)}`
: ''}
</span>
</>
}
actions={
<Button
variant="outline"
size="sm"
disabled={action.busy}
onClick={() => revoke(session.jti)}
>
Sign out
</Button>
}
/>
);
})}
{sessions.length === 0 && (
<p className="px-1 py-2 text-[13px] text-muted-foreground">
No password logins are open.
</p>
)}
</div>
<ShowMore
total={sessions.length}
cap={SESSIONS_SHOWN}
expanded={showAll}
onToggle={() => setShowAll((v) => !v)}
noun="logins"
/>
</Section>
</Panel>
);
}