Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync Upstream #152

Merged
merged 13 commits into from
Apr 4, 2024
1 change: 1 addition & 0 deletions .github/workflows/unit-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ jobs:
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
GOOGLE_BASEFOLDER: ${{ secrets.GOOGLE_BASEFOLDER }}
20 changes: 12 additions & 8 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ model User {
email String? @unique
emailVerified DateTime?
image String?
position String?
position String @default("")

// @deprecated Pending for removal when we expand to multiple universities
admin Boolean @default(false)
Expand All @@ -70,6 +70,8 @@ model User {
approved Approval @default(PENDING)
ChapterID String? @db.ObjectId
Chapter Chapter? @relation(fields: [ChapterID], references: [id])

userRequest UserRequest?
}

enum Approval {
Expand All @@ -92,7 +94,7 @@ model Senior {
description String
StudentIDs String[] @db.ObjectId
Students User[] @relation(fields: [StudentIDs], references: [id])
folder String
folder String @default("")
Files File[]
ChapterID String @db.ObjectId
chapter Chapter @relation(fields: [ChapterID], references: [id])
Expand Down Expand Up @@ -126,12 +128,13 @@ model ChapterRequest {
}

model Chapter {
id String @id @default(auto()) @map("_id") @db.ObjectId
chapterName String
location String
dateCreated DateTime @default(now())
students User[]
seniors Senior[]
id String @id @default(auto()) @map("_id") @db.ObjectId
chapterName String
location String
chapterFolder String @default("")
dateCreated DateTime @default(now())
students User[]
seniors Senior[]
}

model Resource {
Expand All @@ -151,4 +154,5 @@ model UserRequest {
approved Approval @default(PENDING)
uid String @unique @db.ObjectId
chapterId String @db.ObjectId
user User @relation(fields: [uid], references: [id])
}
38 changes: 34 additions & 4 deletions src/app/api/handle-chapter-request/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ import {
HandleChapterRequest,
HandleChapterRequestResponse,
} from "./route.schema";
import { withSession } from "@server/decorator";
import { createDriveService } from "@server/service";
import { env } from "@env/server.mjs";
import { prisma } from "@server/db/client";

export const POST = async (request: NextRequest) => {
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

// Validate a proper JSON was passed in as well
try {
const handleChapterRequest = HandleChapterRequest.safeParse(
await request.json()
await req.json()
);
if (!handleChapterRequest.success) {
return NextResponse.json(
Expand Down Expand Up @@ -53,7 +56,7 @@ export const POST = async (request: NextRequest) => {
}
// If approved, create a new chapter and update approved field of chapter request
if (body.approved === true) {
await prisma.chapter.create({
const createdChapter = await prisma.chapter.create({
data: {
chapterName: chapterRequest.university,
location: chapterRequest.universityAddress,
Expand All @@ -67,6 +70,33 @@ export const POST = async (request: NextRequest) => {
approved: "APPROVED",
},
});

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 service = await createDriveService(session.user.id);
const file = await (
service as NonNullable<typeof service>
).files.create(fileCreateData);
const googleFolderId = (file as any).data.id;

await prisma.chapter.update({
where: {
id: createdChapter.id,
},
data: {
chapterFolder: googleFolderId,
},
});

return NextResponse.json(
HandleChapterRequestResponse.parse({
code: "SUCCESS",
Expand Down Expand Up @@ -101,4 +131,4 @@ export const POST = async (request: NextRequest) => {
{ status: 500 }
);
}
};
});
10 changes: 10 additions & 0 deletions src/app/api/route.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,14 @@ export const invalidFormReponse = invalidFormErrorSchema.parse({
message: "The form is not valid",
});

export const invalidRequestSchema = z.object({
code: z.literal("INVALID_REQUEST"),
message: z.string(),
});

export const invalidRequestResponse = invalidRequestSchema.parse({
code: "INVALID_REQUEST",
message: "Request body is invalid",
});

export type IUnauthorizedErrorSchema = z.infer<typeof unauthorizedErrorSchema>;
66 changes: 35 additions & 31 deletions src/app/api/senior/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import { withSessionAndRole } from "@server/decorator";
import { NextResponse } from "next/server";
import { seniorPostResponse, postSeniorSchema } from "./route.schema";
import { prisma } from "@server/db/client";
import { randomUUID } from "crypto";
import { google } from "googleapis";
import { env } from "process";
import { createDriveService } from "@server/service";

// @TODO - Use google drive service to create folder
export const POST = withSessionAndRole(
Expand Down Expand Up @@ -47,11 +45,36 @@ export const POST = withSessionAndRole(
})
);
}
const baseFolder = "1MVyWBeKCd1erNe9gkwBf7yz3wGa40g9a"; // TODO: make env variable

const chapter = await prisma.chapter.findFirst({
where: {
id: session.user.ChapterID,
},
});
if (!chapter) {
return NextResponse.json(
seniorPostResponse.parse({
code: "UNKNOWN",
message: "Chapter not found",
}),
{ status: 400 }
);
}

const senior = await prisma.senior.create({
data: {
firstname: seniorBody.firstname,
lastname: seniorBody.lastname,
location: seniorBody.location,
description: seniorBody.description,
ChapterID: session.user.ChapterID,
StudentIDs: seniorBody.StudentIDs,
},
});

const baseFolder = chapter.chapterFolder; // TODO: make env variable
const fileMetadata = {
name: [
`${seniorBody.firstname}_${seniorBody.lastname}-${randomUUID()}`,
],
name: [`${seniorBody.firstname}_${seniorBody.lastname}_${senior.id}`],
mimeType: "application/vnd.google-apps.folder",
parents: [baseFolder],
};
Expand All @@ -60,37 +83,18 @@ export const POST = withSessionAndRole(
fields: "id",
};

const { access_token, refresh_token } = (await prisma.account.findFirst({
where: {
userId: session.user.id,
},
})) ?? { access_token: null };
const auth = new google.auth.OAuth2({
clientId: env.GOOGLE_CLIENT_ID,
clientSecret: env.GOOGLE_CLIENT_SECRET,
});
auth.setCredentials({
access_token,
refresh_token,
});
const service = google.drive({
version: "v3",
auth,
});
const service = await createDriveService(session.user.id);

const file = await (service as NonNullable<typeof service>).files.create(
fileCreateData
);
const googleFolderId = (file as any).data.id;

const senior = await prisma.senior.create({
await prisma.senior.update({
where: {
id: senior.id,
},
data: {
firstname: seniorBody.firstname,
lastname: seniorBody.lastname,
location: seniorBody.location,
description: seniorBody.description,
ChapterID: session.user.ChapterID,
StudentIDs: seniorBody.StudentIDs,
folder: googleFolderId,
},
});
Expand Down
44 changes: 43 additions & 1 deletion src/app/api/user-request/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "./route.schema";
import { prisma } from "@server/db/client";
import { withSession } from "@server/decorator/index";
import { createDriveService } from "@server/service";

export const POST = withSession(async ({ req, session }) => {
try {
Expand Down Expand Up @@ -216,15 +217,56 @@ export const PATCH = withSession(async ({ req, session }) => {
approved: "APPROVED",
},
});
await prisma.user.update({
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);
return NextResponse.json(
ManageChapterRequestResponse.parse({ code: "SUCCESS" })
);
Expand Down
16 changes: 16 additions & 0 deletions src/app/api/user/[uid]/edit-position/route.client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TypedRequest } from "@server/type";
import { z } from "zod";
import { editPositionRequest, editPositionResponse } from "./route.schema";

export const editPosition = async (
request: TypedRequest<z.infer<typeof editPositionRequest>>,
uid: string
) => {
const { body, ...options } = request;
const response = await fetch(`/api/user/${uid}/edit-position`, {
method: "PATCH",
body: JSON.stringify(body),
...options,
});
return editPositionResponse.parse(await response.json());
};
11 changes: 11 additions & 0 deletions src/app/api/user/[uid]/edit-position/route.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { z } from "zod";

export const editPositionRequest = z.object({
position: z.string(),
});

export const editPositionResponse = z.discriminatedUnion("code", [
z.object({
code: z.literal("SUCCESS"),
}),
]);
38 changes: 38 additions & 0 deletions src/app/api/user/[uid]/edit-position/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { prisma } from "@server/db/client";
import { withRole, withSession } from "@server/decorator";
import { NextResponse } from "next/server";
import { editPositionRequest, editPositionResponse } from "./route.schema";
import {
invalidRequestResponse,
unauthorizedErrorResponse,
} from "@api/route.schema";

export const PATCH = withSession(
withRole(["CHAPTER_LEADER"], async ({ session, req, params }) => {
const { user: me } = session;
const otherUid: string = params.params.uid;
const other = await prisma.user.findUnique({
where: { id: otherUid },
});
const request = editPositionRequest.safeParse(await req.json());

if (other == null || !request.success) {
return NextResponse.json(invalidRequestResponse, { status: 400 });
}

if (me.role !== "CHAPTER_LEADER" || me.ChapterID !== other.ChapterID) {
return NextResponse.json(unauthorizedErrorResponse, { status: 401 });
}

await prisma.user.update({
where: {
id: otherUid,
},
data: {
position: request.data.position,
},
});

return NextResponse.json(editPositionResponse.parse({ code: "SUCCESS" }));
})
);
Loading
Loading