132 lines
4.1 KiB
TypeScript
132 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import {proportional, pixel} from '@astryxdesign/core/Table';
|
|
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
import {HStack} from '@astryxdesign/core/Layout';
|
|
import {Text} from '@astryxdesign/core/Text';
|
|
import {Badge} from '@astryxdesign/core/Badge';
|
|
import {Button} from '@astryxdesign/core/Button';
|
|
import {useToast} from '@astryxdesign/core/Toast';
|
|
import {ResponsiveTable} from '@/shared/components/patterns/ResponsiveTable';
|
|
import {PanelCard} from '@/shared/components/patterns/PanelCard';
|
|
import {SkeletonRows} from '@/shared/components/patterns/LoadingState';
|
|
import {EmptyPanel} from '@/shared/components/patterns/EmptyPanel';
|
|
import {useResource} from '@/shared/hooks/useResource';
|
|
import {securityRepository} from '@/features/settings/repositories/securityRepository';
|
|
import type {DeviceSession} from '@/features/settings/services/mapSession';
|
|
|
|
/**
|
|
* Every device signed in as the current person, from GET /api/auth/sessions.
|
|
*
|
|
* Shared by the merchant Security settings and the platform console's
|
|
* Settings: `/api/auth/*` is scoped to whoever is signed in, not to a tenant,
|
|
* so it answers for a platform admin exactly as it does for a merchant. One
|
|
* component so the two consoles cannot drift on what "sign out" means.
|
|
*
|
|
* Nothing optimistic: after a revoke the list is re-read, because the platform
|
|
* decides what is still live.
|
|
*/
|
|
export function ActiveSessionsPanel() {
|
|
const toast = useToast();
|
|
const sessions = useResource(securityRepository.sessions());
|
|
|
|
const handleRevokeSession = async (id: string) => {
|
|
const res = await securityRepository.revoke(id);
|
|
if (!res.ok) {
|
|
toast({type: 'error', body: res.message ?? 'Could not sign that device out.'});
|
|
return;
|
|
}
|
|
sessions.refetch();
|
|
toast({body: 'That device has been signed out.'});
|
|
};
|
|
|
|
const handleRevokeAllOther = async () => {
|
|
const res = await securityRepository.revokeOthers();
|
|
if (!res.ok) {
|
|
toast({type: 'error', body: res.message ?? 'Could not sign the other devices out.'});
|
|
return;
|
|
}
|
|
sessions.refetch();
|
|
toast({body: 'Every other device has been signed out.'});
|
|
};
|
|
|
|
const columns: TableColumn<DeviceSession & Record<string, unknown>>[] = [
|
|
{
|
|
key: 'device',
|
|
header: 'Device',
|
|
width: proportional(2),
|
|
renderCell: (row) => (
|
|
<HStack gap={2} vAlign="center">
|
|
<Text size="sm" weight="medium">
|
|
{row.device}
|
|
</Text>
|
|
{row.isCurrent ? <Badge variant="success" label="This browser" /> : null}
|
|
</HStack>
|
|
),
|
|
},
|
|
{
|
|
key: 'lastUsedAt',
|
|
header: 'Last used',
|
|
width: proportional(1.2),
|
|
renderCell: (row) => (
|
|
<Text size="sm" color="secondary">
|
|
{new Date(row.lastUsedAt).toLocaleString()}
|
|
</Text>
|
|
),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: 'Action',
|
|
align: 'center',
|
|
width: pixel(120),
|
|
// The current session CAN be revoked upstream, but doing it from this
|
|
// table signs you out mid-page with no warning. Signing out is what the
|
|
// account menu is for.
|
|
renderCell: (row) =>
|
|
row.isCurrent ? null : (
|
|
<HStack hAlign="center">
|
|
<Button
|
|
size="sm"
|
|
variant="ghost"
|
|
label="Sign out"
|
|
onClick={() => void handleRevokeSession(row.id)}
|
|
/>
|
|
</HStack>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<PanelCard
|
|
title="Active sessions"
|
|
subtitle="Every device signed in as you."
|
|
resource={sessions}
|
|
loading={<SkeletonRows count={3} />}
|
|
empty={
|
|
<EmptyPanel
|
|
icon="security"
|
|
title="No other devices"
|
|
description="Only this browser is signed in."
|
|
/>
|
|
}
|
|
actions={
|
|
<Button
|
|
variant="secondary"
|
|
size="sm"
|
|
label="Sign out everywhere else"
|
|
onClick={() => void handleRevokeAllOther()}
|
|
/>
|
|
}
|
|
>
|
|
{(rows) => (
|
|
<ResponsiveTable
|
|
columns={columns}
|
|
data={rows as (DeviceSession & Record<string, unknown>)[]}
|
|
idKey="id"
|
|
primaryKey="device"
|
|
/>
|
|
)}
|
|
</PanelCard>
|
|
);
|
|
}
|