diff --git a/apps/client/layouts/settings.tsx b/apps/client/layouts/settings.tsx index 5769b5919..2c996bff1 100644 --- a/apps/client/layouts/settings.tsx +++ b/apps/client/layouts/settings.tsx @@ -1,6 +1,6 @@ import { classNames } from "@/shadcn/lib/utils"; import { SidebarProvider } from "@/shadcn/ui/sidebar"; -import { Bell, Flag, KeyRound } from "lucide-react"; +import { Bell, Flag, KeyRound, SearchSlashIcon } from "lucide-react"; import useTranslation from "next-translate/useTranslation"; import Link from "next/link"; import { useRouter } from "next/router"; @@ -57,6 +57,19 @@ export default function Settings({ children }) { Feature Flags + + + + Sessions + diff --git a/apps/client/pages/settings/sessions.tsx b/apps/client/pages/settings/sessions.tsx new file mode 100644 index 000000000..ed89bc3a6 --- /dev/null +++ b/apps/client/pages/settings/sessions.tsx @@ -0,0 +1,124 @@ +import { toast } from "@/shadcn/hooks/use-toast"; +import { Button } from "@/shadcn/ui/button"; +import { getCookie } from "cookies-next"; +import { useEffect, useState } from "react"; + +interface Session { + id: string; + userAgent: string; + ipAddress: string; + createdAt: string; + expires: string; +} + +function getPrettyUserAgent(userAgent: string) { + // Extract browser and OS + const browser = + userAgent + .match(/(Chrome|Safari|Firefox|Edge)\/[\d.]+/)?.[0] + .split("/")[0] ?? "Unknown Browser"; + const os = userAgent.match(/\((.*?)\)/)?.[1].split(";")[0] ?? "Unknown OS"; + + return `${browser} on ${os}`; +} + +export default function Sessions() { + const [sessions, setSessions] = useState([]); + + const fetchSessions = async () => { + try { + const response = await fetch("/api/v1/auth/sessions", { + headers: { + Authorization: `Bearer ${getCookie("session")}`, + }, + }); + if (!response.ok) { + throw new Error("Failed to fetch sessions"); + } + const data = await response.json(); + setSessions(data.sessions); + } catch (error) { + console.error("Error fetching sessions:", error); + + toast({ + variant: "destructive", + title: "Error fetching sessions", + description: "Please try again later", + }); + } + }; + + useEffect(() => { + fetchSessions(); + }, []); + + const revokeSession = async (sessionId: string) => { + try { + const response = await fetch(`/api/v1/auth/sessions/${sessionId}`, { + headers: { + Authorization: `Bearer ${getCookie("session")}`, + }, + method: "DELETE", + }); + + if (!response.ok) { + throw new Error("Failed to revoke session"); + } + + toast({ + title: "Session revoked", + description: "The session has been revoked", + }); + + fetchSessions(); + } catch (error) { + console.error("Error revoking session:", error); + } + }; + + return ( +
+
+

Active Sessions

+ + Devices you are logged in to + +
+
+ {sessions && + sessions.map((session) => ( +
+
+
+ {session.ipAddress === "::1" + ? "Localhost" + : session.ipAddress} +
+
+ {getPrettyUserAgent(session.userAgent)} +
+
+ Created: {new Date(session.createdAt).toLocaleString("en-GB")} +
+
+ Expires: {new Date(session.expires).toLocaleString("en-GB")} +
+
+
+ +
+
+ ))} +
+
+ ); +}