Skip to content

Commit

Permalink
feat: session management in ui (#416)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
potts99 authored Nov 16, 2024
1 parent 664ba7b commit 2c095dd
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 48 deletions.
4 changes: 4 additions & 0 deletions apps/api/src/controllers/ticket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "TicketStatus" ADD VALUE 'hold';
1 change: 1 addition & 0 deletions apps/api/src/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,7 @@ enum Hook {
}

enum TicketStatus {
hold
needs_support
in_progress
in_review
Expand Down
97 changes: 50 additions & 47 deletions apps/client/components/TicketDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -975,45 +976,46 @@ export default function Ticket() {
)}
</Button>

{data.ticket.following.length > 0 && (
<div className="flex space-x-2">
<Popover>
<PopoverTrigger>
<PanelTopClose className="h-4 w-4" />
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col space-y-1">
<span className="text-xs">Followers</span>
{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 ? (
<div key={follower.id}>
<span>{userMatch.name}</span>
</div>
) : null;
}
)}
{data.ticket.following &&
data.ticket.following.length > 0 && (
<div className="flex space-x-2">
<Popover>
<PopoverTrigger>
<PanelTopClose className="h-4 w-4" />
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col space-y-1">
<span className="text-xs">Followers</span>
{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 ? (
<div key={follower.id}>
<span>{userMatch.name}</span>
</div>
) : null;
}
)}

{data.ticket.following.filter(
(follower: any) =>
follower !== data.ticket.assignedTo.id
).length === 0 && (
<span className="text-xs">
This issue has no followers
</span>
)}
</div>
</PopoverContent>
</Popover>
</div>
)}
{data.ticket.following.filter(
(follower: any) =>
follower !== data.ticket.assignedTo.id
).length === 0 && (
<span className="text-xs">
This issue has no followers
</span>
)}
</div>
</PopoverContent>
</Popover>
</div>
)}
</div>
</div>
<div>
Expand Down Expand Up @@ -1110,15 +1112,16 @@ export default function Ticket() {
<span className="text-xs lowercase">
{moment(comment.createdAt).format("LLL")}
</span>
{comment.user &&
comment.userId === user.id && (
<Trash2
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"
onClick={() => {
deleteComment(comment.id);
}}
/>
)}
{(user.isAdmin ||
(comment.user &&
comment.userId === user.id)) && (
<Trash2
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"
onClick={() => {
deleteComment(comment.id);
}}
/>
)}
</div>
<span className="ml-1">{comment.text}</span>
</li>
Expand Down
15 changes: 14 additions & 1 deletion apps/client/layouts/settings.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -57,6 +57,19 @@ export default function Settings({ children }) {
<Flag className="flex-shrink-0 h-5 w-5 text-foreground" />
<span>Feature Flags</span>
</Link>

<Link
href="/settings/sessions"
className={classNames(
router.pathname === "/settings/sessions"
? "bg-secondary dark:bg-primary"
: "hover:bg-[#F0F3F9] dark:hover:bg-white dark:hover:text-gray-900 ",
"group flex items-center gap-x-3 py-2 px-3 rounded-md text-sm font-semibold leading-6"
)}
>
<SearchSlashIcon className="flex-shrink-0 h-5 w-5 text-foreground" />
<span>Sessions</span>
</Link>
</nav>
</aside>

Expand Down
124 changes: 124 additions & 0 deletions apps/client/pages/settings/sessions.tsx
Original file line number Diff line number Diff line change
@@ -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<Session[]>([]);

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 (
<div className="p-6">
<div className="flex flex-col space-y-1 mb-4">
<h1 className="text-2xl font-bold">Active Sessions</h1>
<span className="text-sm text-foreground">
Devices you are logged in to
</span>
</div>
<div className="space-y-4">
{sessions &&
sessions.map((session) => (
<div
key={session.id}
className="flex flex-row items-center justify-between p-4 border rounded-lg group"
>
<div>
<div className="text-base font-bold">
{session.ipAddress === "::1"
? "Localhost"
: session.ipAddress}
</div>
<div className="font-bold text-xs">
{getPrettyUserAgent(session.userAgent)}
</div>
<div className="text-xs text-foreground">
Created: {new Date(session.createdAt).toLocaleString("en-GB")}
</div>
<div className="text-xs text-foreground">
Expires: {new Date(session.expires).toLocaleString("en-GB")}
</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity">
<Button
size="sm"
onClick={() => revokeSession(session.id)}
variant="destructive"
>
Revoke
</Button>
</div>
</div>
))}
</div>
</div>
);
}

0 comments on commit 2c095dd

Please sign in to comment.