From c4d20d4cfd7529416ea614e13b23f82eeb8b9306 Mon Sep 17 00:00:00 2001 From: xiften <108333567+not-ani@users.noreply.github.com> Date: Sun, 13 Oct 2024 22:27:20 -0600 Subject: [PATCH] feat(nextjs): added permissions for the platform --- .../project/[id]/(tasks)/tasks/_actions.ts | 1 + .../(tasks)/tasks/_components/flowchart.tsx | 1 + .../action-guides/_components/GuideCard.tsx | 28 ++--------- .../_components/GuideCardSkeleton.tsx | 49 +++++++++++++++++++ .../app/(home)/guides/action-guides/page.tsx | 16 +++++- apps/nextjs/src/app/(home)/page.tsx | 2 +- packages/api/src/permissions.ts | 48 ++++++++++++++++++ packages/api/src/router/action-guides.ts | 17 ++----- packages/api/src/router/events.ts | 7 +++ packages/api/src/router/projects.ts | 12 +++++ packages/api/src/router/tasks.ts | 13 +++++ 11 files changed, 153 insertions(+), 41 deletions(-) create mode 100644 apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCardSkeleton.tsx create mode 100644 packages/api/src/permissions.ts diff --git a/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_actions.ts b/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_actions.ts index 69b08bc..32521b7 100644 --- a/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_actions.ts +++ b/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_actions.ts @@ -46,6 +46,7 @@ const schema = z.object({ target: z.string(), }), ), + projectId: z.string(), }); type InputProps = z.infer; diff --git a/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_components/flowchart.tsx b/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_components/flowchart.tsx index f2084b7..6e5555b 100644 --- a/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_components/flowchart.tsx +++ b/apps/nextjs/src/app/(dashboard)/project/[id]/(tasks)/tasks/_components/flowchart.tsx @@ -75,6 +75,7 @@ export const Flowchart: React.FC = ({ function handleSubmit() { const data = { + projectId: projectId, tasks: nodes, edges: edges.map((edge) => { return { diff --git a/apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCard.tsx b/apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCard.tsx index 815f254..72b3a30 100644 --- a/apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCard.tsx +++ b/apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCard.tsx @@ -18,37 +18,17 @@ import { api } from "~/trpc/react"; import CreateActionGuide from "./create-action-guide"; export default function Guides() { - const [search, setSearch] = useState(); - const { data: actionGuides } = api.actionGuides.getActionGuides.useQuery({ - title: search ?? undefined, - }); - - const filteredGuides = useMemo(() => { - return actionGuides?.filter((guide) => { - const titleMatch = - guide.title ?? "".toLowerCase().includes(search?.toLowerCase() ?? ""); - return titleMatch; - }); - }, [search, actionGuides]); - - const handleSearch = (e: any) => { - setSearch(e.target.value); - }; + const [actionGuides] = api.actionGuides.getActionGuides.useSuspenseQuery(); return ( -
+

Action Guides

- +
- {filteredGuides?.map((guide) => ( + {actionGuides.map((guide) => ( diff --git a/apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCardSkeleton.tsx b/apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCardSkeleton.tsx new file mode 100644 index 0000000..04481fe --- /dev/null +++ b/apps/nextjs/src/app/(home)/guides/action-guides/_components/GuideCardSkeleton.tsx @@ -0,0 +1,49 @@ +import React from "react"; + +import { + Card, + CardContent, + CardFooter, + CardHeader, + CardTitle, +} from "@amaxa/ui/card"; +import { Input } from "@amaxa/ui/input"; +import { Skeleton } from "@amaxa/ui/skeleton"; + +const GuidesSkeleton = () => { + return ( +
+

Action Guides

+
+ + +
+
+ {[...Array(6)].map((_, index) => ( + + + + + + + + + + + + + + + + + ))} +
+
+ ); +}; + +export default GuidesSkeleton; diff --git a/apps/nextjs/src/app/(home)/guides/action-guides/page.tsx b/apps/nextjs/src/app/(home)/guides/action-guides/page.tsx index cf1ad67..26e004b 100644 --- a/apps/nextjs/src/app/(home)/guides/action-guides/page.tsx +++ b/apps/nextjs/src/app/(home)/guides/action-guides/page.tsx @@ -1,9 +1,21 @@ +import { Suspense } from "react"; + +import { checkAuth } from "~/lib/auth"; +import { api, HydrateClient } from "~/trpc/server"; import Guides from "./_components/GuideCard"; +import GuidesSkeleton from "./_components/GuideCardSkeleton"; + +export default async function Page() { + await checkAuth(); + void api.actionGuides.getActionGuides.prefetch(); -export default function Page() { return (
- + + }> + + +
); } diff --git a/apps/nextjs/src/app/(home)/page.tsx b/apps/nextjs/src/app/(home)/page.tsx index 2aabd66..bb9bde7 100644 --- a/apps/nextjs/src/app/(home)/page.tsx +++ b/apps/nextjs/src/app/(home)/page.tsx @@ -17,7 +17,7 @@ export default async function Page() { return (
-
+
diff --git a/packages/api/src/permissions.ts b/packages/api/src/permissions.ts new file mode 100644 index 0000000..2a018b0 --- /dev/null +++ b/packages/api/src/permissions.ts @@ -0,0 +1,48 @@ +import type { Session } from "@amaxa/auth"; + +export function isProjectStudent(projectId: string, session: Session | null) { + if (!session) return false; + if (session.user.role === "Admin") return true; + const projectPermissions = session.user.project_permissions; + if (!projectPermissions) return false; + return ( + projectPermissions[projectId] === "admin" || + projectPermissions[projectId] === "coach" || + projectPermissions[projectId] === "student" + ); +} + +/* + param projectId: string + @param session: Session | null + @returns boolean + + Returns true if the user has admin or coach privileges for the project +*/ +export function isProjectPrivileged( + projectId: string, + session: Session | null, +) { + if (!session) return false; + if (session.user.role === "Admin") return true; + const projectPermissions = session.user.project_permissions; + if (!projectPermissions) return false; + return ( + projectPermissions[projectId] === "admin" || + projectPermissions[projectId] === "coach" + ); +} + +export function isProjectAdmin(projectId: string, session: Session | null) { + if (!session) return false; + if (session.user.role === "Admin") return true; + const projectPermissions = session.user.project_permissions; + if (!projectPermissions) return false; + return projectPermissions[projectId] === "admin"; +} + +export function isAdmin(session: Session | null) { + if (!session) return false; + if (session.user.role === "Admin") return true; + return false; +} diff --git a/packages/api/src/router/action-guides.ts b/packages/api/src/router/action-guides.ts index aa1e4db..1c4e8df 100644 --- a/packages/api/src/router/action-guides.ts +++ b/packages/api/src/router/action-guides.ts @@ -6,20 +6,9 @@ import { guides } from "@amaxa/db/schema"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const actionGuideRouter = createTRPCRouter({ - getActionGuides: publicProcedure - .input( - z.object({ - title: z.string().optional(), - skillId: z.string().optional(), - }), - ) - .query(async ({ input, ctx }) => { - const { title } = input; - return await ctx.db.query.guides.findMany({ - where: (guides, { and, ilike }) => - and(title ? ilike(guides.title, `%${title}%`) : undefined), - }); - }), + getActionGuides: publicProcedure.query(async ({ ctx }) => { + return await ctx.db.query.guides.findMany({}); + }), create: protectedProcedure .input( diff --git a/packages/api/src/router/events.ts b/packages/api/src/router/events.ts index 0019637..d550727 100644 --- a/packages/api/src/router/events.ts +++ b/packages/api/src/router/events.ts @@ -1,7 +1,9 @@ +import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { createEventSchema, events } from "@amaxa/db/schema"; +import { isAdmin } from "../permissions"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const eventsRouter = createTRPCRouter({ @@ -21,6 +23,11 @@ export const eventsRouter = createTRPCRouter({ create: protectedProcedure .input(createEventSchema) .mutation(async ({ ctx, input }) => { + if (!isAdmin(ctx.session)) + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You do not have permissions to create an event", + }); await ctx.db.insert(events).values(input); }), }); diff --git a/packages/api/src/router/projects.ts b/packages/api/src/router/projects.ts index 31aa75f..52bc024 100644 --- a/packages/api/src/router/projects.ts +++ b/packages/api/src/router/projects.ts @@ -1,10 +1,12 @@ // import { and, ilike } from "@amaxa/db"; // import { Projects } from "@amaxa/db/schema"; +import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { eq } from "@amaxa/db"; import { createProjectSchema, Projects } from "@amaxa/db/schema"; +import { isAdmin, isProjectPrivileged } from "../permissions"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const projectsRouter = createTRPCRouter({ @@ -20,6 +22,11 @@ export const projectsRouter = createTRPCRouter({ create: protectedProcedure .input(createProjectSchema) .mutation(async ({ ctx, input }) => { + if (!isAdmin(ctx.session)) + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You do not have permissions to create a project", + }); await ctx.db.insert(Projects).values(input); }), forDashboard: protectedProcedure @@ -52,6 +59,11 @@ export const projectsRouter = createTRPCRouter({ ) .mutation(async ({ ctx, input }) => { const { id, ...update } = input; + if (!isProjectPrivileged(input.id, ctx.session)) + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You do not have permissions to update this project", + }); await ctx.db.update(Projects).set(update).where(eq(Projects.id, id)); }), }); diff --git a/packages/api/src/router/tasks.ts b/packages/api/src/router/tasks.ts index 8c3fa7c..3d9f59e 100644 --- a/packages/api/src/router/tasks.ts +++ b/packages/api/src/router/tasks.ts @@ -1,8 +1,10 @@ +import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { and, buildConflictUpdateColumns, eq, sql } from "@amaxa/db"; import { edges, statusValues, tasks } from "@amaxa/db/schema"; +import { isProjectStudent } from "../permissions"; import { createTRPCRouter, protectedProcedure } from "../trpc"; export const tasksRouter = createTRPCRouter({ @@ -100,6 +102,7 @@ export const tasksRouter = createTRPCRouter({ }), }), ), + projectId: z.string(), edges: z.array( z.object({ @@ -112,6 +115,11 @@ export const tasksRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + if (!isProjectStudent(input.projectId, ctx.session)) + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You do not have permissions to create an event", + }); const formattedTasks = input.tasks.map((task) => ({ id: task.id, type: task.type, @@ -169,6 +177,11 @@ export const tasksRouter = createTRPCRouter({ }), ) .mutation(async ({ ctx, input }) => { + if (!isProjectStudent(input.projectId, ctx.session)) + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You do not have permissions to create an event", + }); await ctx.db.insert(tasks).values(input); }), update: protectedProcedure