diff --git a/backend/notification/notificationSub.delete.route.ts b/backend/notification/notificationSub.delete.route.ts index 6ce9821..702aa78 100644 --- a/backend/notification/notificationSub.delete.route.ts +++ b/backend/notification/notificationSub.delete.route.ts @@ -3,12 +3,12 @@ import { type Response, Router } from "express"; import z from "zod"; import { db } from "../db/db.controller.js"; import { notificationSub } from "../db/schema/notifications.sub.schema.js"; +import { auth } from "../utils/auth.utils.js"; import { apiReturn } from "../utils/returnHelper.utils.js"; import { tryCatch } from "../utils/trycatch.utils.js"; import { modifiedNotification } from "./notification.controller.js"; const newSubscribe = z.object({ - emails: z.email().array().describe("An array of emails"), userId: z.string().describe("User id."), notificationId: z.string().describe("Notification id"), }); @@ -16,14 +16,26 @@ const newSubscribe = z.object({ const r = Router(); r.delete("/", async (req, res: Response) => { + const hasPermissions = await auth.api.userHasPermission({ + body: { + //userId: req?.user?.id, + role: req.user?.roles as any, + permissions: { + notifications: ["readAll"], // This must match the structure in your access control + }, + }, + }); + try { const validated = newSubscribe.parse(req.body); + const { data, error } = await tryCatch( db .delete(notificationSub) .where( and( - eq(notificationSub.userId, validated.userId), + //eq(notificationSub.userId, hasPermissions ? validated.userId : req?.user?.id ?? ""), // allows the admin to delete this + eq(notificationSub.userId, req?.user?.id ?? ""), eq(notificationSub.notificationId, validated.notificationId), ), ) @@ -44,6 +56,18 @@ r.delete("/", async (req, res: Response) => { }); } + if (data.length <= 0) { + return apiReturn(res, { + success: false, + level: "info", + module: "notification", + subModule: "post", + message: `Subscription was not deleted invalid data sent over`, + data: data ?? [], + status: 200, + }); + } + return apiReturn(res, { success: true, level: "info", diff --git a/backend/notification/notificationSub.get.route.ts b/backend/notification/notificationSub.get.route.ts index e15b025..d601aef 100644 --- a/backend/notification/notificationSub.get.route.ts +++ b/backend/notification/notificationSub.get.route.ts @@ -21,12 +21,16 @@ r.get("/", async (req, res: Response) => { }, }); + if (userId !== "") { + hasPermissions.success = false; + } + const { data, error } = await tryCatch( db .select() .from(notificationSub) .where( - userId || !hasPermissions.success + !hasPermissions.success ? eq(notificationSub.userId, `${req?.user?.id ?? ""}`) : undefined, ), diff --git a/backend/notification/notificationSub.post.route.ts b/backend/notification/notificationSub.post.route.ts index b0f43a0..e9e30a6 100644 --- a/backend/notification/notificationSub.post.route.ts +++ b/backend/notification/notificationSub.post.route.ts @@ -25,8 +25,25 @@ r.post("/", async (req, res: Response) => { try { const validated = newSubscribe.parse(req.body); + const emails = validated.emails + .map((e) => e.trim().toLowerCase()) + .filter(Boolean); + + const uniqueEmails = [...new Set(emails)]; + const { data, error } = await tryCatch( - db.insert(notificationSub).values(validated).returning(), + db + .insert(notificationSub) + .values({ + userId: req?.user?.id ?? "", + notificationId: validated.notificationId, + emails: uniqueEmails, + }) + .onConflictDoUpdate({ + target: [notificationSub.userId, notificationSub.notificationId], + set: { emails: uniqueEmails }, + }) + .returning(), ); await modifiedNotification(validated.notificationId); diff --git a/frontend/src/lib/formSutff/Select.Field.tsx b/frontend/src/lib/formSutff/Select.Field.tsx index 87d9e15..f5e8444 100644 --- a/frontend/src/lib/formSutff/Select.Field.tsx +++ b/frontend/src/lib/formSutff/Select.Field.tsx @@ -42,9 +42,7 @@ export const SelectField = ({ > - + {options.map((option) => ( {option.label} diff --git a/frontend/src/routes/(auth)/-components/NotificationsSubCard.tsx b/frontend/src/routes/(auth)/-components/NotificationsSubCard.tsx index 6d4eb40..ffe4b78 100644 --- a/frontend/src/routes/(auth)/-components/NotificationsSubCard.tsx +++ b/frontend/src/routes/(auth)/-components/NotificationsSubCard.tsx @@ -1,4 +1,6 @@ import { useSuspenseQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { toast } from "sonner"; import { Card, CardContent, @@ -12,15 +14,29 @@ import { notifications } from "../../../lib/queries/notifications"; export default function NotificationsSubCard({ user }: any) { const { data } = useSuspenseQuery(notifications()); - const { data: ns } = useSuspenseQuery(notificationSubs(user.id)); + const { data: ns, refetch } = useSuspenseQuery(notificationSubs(user.id)); const form = useAppForm({ defaultValues: { notificationId: "", emails: [user.email], }, onSubmit: async ({ value }) => { + if (value.notificationId === "") { + toast.error("Please select a notification before trying to subscribe."); + return; + } const postD = { ...value, userId: user.id }; - console.log(postD); + + try { + const res = await axios.post("/lst/api/notification/sub", postD, { + withCredentials: true, + }); + + refetch(); + form.reset(); + } catch (error) { + console.error(error); + } }, }); @@ -32,8 +48,6 @@ export default function NotificationsSubCard({ user }: any) { })); } - console.log(ns); - return (
diff --git a/frontend/src/routes/(auth)/-components/NotificationsTable.tsx b/frontend/src/routes/(auth)/-components/NotificationsTable.tsx new file mode 100644 index 0000000..16e3a70 --- /dev/null +++ b/frontend/src/routes/(auth)/-components/NotificationsTable.tsx @@ -0,0 +1,120 @@ +import { useSuspenseQuery } from "@tanstack/react-query"; +import { createColumnHelper } from "@tanstack/react-table"; +import axios from "axios"; +import { Trash } from "lucide-react"; +import { toast } from "sonner"; +import { Button } from "../../../components/ui/button"; +import { Card, CardContent, CardHeader } from "../../../components/ui/card"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "../../../components/ui/tooltip"; +import { notificationSubs } from "../../../lib/queries/notificationSubs"; +import { notifications } from "../../../lib/queries/notifications"; +import LstTable from "../../../lib/tableStuff/LstTable"; +import SearchableHeader from "../../../lib/tableStuff/SearchableHeader"; + +type Notifications = { + id: string; + name: string; + emails: string; + description: string; + remove: unknown; +}; + +export default function NotificationsTable({ userId }: any) { + const { data: subs, refetch } = useSuspenseQuery(notificationSubs(userId)); + const { data: note } = useSuspenseQuery(notifications()); + const columnHelper = createColumnHelper(); + + // filter out the current + const notificationMap = Object.fromEntries(note.map((n: any) => [n.id, n])); + + const data = subs.map((sub: any) => ({ + ...sub, + name: notificationMap[sub.notificationId].name || null, + description: notificationMap[sub.notificationId].description || null, + emails: sub.emails ? sub.emails.join(",") : null, + })); + + const removeNotification = async (ns: any) => { + try { + const res = await axios.delete(`/lst/api/notification/sub`, { + withCredentials: true, + data: { + userId: ns.userId, + notificationId: ns.notificationId, + }, + }); + + if (res.data.success) { + toast.success(`Subscription removed`); + refetch(); + } else { + console.info(res); + toast.error(res.data.message); + } + } catch { + toast.error(`There was an error removing subscription.`); + } + }; + + const column = [ + columnHelper.accessor("name", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => i.getValue(), + }), + columnHelper.accessor("description", { + header: ({ column }) => ( + + ), + cell: (i) => ( + + + {i.getValue()?.length > 25 ? ( + {i.getValue().slice(0, 25)}... + ) : ( + {i.getValue()} + )} + + {i.getValue()} + + ), + }), + columnHelper.accessor("emails", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => i.getValue(), + }), + columnHelper.accessor("remove", { + header: ({ column }) => ( + + ), + filterFn: "includesString", + cell: (i) => { + return ( + + ); + }, + }), + ]; + return ( + + Subscriptions + + + + + ); +} diff --git a/frontend/src/routes/(auth)/user.profile.tsx b/frontend/src/routes/(auth)/user.profile.tsx index 6d1380b..19ecf9b 100644 --- a/frontend/src/routes/(auth)/user.profile.tsx +++ b/frontend/src/routes/(auth)/user.profile.tsx @@ -13,6 +13,7 @@ import { useAppForm } from "@/lib/formSutff"; import { Spinner } from "../../components/ui/spinner"; import ChangePassword from "./-components/ChangePassword"; import NotificationsSubCard from "./-components/NotificationsSubCard"; +import NotificationsTable from "./-components/NotificationsTable"; export const Route = createFileRoute("/(auth)/user/profile")({ beforeLoad: async () => { @@ -119,11 +120,24 @@ function RouteComponent() {
- - - You are not subscribed to any notifications. - - + + + Subscriptions + + +
+
+ +
+
+
+ + } + > + {session && } +
);