Skip to content

Commit 1c2cb90

Browse files
feat: sessions section with logout one/all sessions (#460)
1 parent 3529c63 commit 1c2cb90

File tree

2 files changed

+113
-0
lines changed

2 files changed

+113
-0
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { type ModalComponent } from '@/src/hooks/use-awaitable-modal';
2+
import { api } from '@/src/lib/trpc';
3+
import { Button, Dialog } from '@radix-ui/themes';
4+
5+
export function DeleteAllSessions({
6+
open,
7+
onResolve,
8+
onClose,
9+
verificationToken
10+
}: ModalComponent<{ verificationToken: string }>) {
11+
const {
12+
mutateAsync: logoutAll,
13+
isLoading: loggingOut,
14+
error
15+
} = api.account.security.deleteAllSessions.useMutation();
16+
return (
17+
<Dialog.Root open={open}>
18+
<Dialog.Content className="w-full max-w-96 p-4">
19+
<Dialog.Title className="mx-auto w-fit">
20+
Delete All Sessions
21+
</Dialog.Title>
22+
<Dialog.Description className="mx-auto flex w-fit text-balance p-2 text-center text-sm font-bold">
23+
This will log you out of all devices and sessions including the
24+
current device. Are you sure you want to continue?
25+
</Dialog.Description>
26+
<div className="text-red-10 w-full">{error?.message}</div>
27+
<div className="flex flex-col gap-2">
28+
<Button
29+
color="red"
30+
onClick={async () => {
31+
await logoutAll({ verificationToken });
32+
onResolve(null);
33+
}}
34+
loading={loggingOut}>
35+
Logout of all sessions
36+
</Button>
37+
<Button
38+
color="gray"
39+
variant="soft"
40+
onClick={() => onClose()}>
41+
Cancel
42+
</Button>
43+
</div>
44+
</Dialog.Content>
45+
</Dialog.Root>
46+
);
47+
}

apps/web/src/app/[orgShortCode]/settings/user/security/page.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@ import { Trash } from 'lucide-react';
2424
import { format } from 'date-fns';
2525
import useLoading from '@/src/hooks/use-loading';
2626
import { startRegistration } from '@simplewebauthn/browser';
27+
import { useRouter } from 'next/navigation';
28+
import { DeleteAllSessions } from './_components/session-modals';
29+
import { useQueryClient } from '@tanstack/react-query';
2730
// import { PasskeyNameModal } from './_components/passkey-modals';
2831

2932
export default function Page() {
33+
const queryClient = useQueryClient();
34+
const router = useRouter();
35+
3036
const {
3137
data: initData,
3238
isLoading: isInitDataLoading,
@@ -82,6 +88,9 @@ export default function Page() {
8288
}
8389
);
8490

91+
const [DeleteAllSessionsModalRoot, openDeleteAllSessionsModal] =
92+
useAwaitableModal(DeleteAllSessions, { verificationToken: '' });
93+
8594
// const [PasskeyNameModalRoot, openPasskeyNameModal] = useAwaitableModal(
8695
// PasskeyNameModal,
8796
// {}
@@ -122,6 +131,9 @@ export default function Page() {
122131
}
123132
);
124133

134+
const { mutateAsync: logoutSingle } =
135+
api.account.security.deleteSession.useMutation();
136+
125137
async function waitForVerification() {
126138
if (!initData) throw new Error('No init data');
127139
if (verificationToken) return verificationToken;
@@ -310,6 +322,59 @@ export default function Page() {
310322
Add New Passkey
311323
</Button>
312324
</div>
325+
326+
<div className="flex flex-col gap-3">
327+
<span className="text-lg font-bold">Sessions</span>
328+
<div className="flex flex-wrap gap-2">
329+
{initData?.sessions.map((session) => (
330+
<div
331+
key={session.publicId}
332+
className="bg-muted flex items-center justify-center gap-2 rounded border px-2 py-1">
333+
<div className="flex flex-col">
334+
<span>
335+
{session.device} - {session.os}
336+
</span>
337+
<span className="text-muted-foreground text-xs">
338+
{format(session.createdAt, ' HH:mm, do MMM yyyy')}
339+
</span>
340+
</div>
341+
<div>
342+
<IconButton
343+
size="2"
344+
variant="soft"
345+
onClick={async () => {
346+
const token = await waitForVerification();
347+
if (!token) return;
348+
await logoutSingle({
349+
sessionPublicId: session.publicId,
350+
verificationToken: verificationToken ?? token
351+
})
352+
.then(() => refreshSecurityData())
353+
.catch(() => null);
354+
}}>
355+
<Trash size={16} />
356+
</IconButton>
357+
</div>
358+
</div>
359+
))}
360+
</div>
361+
<Button
362+
className="w-fit"
363+
onClick={async () => {
364+
const token = await waitForVerification();
365+
if (!token) return;
366+
await openDeleteAllSessionsModal({
367+
verificationToken: verificationToken ?? token
368+
})
369+
.then(() => {
370+
queryClient.removeQueries();
371+
router.replace('/');
372+
})
373+
.catch(() => null);
374+
}}>
375+
Logout of All Sessions
376+
</Button>
377+
</div>
313378
</div>
314379
)}
315380

@@ -319,6 +384,7 @@ export default function Page() {
319384
<RecoveryModalRoot />
320385
<DeletePasskeyModalRoot />
321386
{/* <PasskeyNameModalRoot /> */}
387+
<DeleteAllSessionsModalRoot />
322388
</Flex>
323389
);
324390
}

0 commit comments

Comments
 (0)