diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index fd063652..db81f1e7 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -114,6 +114,7 @@ model File {
model ChapterRequest {
id String @id @default(auto()) @map("_id") @db.ObjectId
+ // ChapterRequest can only be in pending or accepted state. We don't use DENIED.
approved Approval @default(PENDING)
firstName String
lastName String
@@ -151,6 +152,7 @@ model Email {
model UserRequest {
id String @id @default(auto()) @map("_id") @db.ObjectId
+ // @deprecated - We ended up not using this anywhere
approved Approval @default(PENDING)
uid String @unique @db.ObjectId
chapterId String @db.ObjectId
diff --git a/src/app/api/handle-chapter-request/route.client.ts b/src/app/api/handle-chapter-request/route.client.ts
index 4b8bea4a..f3b30c94 100644
--- a/src/app/api/handle-chapter-request/route.client.ts
+++ b/src/app/api/handle-chapter-request/route.client.ts
@@ -3,69 +3,11 @@ import {
HandleChapterRequest,
HandleChapterRequestResponse,
} from "./route.schema";
+import { TypedRequest } from "@server/type";
-/**
- * Describe the interface of SignInRequest.
- */
-type IHandleChapterRequest = z.infer;
-
-type IHandleChapterRequestResponse = z.infer<
- typeof HandleChapterRequestResponse
->;
-
-/**
- * Extract all the values of "code".
- */
-type HandleChapterRequestResponseCode = z.infer<
- typeof HandleChapterRequestResponse
->["code"];
-
-/**
- * Extends the parameters of fetch() function to give types to the RequestBody.
- */
-interface IRequest extends Omit {
- body: IHandleChapterRequest;
-}
-
-const MOCK_SUCCESS: IHandleChapterRequestResponse = {
- code: "SUCCESS",
- message: "Chapter request successfully handled",
-};
-
-const MOCK_INVALID_REQUEST: IHandleChapterRequestResponse = {
- code: "INVALID_REQUEST",
- message: "Invalid API request",
-};
-
-const MOCK_UNKNOWN: IHandleChapterRequestResponse = {
- code: "UNKNOWN",
- message: "Unknown error received",
-};
-
-const MOCK_CHAPTER_REQUEST_NOT_FOUND: IHandleChapterRequestResponse = {
- code: "CHAPTER_REQUEST_NOT_FOUND",
- message: "A chapter request associated with the given ID does not exist",
-};
-
-/**
- * If "mock" is given as a parameter, the function can return mocked data for a specific case. This
- * pattern allows frontend developers to use your API before you finished implementing it!
- *
- * In addition, using Zod schemas to parse the response will make the input/output well-typed, making the code cleaner.
- */
export const handleChapterRequest = async (
- request: IRequest,
- mock?: HandleChapterRequestResponseCode
+ request: TypedRequest>
) => {
- if (mock === "SUCCESS") {
- return HandleChapterRequestResponse.parse(MOCK_SUCCESS);
- } else if (mock === "INVALID_REQUEST") {
- return HandleChapterRequestResponse.parse(MOCK_INVALID_REQUEST);
- } else if (mock === "UNKNOWN") {
- return HandleChapterRequestResponse.parse(MOCK_UNKNOWN);
- } else if (mock === "CHAPTER_REQUEST_NOT_FOUND") {
- return HandleChapterRequestResponse.parse(MOCK_CHAPTER_REQUEST_NOT_FOUND);
- }
const { body, ...options } = request;
const response = await fetch("/api/handle-chapter-request", {
method: "POST",
diff --git a/src/app/api/handle-chapter-request/route.schema.ts b/src/app/api/handle-chapter-request/route.schema.ts
index 146b8363..b57551e0 100644
--- a/src/app/api/handle-chapter-request/route.schema.ts
+++ b/src/app/api/handle-chapter-request/route.schema.ts
@@ -14,14 +14,4 @@ export const HandleChapterRequestResponse = z.discriminatedUnion("code", [
code: z.literal("INVALID_REQUEST"),
message: z.literal("Invalid API request"),
}),
- z.object({
- code: z.literal("UNKNOWN"),
- message: z.literal("Unknown error received"),
- }),
- z.object({
- code: z.literal("CHAPTER_REQUEST_NOT_FOUND"),
- message: z.literal(
- "A chapter request associated with the given ID does not exist"
- ),
- }),
]);
diff --git a/src/app/api/handle-chapter-request/route.ts b/src/app/api/handle-chapter-request/route.ts
index aee19033..fc549ac0 100644
--- a/src/app/api/handle-chapter-request/route.ts
+++ b/src/app/api/handle-chapter-request/route.ts
@@ -1,4 +1,4 @@
-import { NextRequest, NextResponse } from "next/server";
+import { NextResponse } from "next/server";
import {
HandleChapterRequest,
HandleChapterRequestResponse,
@@ -9,126 +9,89 @@ import { env } from "@env/server.mjs";
import { prisma } from "@server/db/client";
export const POST = withSession(async ({ req, session }) => {
- // Validate the data in the request
- // If the data is invalid, return a 400 response
- // with a JSON body containing the validation errors
+ const handleChapterRequest = HandleChapterRequest.safeParse(await req.json());
+ if (!handleChapterRequest.success) {
+ return NextResponse.json(
+ HandleChapterRequestResponse.parse({
+ code: "INVALID_REQUEST",
+ message: "Invalid API request",
+ }),
+ { status: 400 }
+ );
+ }
- // Validate a proper JSON was passed in as well
- try {
- const handleChapterRequest = HandleChapterRequest.safeParse(
- await req.json()
+ const body = handleChapterRequest.data;
+ const chapterRequest = await prisma.chapterRequest.findFirst({
+ where: {
+ id: body.chapterRequestId,
+ },
+ });
+ if (chapterRequest == null || chapterRequest.approved === "APPROVED") {
+ // If chapter request does not exist, it means that another admin has denied it.
+ // If chapter request has been approved, do nothing.
+ return NextResponse.json(
+ HandleChapterRequestResponse.parse({
+ code: "SUCCESS",
+ message: "Chapter request successfully handled",
+ })
+ );
+ } else if (!body.approved) {
+ await prisma.chapterRequest.delete({
+ where: {
+ id: body.chapterRequestId,
+ },
+ });
+ return NextResponse.json(
+ HandleChapterRequestResponse.parse({
+ code: "SUCCESS",
+ message: "Chapter request successfully handled",
+ }),
+ { status: 200 }
);
- if (!handleChapterRequest.success) {
- return NextResponse.json(
- HandleChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "Invalid API request",
- }),
- { status: 400 }
- );
- } else {
- const body = handleChapterRequest.data;
- // Check if the chapter request even exists
- const chapterRequest = await prisma.chapterRequest.findFirst({
- where: {
- id: String(body.chapterRequestId),
- },
- });
- if (!chapterRequest) {
- return NextResponse.json(
- HandleChapterRequestResponse.parse({
- code: "CHAPTER_REQUEST_NOT_FOUND",
- message:
- "A chapter request associated with the given ID does not exist",
- }),
- { status: 400 }
- );
- }
- // Check if the chapter request has already been approved/denied
- if (chapterRequest.approved !== "PENDING") {
- return NextResponse.json(
- {
- code: "INVALID_REQUEST",
- message: "Invalid API request",
- },
- { status: 400 }
- );
- }
- // If approved, create a new chapter and update approved field of chapter request
- if (body.approved === true) {
- const createdChapter = await prisma.chapter.create({
- data: {
- chapterName: chapterRequest.university,
- location: chapterRequest.universityAddress,
- },
- });
- await prisma.chapterRequest.update({
- where: {
- id: body.chapterRequestId,
- },
- data: {
- approved: "APPROVED",
- },
- });
+ } else {
+ // Don't delete chapterRequest on approve to store metadata
+ await prisma.chapterRequest.update({
+ where: { id: body.chapterRequestId },
+ data: { approved: "APPROVED" },
+ });
+
+ const createdChapter = await prisma.chapter.create({
+ data: {
+ chapterName: chapterRequest.university,
+ location: chapterRequest.universityAddress,
+ },
+ });
- const baseFolder = env.GOOGLE_BASEFOLDER; // TODO: make env variable
- const fileMetadata = {
- name: [`${chapterRequest.university}-${createdChapter.id}`],
- mimeType: "application/vnd.google-apps.folder",
- parents: [baseFolder],
- };
- const fileCreateData = {
- resource: fileMetadata,
- fields: "id",
- };
+ const baseFolder = env.GOOGLE_BASEFOLDER;
+ const fileMetadata = {
+ name: [`${chapterRequest.university}-${createdChapter.id}`],
+ mimeType: "application/vnd.google-apps.folder",
+ parents: [baseFolder],
+ };
+ const fileCreateData = {
+ resource: fileMetadata,
+ fields: "id",
+ };
- const service = await createDriveService(session.user.id);
- const file = await (
- service as NonNullable
- ).files.create(fileCreateData);
- const googleFolderId = (file as any).data.id;
+ const service = await createDriveService(session.user.id);
+ const file = await service.files.create(fileCreateData);
+ const googleFolderId = file.data.id as string;
- await prisma.chapter.update({
- where: {
- id: createdChapter.id,
- },
- data: {
- chapterFolder: googleFolderId,
- },
- });
+ await prisma.chapter.update({
+ where: {
+ id: createdChapter.id,
+ },
+ data: {
+ chapterFolder: googleFolderId,
+ },
+ });
- return NextResponse.json(
- HandleChapterRequestResponse.parse({
- code: "SUCCESS",
- message: "Chapter request successfully handled",
- }),
- { status: 200 }
- );
- }
- // If denied just updated approved field of chapter request
- await prisma.chapterRequest.update({
- where: {
- id: body.chapterRequestId,
- },
- data: {
- approved: "DENIED",
- },
- });
- return NextResponse.json(
- HandleChapterRequestResponse.parse({
- code: "SUCCESS",
- message: "Chapter request successfully handled",
- }),
- { status: 200 }
- );
- }
- } catch {
return NextResponse.json(
HandleChapterRequestResponse.parse({
- code: "UNKNOWN",
- message: "Unknown error received",
+ code: "SUCCESS",
+ message: "Chapter request successfully handled",
}),
- { status: 500 }
+ { status: 200 }
);
}
});
diff --git a/src/app/api/user-request/route.client.ts b/src/app/api/user-request/route.client.ts
index 81abb51a..2c3cd05b 100644
--- a/src/app/api/user-request/route.client.ts
+++ b/src/app/api/user-request/route.client.ts
@@ -5,17 +5,12 @@ import {
JoinChapterRequestResponse,
ManageChapterRequestResponse,
} from "./route.schema";
+import { TypedRequest } from "@server/type";
-interface IJoinChapterRequest extends Omit {
- body: z.infer;
-}
-
-interface IManageChapterRequest extends Omit {
- body: z.infer;
-}
+type AcceptOrDenyRequest = TypedRequest>;
export const handleJoinChapterRequest = async (
- request: IJoinChapterRequest
+ request: TypedRequest>
) => {
const { body, ...options } = request;
const response = await fetch("/api/user-request", {
@@ -28,10 +23,9 @@ export const handleJoinChapterRequest = async (
};
export const handleManageChapterRequest = async (
- request: IManageChapterRequest
+ request: AcceptOrDenyRequest
) => {
const { body, ...options } = request;
- console.log("body", body);
const response = await fetch("/api/user-request", {
method: "DELETE",
body: JSON.stringify(body),
@@ -42,7 +36,7 @@ export const handleManageChapterRequest = async (
};
export const handleAcceptChapterRequest = async (
- request: IManageChapterRequest
+ request: AcceptOrDenyRequest
) => {
const { body, ...options } = request;
const response = await fetch("/api/user-request", {
diff --git a/src/app/api/user-request/route.ts b/src/app/api/user-request/route.ts
index 23fbb16d..05d7712d 100644
--- a/src/app/api/user-request/route.ts
+++ b/src/app/api/user-request/route.ts
@@ -77,203 +77,146 @@ export const POST = withSession(async ({ req, session }) => {
});
export const DELETE = withSession(async ({ req, session }) => {
- try {
- const denyChapterReq = ManageChapterRequest.safeParse(await req.json());
+ const denyChapterReq = ManageChapterRequest.safeParse(await req.json());
- if (!denyChapterReq.success) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "Invalid request body",
- }),
- { status: 400 }
- );
- }
+ if (!denyChapterReq.success) {
+ return NextResponse.json(
+ ManageChapterRequestResponse.parse({
+ code: "INVALID_REQUEST",
+ message: "Invalid request body",
+ }),
+ { status: 400 }
+ );
+ }
- const targetUID = denyChapterReq.data.userId;
- const target = await prisma.user.findFirst({
- where: {
- id: targetUID,
- },
- });
- if (target == null) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "User doesn't exist",
- }),
- { status: 400 }
- );
- }
+ const targetUID = denyChapterReq.data.userId;
+ const joinChapterRequest = await prisma.userRequest.findFirst({
+ where: {
+ uid: targetUID,
+ },
+ });
- const joinChapterRequest = await prisma.userRequest.findFirst({
- where: {
- uid: session.user.id,
- },
- });
+ if (joinChapterRequest == null) {
+ // An admin have denied or accepted the user
+ return NextResponse.json(
+ ManageChapterRequestResponse.parse({
+ code: "SUCCESS",
+ })
+ );
+ }
- if (joinChapterRequest == null) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "User doesn't have any active request",
- }),
- { status: 400 }
- );
- }
+ const canApprove =
+ session.user.role === "ADMIN" ||
+ (session.user.role === "CHAPTER_LEADER" &&
+ session.user.ChapterID === joinChapterRequest.chapterId) ||
+ session.user.id === targetUID;
- const canApprove =
- session.user.role === "ADMIN" ||
- (session.user.role === "CHAPTER_LEADER" &&
- session.user.ChapterID === joinChapterRequest.chapterId) ||
- session.user.id === targetUID;
+ if (!canApprove) {
+ return NextResponse.json(
+ ManageChapterRequestResponse.parse({
+ code: "UNAUTHORIZED_REQUEST",
+ message: "User doesn't have permission to deny request",
+ }),
+ { status: 400 }
+ );
+ }
- if (!canApprove) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "UNAUTHORIZED_REQUEST",
- message: "User doesn't have permission to deny request",
- }),
- { status: 400 }
- );
- }
+ await prisma.userRequest.delete({
+ where: {
+ uid: targetUID,
+ },
+ });
- await prisma.userRequest.delete({
- where: {
- uid: targetUID,
- },
- });
+ return NextResponse.json(
+ ManageChapterRequestResponse.parse({ code: "SUCCESS" })
+ );
+});
+export const PATCH = withSession(async ({ req, session }) => {
+ const approveChapterReq = ManageChapterRequest.safeParse(await req.json());
+ if (!approveChapterReq.success) {
return NextResponse.json(
- ManageChapterRequestResponse.parse({ code: "SUCCESS" })
+ ManageChapterRequestResponse.parse({
+ code: "INVALID_REQUEST",
+ message: "Invalid request body",
+ }),
+ { status: 400 }
);
- } catch (e: any) {
+ }
+ const targetUID = approveChapterReq.data.userId;
+ const chapterRequest = await prisma.userRequest.findUnique({
+ where: { uid: targetUID },
+ include: { user: true },
+ });
+ if (chapterRequest == null || chapterRequest.user.ChapterID != null) {
+ // If chapterRequest doesn't exist, another admin has denied the user / chapter has been deleted / user has been accepted
return NextResponse.json(
- ManageChapterRequestResponse.parse({ code: "UNKNOWN" }),
- { status: 500 }
+ ManageChapterRequestResponse.parse({ code: "SUCCESS" })
);
}
-});
-export const PATCH = withSession(async ({ req, session }) => {
- try {
- const approveChapterReq = ManageChapterRequest.safeParse(await req.json());
- if (!approveChapterReq.success) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "Invalid request body",
- }),
- { status: 400 }
- );
- }
- const targetUID = approveChapterReq.data.userId;
- const target = await prisma.user.findFirst({
- where: {
- id: targetUID,
- },
- });
- if (target == null) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "User doesn't exist",
- }),
- { status: 400 }
- );
- }
- const approveChapterRequest = await prisma.userRequest.findFirst({
- where: {
- uid: targetUID,
- },
- });
- if (approveChapterRequest == null) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "User doesn't have any active request",
- }),
- { status: 400 }
- );
- }
- const canApprove =
- session.user.role === "ADMIN" ||
- (session.user.role === "CHAPTER_LEADER" &&
- session.user.ChapterID === approveChapterRequest.chapterId);
- if (!canApprove) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "UNAUTHORIZED_REQUEST",
- message: "User doesn't have permission to approve request",
- }),
- { status: 400 }
- );
- }
- await prisma.userRequest.update({
- where: {
- uid: targetUID,
- },
- data: {
- approved: "APPROVED",
- },
- });
- const user = await prisma.user.update({
- where: {
- id: targetUID,
- },
- data: {
- ChapterID: approveChapterRequest.chapterId,
- },
- });
- const chapter = await prisma.chapter.findFirst({
- where: {
- id: approveChapterRequest.chapterId,
- },
- });
-
- if (chapter == null || user == null || user.email == null) {
- return NextResponse.json(
- ManageChapterRequestResponse.parse({
- code: "INVALID_REQUEST",
- message: "Chapter or user (or email) doesn't exist",
- }),
- { status: 400 }
- );
- }
-
- const folderId = chapter.chapterFolder;
-
- // Next, share the folder with the user that is accepted
- const shareFolder = async (folderId: string, userEmail: string) => {
- const service = await createDriveService(session.user.id);
-
- try {
- // Define the permission
- const permission = {
- type: "user",
- role: "writer", // Change role as per your requirement
- emailAddress: userEmail,
- };
-
- // Share the folder
- await service.permissions.create({
- fileId: folderId,
- requestBody: permission,
- });
-
- console.log("Folder shared successfully!");
- } catch (error) {
- console.error("Error sharing folder:", error);
- }
- };
- await shareFolder(folderId, user.email);
+ const canApprove =
+ session.user.role === "ADMIN" ||
+ (session.user.role === "CHAPTER_LEADER" &&
+ session.user.ChapterID === chapterRequest.chapterId);
+ if (!canApprove) {
return NextResponse.json(
- ManageChapterRequestResponse.parse({ code: "SUCCESS" })
+ ManageChapterRequestResponse.parse({
+ code: "UNAUTHORIZED_REQUEST",
+ message: "User doesn't have permission to approve request",
+ }),
+ { status: 400 }
);
- } catch (e: any) {
+ }
+ await prisma.userRequest.delete({
+ where: {
+ uid: targetUID,
+ },
+ });
+
+ const chapter = await prisma.chapter.findFirst({
+ where: {
+ id: chapterRequest.chapterId,
+ },
+ });
+
+ if (chapter == null) {
return NextResponse.json(
- ManageChapterRequestResponse.parse({ code: "UNKNOWN" }),
- { status: 500 }
+ ManageChapterRequestResponse.parse({
+ code: "INVALID_REQUEST",
+ message: "Chapter or user (or email) doesn't exist",
+ }),
+ { status: 400 }
);
}
+
+ const folderId = chapter.chapterFolder;
+
+ // Next, share the folder with the user that is accepted
+ const shareFolder = async (folderId: string, userEmail: string) => {
+ const service = await createDriveService(session.user.id);
+ // Define the permission
+ const permission = {
+ type: "user",
+ role: "writer", // Change role as per your requirement
+ emailAddress: userEmail,
+ };
+
+ // Share the folder
+ await service.permissions.create({
+ fileId: folderId,
+ requestBody: permission,
+ });
+ };
+ // Since we use Google login, they must have an email
+ await shareFolder(folderId, chapterRequest.user.email ?? "");
+ // We update the chapter ID second to allow the user to rejoin in the case that shareFolder fails midway
+ await prisma.user.update({
+ where: { id: chapterRequest.uid },
+ data: { ChapterID: chapterRequest.chapterId },
+ });
+
+ return NextResponse.json(
+ ManageChapterRequestResponse.parse({ code: "SUCCESS" })
+ );
});
diff --git a/src/components/ChapterRequest.tsx b/src/components/ChapterRequest.tsx
index 9ba730b8..7634aeee 100644
--- a/src/components/ChapterRequest.tsx
+++ b/src/components/ChapterRequest.tsx
@@ -1,5 +1,4 @@
"use client";
-
import { handleChapterRequest } from "src/app/api/handle-chapter-request/route.client";
import { useRouter } from "next/navigation";
import { ChapterRequest } from "@prisma/client";
diff --git a/src/components/DisplayChapterInfo.tsx b/src/components/DisplayChapterInfo.tsx
index 44af854e..8a18d0d1 100644
--- a/src/components/DisplayChapterInfo.tsx
+++ b/src/components/DisplayChapterInfo.tsx
@@ -123,7 +123,7 @@ const DisplayChapterInfo = ({
);
})}
diff --git a/src/components/PendingCard.tsx b/src/components/PendingCard.tsx
index 5e0f6403..470510ba 100644
--- a/src/components/PendingCard.tsx
+++ b/src/components/PendingCard.tsx
@@ -9,6 +9,9 @@ import {
handleManageChapterRequest,
handleAcceptChapterRequest,
} from "@api/user-request/route.client";
+import { useApiThrottle } from "@hooks";
+import { useRouter } from "next/navigation";
+import { Spinner } from "./skeleton";
interface IPendingCard {
name: string;
@@ -16,6 +19,25 @@ interface IPendingCard {
}
const PendingCard = (props: IPendingCard) => {
+ const router = useRouter();
+ const {
+ fetching: acceptChapterRequestFetching,
+ fn: throttleAcceptChapterRequest,
+ } = useApiThrottle({
+ fn: handleAcceptChapterRequest,
+ callback: () => router.refresh(),
+ });
+ const {
+ fetching: manageChapterRequestFetching,
+ fn: throttleManageChapterRequest,
+ } = useApiThrottle({
+ fn: handleManageChapterRequest,
+ callback: () => router.refresh(),
+ });
+
+ const isFetching =
+ acceptChapterRequestFetching || manageChapterRequestFetching;
+
return (
@@ -25,34 +47,28 @@ const PendingCard = (props: IPendingCard) => {
-
- {
- handleManageChapterRequest({
- body: {
- userId: props.uid,
- },
- }).then(() => {
- window.location.reload();
- });
- }}
- />
+ {!isFetching ? (
+ <>
+
+
+ throttleManageChapterRequest({ body: { userId: props.uid } })
+ }
+ />
+ >
+ ) : (
+
+ )}
);
diff --git a/src/components/TileGrid/InfoTile.tsx b/src/components/TileGrid/InfoTile.tsx
index 33421776..adfbdfcb 100644
--- a/src/components/TileGrid/InfoTile.tsx
+++ b/src/components/TileGrid/InfoTile.tsx
@@ -27,12 +27,6 @@ interface InfoTileProps {
const InfoTile = (params: InfoTileProps) => {
const { title, information, topRightButton, moreInformation, href } = params;
- const [shouldShowMore, setShouldShowMore] = React.useState(false);
-
- const onToggleShouldShowMore = () => {
- setShouldShowMore(!shouldShowMore);
- };
-
return (
@@ -55,15 +49,17 @@ const InfoTile = (params: InfoTileProps) => {
))}
-
- {!shouldShowMore ? "Show more" : "Show less"}
-
- }
- >
- {moreInformation}
-
+ {moreInformation ? (
+
+ Show more
+
+ }
+ >
+ {moreInformation}
+
+ ) : null}
);
};