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")}
+
+
+
+
+
+
+ ))}
+
+
+ );
+}