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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
157 lines · 5.2 KB Raw History
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>
  );
}