Skip to content

Commit 2c095dd

Browse files
authored
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
1 parent 664ba7b commit 2c095dd

File tree

6 files changed

+195
-48
lines changed

6 files changed

+195
-48
lines changed

apps/api/src/controllers/ticket.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,10 @@ export function ticketRoutes(fastify: FastifyInstance) {
199199
});
200200

201201
await sendAssignedEmail(assgined!.email);
202+
203+
const user = await checkSession(request);
204+
205+
await assignedNotification(engineer, ticket, user);
202206
}
203207

204208
const webhook = await prisma.webhooks.findMany({
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterEnum
2+
ALTER TYPE "TicketStatus" ADD VALUE 'hold';

apps/api/src/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,7 @@ enum Hook {
417417
}
418418

419419
enum TicketStatus {
420+
hold
420421
needs_support
421422
in_progress
422423
in_review

apps/client/components/TicketDetails/index.tsx

Lines changed: 50 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { useUser } from "../../store/session";
6464
import { ClientCombo, IconCombo, UserCombo } from "../Combo";
6565

6666
const ticketStatusMap = [
67+
{ id: 0, value: "hold", name: "Hold", icon: CircleDotDashed },
6768
{ id: 1, value: "needs_support", name: "Needs Support", icon: LifeBuoy },
6869
{ id: 2, value: "in_progress", name: "In Progress", icon: CircleDotDashed },
6970
{ id: 3, value: "in_review", name: "In Review", icon: Loader },
@@ -975,45 +976,46 @@ export default function Ticket() {
975976
)}
976977
</Button>
977978

978-
{data.ticket.following.length > 0 && (
979-
<div className="flex space-x-2">
980-
<Popover>
981-
<PopoverTrigger>
982-
<PanelTopClose className="h-4 w-4" />
983-
</PopoverTrigger>
984-
<PopoverContent>
985-
<div className="flex flex-col space-y-1">
986-
<span className="text-xs">Followers</span>
987-
{data.ticket.following.map(
988-
(follower: any) => {
989-
const userMatch = users.find(
990-
(user) =>
991-
user.id === follower &&
992-
user.id !==
993-
data.ticket.assignedTo.id
994-
);
995-
console.log(userMatch);
996-
return userMatch ? (
997-
<div key={follower.id}>
998-
<span>{userMatch.name}</span>
999-
</div>
1000-
) : null;
1001-
}
1002-
)}
979+
{data.ticket.following &&
980+
data.ticket.following.length > 0 && (
981+
<div className="flex space-x-2">
982+
<Popover>
983+
<PopoverTrigger>
984+
<PanelTopClose className="h-4 w-4" />
985+
</PopoverTrigger>
986+
<PopoverContent>
987+
<div className="flex flex-col space-y-1">
988+
<span className="text-xs">Followers</span>
989+
{data.ticket.following.map(
990+
(follower: any) => {
991+
const userMatch = users.find(
992+
(user) =>
993+
user.id === follower &&
994+
user.id !==
995+
data.ticket.assignedTo.id
996+
);
997+
console.log(userMatch);
998+
return userMatch ? (
999+
<div key={follower.id}>
1000+
<span>{userMatch.name}</span>
1001+
</div>
1002+
) : null;
1003+
}
1004+
)}
10031005

1004-
{data.ticket.following.filter(
1005-
(follower: any) =>
1006-
follower !== data.ticket.assignedTo.id
1007-
).length === 0 && (
1008-
<span className="text-xs">
1009-
This issue has no followers
1010-
</span>
1011-
)}
1012-
</div>
1013-
</PopoverContent>
1014-
</Popover>
1015-
</div>
1016-
)}
1006+
{data.ticket.following.filter(
1007+
(follower: any) =>
1008+
follower !== data.ticket.assignedTo.id
1009+
).length === 0 && (
1010+
<span className="text-xs">
1011+
This issue has no followers
1012+
</span>
1013+
)}
1014+
</div>
1015+
</PopoverContent>
1016+
</Popover>
1017+
</div>
1018+
)}
10171019
</div>
10181020
</div>
10191021
<div>
@@ -1110,15 +1112,16 @@ export default function Ticket() {
11101112
<span className="text-xs lowercase">
11111113
{moment(comment.createdAt).format("LLL")}
11121114
</span>
1113-
{comment.user &&
1114-
comment.userId === user.id && (
1115-
<Trash2
1116-
className="h-4 w-4 absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer text-muted-foreground hover:text-destructive"
1117-
onClick={() => {
1118-
deleteComment(comment.id);
1119-
}}
1120-
/>
1121-
)}
1115+
{(user.isAdmin ||
1116+
(comment.user &&
1117+
comment.userId === user.id)) && (
1118+
<Trash2
1119+
className="h-4 w-4 absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer text-muted-foreground hover:text-destructive"
1120+
onClick={() => {
1121+
deleteComment(comment.id);
1122+
}}
1123+
/>
1124+
)}
11221125
</div>
11231126
<span className="ml-1">{comment.text}</span>
11241127
</li>

apps/client/layouts/settings.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { classNames } from "@/shadcn/lib/utils";
22
import { SidebarProvider } from "@/shadcn/ui/sidebar";
3-
import { Bell, Flag, KeyRound } from "lucide-react";
3+
import { Bell, Flag, KeyRound, SearchSlashIcon } from "lucide-react";
44
import useTranslation from "next-translate/useTranslation";
55
import Link from "next/link";
66
import { useRouter } from "next/router";
@@ -57,6 +57,19 @@ export default function Settings({ children }) {
5757
<Flag className="flex-shrink-0 h-5 w-5 text-foreground" />
5858
<span>Feature Flags</span>
5959
</Link>
60+
61+
<Link
62+
href="/settings/sessions"
63+
className={classNames(
64+
router.pathname === "/settings/sessions"
65+
? "bg-secondary dark:bg-primary"
66+
: "hover:bg-[#F0F3F9] dark:hover:bg-white dark:hover:text-gray-900 ",
67+
"group flex items-center gap-x-3 py-2 px-3 rounded-md text-sm font-semibold leading-6"
68+
)}
69+
>
70+
<SearchSlashIcon className="flex-shrink-0 h-5 w-5 text-foreground" />
71+
<span>Sessions</span>
72+
</Link>
6073
</nav>
6174
</aside>
6275

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { toast } from "@/shadcn/hooks/use-toast";
2+
import { Button } from "@/shadcn/ui/button";
3+
import { getCookie } from "cookies-next";
4+
import { useEffect, useState } from "react";
5+
6+
interface Session {
7+
id: string;
8+
userAgent: string;
9+
ipAddress: string;
10+
createdAt: string;
11+
expires: string;
12+
}
13+
14+
function getPrettyUserAgent(userAgent: string) {
15+
// Extract browser and OS
16+
const browser =
17+
userAgent
18+
.match(/(Chrome|Safari|Firefox|Edge)\/[\d.]+/)?.[0]
19+
.split("/")[0] ?? "Unknown Browser";
20+
const os = userAgent.match(/\((.*?)\)/)?.[1].split(";")[0] ?? "Unknown OS";
21+
22+
return `${browser} on ${os}`;
23+
}
24+
25+
export default function Sessions() {
26+
const [sessions, setSessions] = useState<Session[]>([]);
27+
28+
const fetchSessions = async () => {
29+
try {
30+
const response = await fetch("/api/v1/auth/sessions", {
31+
headers: {
32+
Authorization: `Bearer ${getCookie("session")}`,
33+
},
34+
});
35+
if (!response.ok) {
36+
throw new Error("Failed to fetch sessions");
37+
}
38+
const data = await response.json();
39+
setSessions(data.sessions);
40+
} catch (error) {
41+
console.error("Error fetching sessions:", error);
42+
43+
toast({
44+
variant: "destructive",
45+
title: "Error fetching sessions",
46+
description: "Please try again later",
47+
});
48+
}
49+
};
50+
51+
useEffect(() => {
52+
fetchSessions();
53+
}, []);
54+
55+
const revokeSession = async (sessionId: string) => {
56+
try {
57+
const response = await fetch(`/api/v1/auth/sessions/${sessionId}`, {
58+
headers: {
59+
Authorization: `Bearer ${getCookie("session")}`,
60+
},
61+
method: "DELETE",
62+
});
63+
64+
if (!response.ok) {
65+
throw new Error("Failed to revoke session");
66+
}
67+
68+
toast({
69+
title: "Session revoked",
70+
description: "The session has been revoked",
71+
});
72+
73+
fetchSessions();
74+
} catch (error) {
75+
console.error("Error revoking session:", error);
76+
}
77+
};
78+
79+
return (
80+
<div className="p-6">
81+
<div className="flex flex-col space-y-1 mb-4">
82+
<h1 className="text-2xl font-bold">Active Sessions</h1>
83+
<span className="text-sm text-foreground">
84+
Devices you are logged in to
85+
</span>
86+
</div>
87+
<div className="space-y-4">
88+
{sessions &&
89+
sessions.map((session) => (
90+
<div
91+
key={session.id}
92+
className="flex flex-row items-center justify-between p-4 border rounded-lg group"
93+
>
94+
<div>
95+
<div className="text-base font-bold">
96+
{session.ipAddress === "::1"
97+
? "Localhost"
98+
: session.ipAddress}
99+
</div>
100+
<div className="font-bold text-xs">
101+
{getPrettyUserAgent(session.userAgent)}
102+
</div>
103+
<div className="text-xs text-foreground">
104+
Created: {new Date(session.createdAt).toLocaleString("en-GB")}
105+
</div>
106+
<div className="text-xs text-foreground">
107+
Expires: {new Date(session.expires).toLocaleString("en-GB")}
108+
</div>
109+
</div>
110+
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
111+
<Button
112+
size="sm"
113+
onClick={() => revokeSession(session.id)}
114+
variant="destructive"
115+
>
116+
Revoke
117+
</Button>
118+
</div>
119+
</div>
120+
))}
121+
</div>
122+
</div>
123+
);
124+
}

0 commit comments

Comments
 (0)