From 2c095dd12245d9a639853e53ba50b6edb996723c Mon Sep 17 00:00:00 2001 From: Jack Andrews Date: Sat, 16 Nov 2024 13:02:41 +0000 Subject: [PATCH] feat: session management in ui (#416) * patch: issue deletion * feat: update client * chore: admin can delete any comment (#413) * chore: add on hold status (#415) * feat: follow an issue (#414) * patch: issue deletion * feat: update client * feat: follow an issue * feat: notifications when following * feat: see who is subscribed to this issue * patch: on hold * patch: migratiom * patch: fix notififaction * patch: remove dupe code * patch: fix null check * patch: remove code * feat: session management --- apps/api/src/controllers/ticket.ts | 4 + .../20241116014522_hold/migration.sql | 2 + apps/api/src/prisma/schema.prisma | 1 + .../client/components/TicketDetails/index.tsx | 97 +++++++------- apps/client/layouts/settings.tsx | 15 ++- apps/client/pages/settings/sessions.tsx | 124 ++++++++++++++++++ 6 files changed, 195 insertions(+), 48 deletions(-) create mode 100644 apps/api/src/prisma/migrations/20241116014522_hold/migration.sql create mode 100644 apps/client/pages/settings/sessions.tsx diff --git a/apps/api/src/controllers/ticket.ts b/apps/api/src/controllers/ticket.ts index a3bc8b7cc..27ec2e573 100644 --- a/apps/api/src/controllers/ticket.ts +++ b/apps/api/src/controllers/ticket.ts @@ -199,6 +199,10 @@ export function ticketRoutes(fastify: FastifyInstance) { }); await sendAssignedEmail(assgined!.email); + + const user = await checkSession(request); + + await assignedNotification(engineer, ticket, user); } const webhook = await prisma.webhooks.findMany({ diff --git a/apps/api/src/prisma/migrations/20241116014522_hold/migration.sql b/apps/api/src/prisma/migrations/20241116014522_hold/migration.sql new file mode 100644 index 000000000..720977a52 --- /dev/null +++ b/apps/api/src/prisma/migrations/20241116014522_hold/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "TicketStatus" ADD VALUE 'hold'; diff --git a/apps/api/src/prisma/schema.prisma b/apps/api/src/prisma/schema.prisma index 686f6d6bd..040ed6460 100644 --- a/apps/api/src/prisma/schema.prisma +++ b/apps/api/src/prisma/schema.prisma @@ -417,6 +417,7 @@ enum Hook { } enum TicketStatus { + hold needs_support in_progress in_review diff --git a/apps/client/components/TicketDetails/index.tsx b/apps/client/components/TicketDetails/index.tsx index ba175d9c9..79bf9ce31 100644 --- a/apps/client/components/TicketDetails/index.tsx +++ b/apps/client/components/TicketDetails/index.tsx @@ -64,6 +64,7 @@ import { useUser } from "../../store/session"; import { ClientCombo, IconCombo, UserCombo } from "../Combo"; const ticketStatusMap = [ + { id: 0, value: "hold", name: "Hold", icon: CircleDotDashed }, { id: 1, value: "needs_support", name: "Needs Support", icon: LifeBuoy }, { id: 2, value: "in_progress", name: "In Progress", icon: CircleDotDashed }, { id: 3, value: "in_review", name: "In Review", icon: Loader }, @@ -975,45 +976,46 @@ export default function Ticket() { )} - {data.ticket.following.length > 0 && ( -
- - - - - -
- Followers - {data.ticket.following.map( - (follower: any) => { - const userMatch = users.find( - (user) => - user.id === follower && - user.id !== - data.ticket.assignedTo.id - ); - console.log(userMatch); - return userMatch ? ( -
- {userMatch.name} -
- ) : null; - } - )} + {data.ticket.following && + data.ticket.following.length > 0 && ( +
+ + + + + +
+ Followers + {data.ticket.following.map( + (follower: any) => { + const userMatch = users.find( + (user) => + user.id === follower && + user.id !== + data.ticket.assignedTo.id + ); + console.log(userMatch); + return userMatch ? ( +
+ {userMatch.name} +
+ ) : null; + } + )} - {data.ticket.following.filter( - (follower: any) => - follower !== data.ticket.assignedTo.id - ).length === 0 && ( - - This issue has no followers - - )} -
-
-
-
- )} + {data.ticket.following.filter( + (follower: any) => + follower !== data.ticket.assignedTo.id + ).length === 0 && ( + + This issue has no followers + + )} +
+
+
+
+ )}
@@ -1110,15 +1112,16 @@ export default function Ticket() { {moment(comment.createdAt).format("LLL")} - {comment.user && - comment.userId === user.id && ( - { - deleteComment(comment.id); - }} - /> - )} + {(user.isAdmin || + (comment.user && + comment.userId === user.id)) && ( + { + deleteComment(comment.id); + }} + /> + )}
{comment.text} 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")} +
+
+
+ +
+
+ ))} +
+
+ ); +}