From 8d82c38cc8002e57fadbaaca02bfe2d67355ec01 Mon Sep 17 00:00:00 2001 From: sburchfield33 Date: Sun, 28 Jan 2024 13:52:12 -0500 Subject: [PATCH 01/77] We started the POST request and create the POST request schema Co-authored-by: nathan-j-edwards --- prisma/schema.prisma | 2 ++ src/app/api/senior/[id]/route.client.ts | 19 ++++++++++++++ src/app/api/senior/[id]/route.schema.ts | 19 +++++++++++++- src/app/api/senior/[id]/route.ts | 35 ++++++++++++++++++++++++- 4 files changed, 73 insertions(+), 2 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2b05cb3b..e99a3393 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -90,6 +90,7 @@ model Senior { Students User[] @relation(fields: [StudentIDs], references: [id]) folder String Files File[] + chapter Chapter @relation(fields: [id], references: [id]) } model File { @@ -125,6 +126,7 @@ model Chapter { location String dateCreated DateTime @default(now()) students User[] + seniors Senior[] } model Resource { diff --git a/src/app/api/senior/[id]/route.client.ts b/src/app/api/senior/[id]/route.client.ts index 5ca5bafd..ada38c26 100644 --- a/src/app/api/senior/[id]/route.client.ts +++ b/src/app/api/senior/[id]/route.client.ts @@ -1,6 +1,7 @@ import { seniorDeleteResponse, seniorPatchResponse, + seniorPostResponse, IPatchSeniorRequestSchema, } from "./route.schema"; @@ -13,6 +14,12 @@ interface IPatchSeniorRequest extends Omit { body: IPatchSeniorRequestSchema; } +interface IPostSeniorRequestSchema extends Omit { + body: IPostSeniorRequestSchema; +} + + + export const deleteSenior = async (request: IDeleteSeniorRequest) => { const { seniorId, ...options } = request; const response = await fetch(`/api/senior/${seniorId}`, { @@ -33,3 +40,15 @@ export const patchSenior = async (request: IPatchSeniorRequest) => { const json = await response.json(); return seniorPatchResponse.parse(json); }; + + +export const postSenior = async (request: IPostSeniorRequestSchema) => { + const {body, ...options} = request; + const response = await fetch("/api/senior/", { + method: "POST", + body: JSON.stringify(body), + ...options, + }); + const json = await response.json(); + return seniorPostResponse.parse(json); +} \ No newline at end of file diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index a28620b8..e9cd4f40 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -21,7 +21,7 @@ export const seniorSchema = z.object({ location: z.string(), description: z.string(), StudentIDs: z.array(z.string()), - folder: z.string() + folder: z.string(), }) satisfies z.ZodType; export const patchSeniorSchema = z.object({ @@ -31,10 +31,21 @@ export const patchSeniorSchema = z.object({ StudentIDs: z.array(z.string()) }) +export const postSeniorSchema = z.object({ + name: z.string(), + location: z.string(), + description: z.string(), + StudentIDs: z.array(z.string()), + folder: z.string(), + chapterID: z.string(), +}) + export type ISeniorSchema = z.infer export type IPatchSeniorRequestSchema = z.infer +export type IPostSeniorRequestSchema = z.infer + export const seniorPatchResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS"), data: seniorSchema}), z.object({ @@ -47,6 +58,12 @@ export const seniorPatchResponse = z.discriminatedUnion("code", [ ]); +export const seniorPostResponse = z.discriminatedUnion("code", [ + z.object({ code: z.literal("SUCCESS")}), + unknownErrorSchema, + unauthorizedErrorSchema, +]); + diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index fa69594c..8725377b 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -1,12 +1,14 @@ /** * @todo Migrate the other routes */ -import { withSession } from "@server/decorator"; +import { withSession, withSessionAndRole } from "@server/decorator"; import { NextResponse } from "next/server"; import { seniorDeleteResponse, seniorPatchResponse, patchSeniorSchema, + postSeniorSchema, + seniorPostResponse, } from "./route.schema"; import { prisma } from "@server/db/client"; @@ -146,3 +148,34 @@ export const PATCH = withSession(async ({ params, req }) => { ); } }); + +export const POST = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { + try { + const body = await request.req.json(); + const newsenior = postSeniorSchema.safeParse(body); + + if (!newsenior.success) { + return NextResponse.json( + seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), + { status: 500 } + ); + } + const newSeniordata = newsenior.data; + + const senior = await prisma.senior.create({ + data: { + ...newSeniordata + }, + }) + + return NextResponse.json( + seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), + { status: 500 } + ); + } catch { + return NextResponse.json( + seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), + { status: 500 } + ); + } +}); From 86bd52bfb4d9c59d0b9e755109d0e697812da761 Mon Sep 17 00:00:00 2001 From: nathan-j-edwards Date: Wed, 31 Jan 2024 20:56:46 -0500 Subject: [PATCH 02/77] finished POST request --- prisma/schema.prisma | 3 +- src/app/api/senior/[id]/route.client.ts | 10 ++-- src/app/api/senior/[id]/route.schema.ts | 31 +++++------ src/app/api/senior/[id]/route.ts | 70 +++++++++++++++++++------ 4 files changed, 75 insertions(+), 39 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index de9de723..ea7aec66 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -93,7 +93,8 @@ model Senior { Students User[] @relation(fields: [StudentIDs], references: [id]) folder String Files File[] - chapter Chapter @relation(fields: [id], references: [id]) + ChapterID String @db.ObjectId + chapter Chapter @relation(fields: [ChapterID], references: [id]) } model File { diff --git a/src/app/api/senior/[id]/route.client.ts b/src/app/api/senior/[id]/route.client.ts index ada38c26..527f5797 100644 --- a/src/app/api/senior/[id]/route.client.ts +++ b/src/app/api/senior/[id]/route.client.ts @@ -11,15 +11,14 @@ interface IDeleteSeniorRequest extends Omit { interface IPatchSeniorRequest extends Omit { seniorId: string; - body: IPatchSeniorRequestSchema; + body: IPatchSeniorRequestSchema; } interface IPostSeniorRequestSchema extends Omit { + userId: string; body: IPostSeniorRequestSchema; } - - export const deleteSenior = async (request: IDeleteSeniorRequest) => { const { seniorId, ...options } = request; const response = await fetch(`/api/senior/${seniorId}`, { @@ -41,9 +40,8 @@ export const patchSenior = async (request: IPatchSeniorRequest) => { return seniorPatchResponse.parse(json); }; - export const postSenior = async (request: IPostSeniorRequestSchema) => { - const {body, ...options} = request; + const { userId, body, ...options } = request; const response = await fetch("/api/senior/", { method: "POST", body: JSON.stringify(body), @@ -51,4 +49,4 @@ export const postSenior = async (request: IPostSeniorRequestSchema) => { }); const json = await response.json(); return seniorPostResponse.parse(json); -} \ No newline at end of file +}; diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index e9cd4f40..f52bf53a 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -3,7 +3,7 @@ import { unauthorizedErrorSchema, unknownErrorSchema, } from "../../route.schema"; -import { Prisma } from "@prisma/client"; +import { Senior } from "@prisma/client"; export const seniorDeleteResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS") }), @@ -22,51 +22,48 @@ export const seniorSchema = z.object({ description: z.string(), StudentIDs: z.array(z.string()), folder: z.string(), -}) satisfies z.ZodType; + ChapterID: z.string(), +}) satisfies z.ZodType; export const patchSeniorSchema = z.object({ name: z.string(), location: z.string(), description: z.string(), - StudentIDs: z.array(z.string()) -}) + StudentIDs: z.array(z.string()), +}); -export const postSeniorSchema = z.object({ +/* export const postSeniorSchema = z.object({ name: z.string(), location: z.string(), description: z.string(), StudentIDs: z.array(z.string()), folder: z.string(), chapterID: z.string(), -}) +}); */ -export type ISeniorSchema = z.infer +export type ISeniorSchema = z.infer; -export type IPatchSeniorRequestSchema = z.infer +export type IPatchSeniorRequestSchema = z.infer; -export type IPostSeniorRequestSchema = z.infer +export type IPostSeniorRequestSchema = z.infer; export const seniorPatchResponse = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS"), data: seniorSchema}), + z.object({ code: z.literal("SUCCESS"), data: seniorSchema }), z.object({ code: z.literal("NOT_FOUND"), message: z.string(), }), - z.object({code: z.literal("INVALID_EDIT")}), + z.object({ code: z.literal("INVALID_EDIT") }), unknownErrorSchema, unauthorizedErrorSchema, ]); - export const seniorPostResponse = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS")}), + z.object({ code: z.literal("SUCCESS") }), unknownErrorSchema, unauthorizedErrorSchema, ]); - - - // export const seniorGetResponse = z.discriminatedUnion("code", [ // z.object({ code: z.literal("SUCCESS"), data: getSeniorSchema }), @@ -77,4 +74,4 @@ export const seniorPostResponse = z.discriminatedUnion("code", [ // unknownErrorSchema, // unauthorizedErrorSchema, -// ]); \ No newline at end of file +// ]); diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 8725377b..fa4bd1ec 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -7,8 +7,8 @@ import { seniorDeleteResponse, seniorPatchResponse, patchSeniorSchema, - postSeniorSchema, seniorPostResponse, + seniorSchema, } from "./route.schema"; import { prisma } from "@server/db/client"; @@ -152,26 +152,66 @@ export const PATCH = withSession(async ({ params, req }) => { export const POST = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { try { const body = await request.req.json(); - const newsenior = postSeniorSchema.safeParse(body); + const nextParams: { id: string } = request.params; + const { id: userId } = nextParams; + const newSenior = seniorSchema.safeParse(body); - if (!newsenior.success) { + if (!newSenior.success) { return NextResponse.json( - seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), + seniorPostResponse.parse({ + code: "UNKNOWN", + message: "Network error", + }), { status: 500 } ); - } - const newSeniordata = newsenior.data; + } else { + const user = await prisma.user.findFirst({ + where: { + id: userId, + }, + }); - const senior = await prisma.senior.create({ - data: { - ...newSeniordata - }, - }) + if (!user) { + return NextResponse.json( + seniorPatchResponse.parse({ + code: "UNKNOWN", + message: "User was not found", + }) + ); + } + const newSeniorData = newSenior.data; + + if (user?.ChapterID != newSeniorData.ChapterID) { + return NextResponse.json( + seniorPatchResponse.parse({ + code: "ERROR", + message: "User has no authority to add", + }) + ); + } - return NextResponse.json( - seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), - { status: 500 } - ); + const senior = await prisma.senior.create({ + data: { + ...newSeniorData, + }, + }); + + if (!senior) { + return NextResponse.json( + seniorPatchResponse.parse({ + code: "UNKNOWN", + message: "Network error", + }) + ); + } + + return NextResponse.json( + seniorPatchResponse.parse({ + code: "SUCCESS", + data: senior, + }) + ); + } } catch { return NextResponse.json( seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), From 678e52d0e0b01d24f3ae7ab9b3d0d3b38be5d9ff Mon Sep 17 00:00:00 2001 From: nathan-j-edwards Date: Sun, 4 Feb 2024 14:28:50 -0500 Subject: [PATCH 03/77] added authorization to patch and delete --- src/app/api/senior/[id]/route.client.ts | 7 +- src/app/api/senior/[id]/route.schema.ts | 11 +- src/app/api/senior/[id]/route.ts | 131 ++++++++++++------ .../private/[uid]/admin/elist/test/page.tsx | 29 ++++ 4 files changed, 134 insertions(+), 44 deletions(-) create mode 100644 src/app/private/[uid]/admin/elist/test/page.tsx diff --git a/src/app/api/senior/[id]/route.client.ts b/src/app/api/senior/[id]/route.client.ts index 527f5797..01b66587 100644 --- a/src/app/api/senior/[id]/route.client.ts +++ b/src/app/api/senior/[id]/route.client.ts @@ -3,6 +3,7 @@ import { seniorPatchResponse, seniorPostResponse, IPatchSeniorRequestSchema, + IPostSeniorRequestSchema, } from "./route.schema"; interface IDeleteSeniorRequest extends Omit { @@ -14,7 +15,7 @@ interface IPatchSeniorRequest extends Omit { body: IPatchSeniorRequestSchema; } -interface IPostSeniorRequestSchema extends Omit { +interface IPostSeniorRequest extends Omit { userId: string; body: IPostSeniorRequestSchema; } @@ -40,9 +41,9 @@ export const patchSenior = async (request: IPatchSeniorRequest) => { return seniorPatchResponse.parse(json); }; -export const postSenior = async (request: IPostSeniorRequestSchema) => { +export const postSenior = async (request: IPostSeniorRequest) => { const { userId, body, ...options } = request; - const response = await fetch("/api/senior/", { + const response = await fetch(`/api/senior/${userId}`, { method: "POST", body: JSON.stringify(body), ...options, diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index f52bf53a..9c69f684 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -30,6 +30,15 @@ export const patchSeniorSchema = z.object({ location: z.string(), description: z.string(), StudentIDs: z.array(z.string()), + ChapterID: z.string(), +}); + +export const postSeniorSchema = z.object({ + name: z.string(), + location: z.string(), + description: z.string(), + StudentIDs: z.array(z.string()), + ChapterID: z.string(), }); /* export const postSeniorSchema = z.object({ @@ -45,7 +54,7 @@ export type ISeniorSchema = z.infer; export type IPatchSeniorRequestSchema = z.infer; -export type IPostSeniorRequestSchema = z.infer; +export type IPostSeniorRequestSchema = z.infer; export const seniorPatchResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS"), data: seniorSchema }), diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index fa4bd1ec..9dfcb727 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -9,58 +9,70 @@ import { patchSeniorSchema, seniorPostResponse, seniorSchema, + postSeniorSchema, } from "./route.schema"; import { prisma } from "@server/db/client"; +import { request } from "http"; +import { randomUUID } from "crypto"; +import { drive } from "googleapis/build/src/apis/drive"; +import { google } from "googleapis"; +import { env } from "process"; /** * @todo Enforces that this API request be made with a Chapter leader from the university * @todo Should senior/[id] be nested under /university/[uid]? This design decision is unclear... which is why we want * to wrap call into a client facing function. */ -export const DELETE = withSession(async ({ params }) => { - const nextParams: { id: string } = params.params; - const { id: seniorId } = nextParams; +export const DELETE = withSessionAndRole( + ["CHAPTER_LEADER"], + async (request) => { + const nextParams: { id: string } = request.params; + const { id: seniorId } = nextParams; - try { - const maybeSenior = prisma.senior.findUnique({ where: { id: seniorId } }); - if (maybeSenior == null) { + try { + const maybeSenior = prisma.senior.findUnique({ where: { id: seniorId } }); + if (maybeSenior == null) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + + const disconnectSenior = await prisma.senior.update({ + where: { + id: seniorId, + }, + data: { + Students: { + set: [], + }, + }, + }); + const deleteSenior = await prisma.senior.delete({ + where: { + id: seniorId, + }, + }); + + return NextResponse.json({ code: "SUCCESS" }); + } catch { return NextResponse.json( seniorDeleteResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", + code: "UNKNOWN", + message: "Network error", }), - { status: 404 } + { status: 500 } ); } - - const disconnectSenior = await prisma.senior.update({ - where: { - id: seniorId, - }, - data: { - Students: { - set: [], - }, - }, - }); - const deleteSenior = await prisma.senior.delete({ - where: { - id: seniorId, - }, - }); - - return NextResponse.json({ code: "SUCCESS" }); - } catch { - return NextResponse.json( - seniorDeleteResponse.parse({ code: "UNKNOWN", message: "Network error" }), - { status: 500 } - ); } -}); +); -export const PATCH = withSession(async ({ params, req }) => { - const body = await req.json(); - const nextParams: { id: string } = params.params; +export const PATCH = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { + const body = await request.req.json(); + const nextParams: { id: string } = request.params; const { id: seniorId } = nextParams; const maybeBody = patchSeniorSchema.safeParse(body); @@ -154,13 +166,13 @@ export const POST = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { const body = await request.req.json(); const nextParams: { id: string } = request.params; const { id: userId } = nextParams; - const newSenior = seniorSchema.safeParse(body); + const newSenior = postSeniorSchema.safeParse(body); if (!newSenior.success) { return NextResponse.json( seniorPostResponse.parse({ code: "UNKNOWN", - message: "Network error", + message: "Invalid senior template", }), { status: 500 } ); @@ -190,9 +202,48 @@ export const POST = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { ); } + const baseFolder = "1MVyWBeKCd1erNe9gkwBf7yz3wGa40g9a"; // TODO: make env variable + const fileMetadata = { + name: [`${body.name}-${randomUUID()}`], + mimeType: "application/vnd.google-apps.folder", + parents: [baseFolder], + }; + const fileCreateData = { + resource: fileMetadata, + fields: "id", + }; + + const { access_token, refresh_token } = (await prisma.account.findFirst({ + where: { + userId: request.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 file = await (service as NonNullable).files.create( + fileCreateData + ); + const googleFolderId = (file as any).data.id; + const senior = await prisma.senior.create({ data: { - ...newSeniorData, + name: body.name, + location: body.location, + description: body.description, + StudentIDs: body.StudentIDs, + ChapterID: body.ChapterID, + folder: googleFolderId, }, }); @@ -200,7 +251,7 @@ export const POST = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { return NextResponse.json( seniorPatchResponse.parse({ code: "UNKNOWN", - message: "Network error", + message: "Adding senior failed", }) ); } diff --git a/src/app/private/[uid]/admin/elist/test/page.tsx b/src/app/private/[uid]/admin/elist/test/page.tsx new file mode 100644 index 00000000..87c058f9 --- /dev/null +++ b/src/app/private/[uid]/admin/elist/test/page.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { + deleteSenior, + patchSenior, + postSenior, +} from "src/app/api/senior/[id]/route.client"; +import { IPostSeniorRequestSchema } from "@api/senior/[id]/route.schema"; + +function Test() { + async function Poo() { + postSenior({ + userId: "6587829665af0d81089c42fb", + body: { + name: "Stephen", + location: "Boston", + description: "He is cool", + StudentIDs: ["6587829665af0d81089c42fb"], + ChapterID: "65878595b94284e0c3e02d55", + }, + }).then((res) => { + console.log(res); + }); + } + + return ; +} + +export default Test; From 8226fbf3c12b5489a237eaa82570ffd61f42e49c Mon Sep 17 00:00:00 2001 From: nathan-j-edwards Date: Mon, 5 Feb 2024 17:25:08 -0500 Subject: [PATCH 04/77] worked on delete and patch --- src/app/api/senior/[id]/route.client.ts | 11 +- src/app/api/senior/[id]/route.schema.ts | 7 + src/app/api/senior/[id]/route.ts | 169 +++++++++++------- .../private/[uid]/admin/elist/test/page.tsx | 22 ++- 4 files changed, 144 insertions(+), 65 deletions(-) diff --git a/src/app/api/senior/[id]/route.client.ts b/src/app/api/senior/[id]/route.client.ts index 01b66587..87759442 100644 --- a/src/app/api/senior/[id]/route.client.ts +++ b/src/app/api/senior/[id]/route.client.ts @@ -4,13 +4,17 @@ import { seniorPostResponse, IPatchSeniorRequestSchema, IPostSeniorRequestSchema, + IDeleteSeniorRequestSchema, } from "./route.schema"; interface IDeleteSeniorRequest extends Omit { seniorId: string; + body: IDeleteSeniorRequestSchema; } +/* Note talk to nick about how to best pass the userId and the seniorId */ interface IPatchSeniorRequest extends Omit { + userId: string; seniorId: string; body: IPatchSeniorRequestSchema; } @@ -21,9 +25,10 @@ interface IPostSeniorRequest extends Omit { } export const deleteSenior = async (request: IDeleteSeniorRequest) => { - const { seniorId, ...options } = request; + const { seniorId, body, ...options } = request; const response = await fetch(`/api/senior/${seniorId}`, { method: "DELETE", + body: JSON.stringify(body), ...options, }); const json = await response.json(); @@ -31,8 +36,8 @@ export const deleteSenior = async (request: IDeleteSeniorRequest) => { }; export const patchSenior = async (request: IPatchSeniorRequest) => { - const { seniorId, body, ...options } = request; - const response = await fetch(`/api/senior/${seniorId}`, { + const { userId, seniorId, body, ...options } = request; + const response = await fetch(`/api/senior/${userId}/${seniorId}`, { method: "PATCH", body: JSON.stringify(body), ...options, diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index 9c69f684..db78ca60 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -4,6 +4,7 @@ import { unknownErrorSchema, } from "../../route.schema"; import { Senior } from "@prisma/client"; +import { deleteSenior } from "./route.client"; export const seniorDeleteResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS") }), @@ -41,6 +42,10 @@ export const postSeniorSchema = z.object({ ChapterID: z.string(), }); +export const deleteSeniorSchema = z.object({ + userId: z.string(), +}); + /* export const postSeniorSchema = z.object({ name: z.string(), location: z.string(), @@ -52,6 +57,8 @@ export const postSeniorSchema = z.object({ export type ISeniorSchema = z.infer; +export type IDeleteSeniorRequestSchema = z.infer; + export type IPatchSeniorRequestSchema = z.infer; export type IPostSeniorRequestSchema = z.infer; diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 9dfcb727..d05e821a 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -26,8 +26,8 @@ import { env } from "process"; export const DELETE = withSessionAndRole( ["CHAPTER_LEADER"], async (request) => { - const nextParams: { id: string } = request.params; - const { id: seniorId } = nextParams; + const nextParams: { userid: string; seniorid: string } = request.params; + const { userid: userId, seniorid: seniorId } = nextParams; try { const maybeSenior = prisma.senior.findUnique({ where: { id: seniorId } }); @@ -41,6 +41,27 @@ export const DELETE = withSessionAndRole( ); } + const maybeUser = prisma.user.findUnique({ where: { id: userId } }); + + if (maybeUser == null) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "NOT_FOUND", + message: "User not found", + }), + { status: 404 } + ); + } + if (maybeUser.Chapter != maybeSenior.chapter) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "ERROR", + message: "User is not of same chapter of senior", + }), + { status: 404 } + ); + } + const disconnectSenior = await prisma.senior.update({ where: { id: seniorId, @@ -71,87 +92,113 @@ export const DELETE = withSessionAndRole( ); export const PATCH = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { - const body = await request.req.json(); - const nextParams: { id: string } = request.params; - const { id: seniorId } = nextParams; - - const maybeBody = patchSeniorSchema.safeParse(body); - if (!maybeBody.success) { - return NextResponse.json( - seniorPatchResponse.parse({ code: "INVALID_EDIT" }), - { status: 400 } - ); - } - - const seniorBody = maybeBody.data; try { - const maybeSenior = await prisma.senior.findUnique({ - where: { id: seniorId }, - select: { StudentIDs: true }, - }); - if (maybeSenior == null) { + const body = await request.req.json(); + const nextParams: { userid: string; seniorid: string } = request.params; + const { userid: userId, seniorid: seniorId } = nextParams; + + const maybeBody = patchSeniorSchema.safeParse(body); + if (!maybeBody.success) { return NextResponse.json( - seniorPatchResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } + seniorPatchResponse.parse({ code: "INVALID_EDIT" }), + { status: 400 } ); - } + } else { + const seniorBody = maybeBody.data; - const senior = await prisma.senior.update({ - where: { - id: seniorId, - }, - data: { - ...seniorBody, - }, - }); + const maybeSenior = await prisma.senior.findUnique({ + where: { id: seniorId }, + select: { StudentIDs: true }, + }); + if (maybeSenior == null) { + return NextResponse.json( + seniorPatchResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + const maybeUser = await prisma.user.findFirst({ + where: { + id: userId, + }, + }); - // Remove if senior.studentIds is not contained in body.studentIds - const studentsToRemove = maybeSenior.StudentIDs.filter( - (id) => !seniorBody.StudentIDs.includes(id) - ); - const studentsToAdd = seniorBody.StudentIDs; + if (!maybeUser) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "NOT_FOUND", + message: "User not found", + }), + { status: 404 } + ); + } - const prismaStudentsToRemove = await prisma.user.findMany({ - where: { id: { in: studentsToRemove } }, - }); - const prismaStudentsToAdd = await prisma.user.findMany({ - where: { id: { in: studentsToAdd } }, - }); + if (maybeUser?.ChapterID != seniorBody.ChapterID) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "ERROR", + message: "User is not of same chapter of senior", + }), + { status: 404 } + ); + } - for (const student of prismaStudentsToRemove) { - await prisma.user.update({ + const senior = await prisma.senior.update({ where: { - id: student.id, + id: seniorId, }, data: { - SeniorIDs: student.SeniorIDs.filter((id) => id !== seniorId), + ...seniorBody, }, }); - } - for (const student of prismaStudentsToAdd) { - //Checks if student has already been added - if (!student.SeniorIDs.includes(seniorId)) { + // Remove if senior.studentIds is not contained in body.studentIds + const studentsToRemove = maybeSenior.StudentIDs.filter( + (id) => !seniorBody.StudentIDs.includes(id) + ); + const studentsToAdd = seniorBody.StudentIDs; + + const prismaStudentsToRemove = await prisma.user.findMany({ + where: { id: { in: studentsToRemove } }, + }); + const prismaStudentsToAdd = await prisma.user.findMany({ + where: { id: { in: studentsToAdd } }, + }); + + for (const student of prismaStudentsToRemove) { await prisma.user.update({ where: { id: student.id, }, data: { - SeniorIDs: [...student.SeniorIDs, seniorId], + SeniorIDs: student.SeniorIDs.filter((id) => id !== seniorId), }, }); } - } - return NextResponse.json( - seniorPatchResponse.parse({ - code: "SUCCESS", - data: senior, - }) - ); + for (const student of prismaStudentsToAdd) { + //Checks if student has already been added + if (!student.SeniorIDs.includes(seniorId)) { + await prisma.user.update({ + where: { + id: student.id, + }, + data: { + SeniorIDs: [...student.SeniorIDs, seniorId], + }, + }); + } + } + + return NextResponse.json( + seniorPatchResponse.parse({ + code: "SUCCESS", + data: senior, + }) + ); + } } catch (e: any) { console.log("Error", e); return NextResponse.json( diff --git a/src/app/private/[uid]/admin/elist/test/page.tsx b/src/app/private/[uid]/admin/elist/test/page.tsx index 87c058f9..0698d514 100644 --- a/src/app/private/[uid]/admin/elist/test/page.tsx +++ b/src/app/private/[uid]/admin/elist/test/page.tsx @@ -23,7 +23,27 @@ function Test() { }); } - return ; + async function Delete() { + deleteSenior({ + seniorId: "65bfe4fe217927ba65687658", + body: { + userId: "6587829665af0d81089c42fb", + }, + }); + } + + // async function Patch() { + // patchSenior({ + + // }) + // } + + return ( +
+ + +
+ ); } export default Test; From 3a08df9b584a3fd7f4d9740e3ab1506c7116108c Mon Sep 17 00:00:00 2001 From: nathan-j-edwards Date: Wed, 7 Feb 2024 20:53:16 -0500 Subject: [PATCH 05/77] Trying to fix DELETE --- src/app/api/senior/[id]/route.client.ts | 28 +- src/app/api/senior/[id]/route.schema.ts | 23 -- src/app/api/senior/[id]/route.ts | 382 ++++++------------ src/app/api/senior/route.client.ts | 16 + src/app/api/senior/route.schema.ts | 18 + src/app/api/senior/route.ts | 125 ++++++ .../private/[uid]/admin/elist/test/page.tsx | 37 +- 7 files changed, 317 insertions(+), 312 deletions(-) create mode 100644 src/app/api/senior/route.client.ts create mode 100644 src/app/api/senior/route.schema.ts create mode 100644 src/app/api/senior/route.ts diff --git a/src/app/api/senior/[id]/route.client.ts b/src/app/api/senior/[id]/route.client.ts index 87759442..719741bb 100644 --- a/src/app/api/senior/[id]/route.client.ts +++ b/src/app/api/senior/[id]/route.client.ts @@ -1,34 +1,23 @@ import { seniorDeleteResponse, seniorPatchResponse, - seniorPostResponse, IPatchSeniorRequestSchema, - IPostSeniorRequestSchema, - IDeleteSeniorRequestSchema, } from "./route.schema"; interface IDeleteSeniorRequest extends Omit { seniorId: string; - body: IDeleteSeniorRequestSchema; } /* Note talk to nick about how to best pass the userId and the seniorId */ interface IPatchSeniorRequest extends Omit { - userId: string; seniorId: string; body: IPatchSeniorRequestSchema; } -interface IPostSeniorRequest extends Omit { - userId: string; - body: IPostSeniorRequestSchema; -} - export const deleteSenior = async (request: IDeleteSeniorRequest) => { - const { seniorId, body, ...options } = request; + const { seniorId, ...options } = request; const response = await fetch(`/api/senior/${seniorId}`, { method: "DELETE", - body: JSON.stringify(body), ...options, }); const json = await response.json(); @@ -36,8 +25,8 @@ export const deleteSenior = async (request: IDeleteSeniorRequest) => { }; export const patchSenior = async (request: IPatchSeniorRequest) => { - const { userId, seniorId, body, ...options } = request; - const response = await fetch(`/api/senior/${userId}/${seniorId}`, { + const { seniorId, body, ...options } = request; + const response = await fetch(`/api/senior/${seniorId}`, { method: "PATCH", body: JSON.stringify(body), ...options, @@ -45,14 +34,3 @@ export const patchSenior = async (request: IPatchSeniorRequest) => { const json = await response.json(); return seniorPatchResponse.parse(json); }; - -export const postSenior = async (request: IPostSeniorRequest) => { - const { userId, body, ...options } = request; - const response = await fetch(`/api/senior/${userId}`, { - method: "POST", - body: JSON.stringify(body), - ...options, - }); - const json = await response.json(); - return seniorPostResponse.parse(json); -}; diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index db78ca60..1a710fd3 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -4,7 +4,6 @@ import { unknownErrorSchema, } from "../../route.schema"; import { Senior } from "@prisma/client"; -import { deleteSenior } from "./route.client"; export const seniorDeleteResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS") }), @@ -34,18 +33,6 @@ export const patchSeniorSchema = z.object({ ChapterID: z.string(), }); -export const postSeniorSchema = z.object({ - name: z.string(), - location: z.string(), - description: z.string(), - StudentIDs: z.array(z.string()), - ChapterID: z.string(), -}); - -export const deleteSeniorSchema = z.object({ - userId: z.string(), -}); - /* export const postSeniorSchema = z.object({ name: z.string(), location: z.string(), @@ -57,12 +44,8 @@ export const deleteSeniorSchema = z.object({ export type ISeniorSchema = z.infer; -export type IDeleteSeniorRequestSchema = z.infer; - export type IPatchSeniorRequestSchema = z.infer; -export type IPostSeniorRequestSchema = z.infer; - export const seniorPatchResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS"), data: seniorSchema }), z.object({ @@ -74,12 +57,6 @@ export const seniorPatchResponse = z.discriminatedUnion("code", [ unauthorizedErrorSchema, ]); -export const seniorPostResponse = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS") }), - unknownErrorSchema, - unauthorizedErrorSchema, -]); - // export const seniorGetResponse = z.discriminatedUnion("code", [ // z.object({ code: z.literal("SUCCESS"), data: getSeniorSchema }), diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index d05e821a..694d4c4d 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -7,9 +7,7 @@ import { seniorDeleteResponse, seniorPatchResponse, patchSeniorSchema, - seniorPostResponse, seniorSchema, - postSeniorSchema, } from "./route.schema"; import { prisma } from "@server/db/client"; import { request } from "http"; @@ -25,295 +23,183 @@ import { env } from "process"; */ export const DELETE = withSessionAndRole( ["CHAPTER_LEADER"], - async (request) => { - const nextParams: { userid: string; seniorid: string } = request.params; - const { userid: userId, seniorid: seniorId } = nextParams; + async ({ session, params }) => { + const nextParams: { id: string } = params.params; + const { id: seniorId } = nextParams; - try { - const maybeSenior = prisma.senior.findUnique({ where: { id: seniorId } }); - if (maybeSenior == null) { - return NextResponse.json( - seniorDeleteResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } - - const maybeUser = prisma.user.findUnique({ where: { id: userId } }); - - if (maybeUser == null) { - return NextResponse.json( - seniorDeleteResponse.parse({ - code: "NOT_FOUND", - message: "User not found", - }), - { status: 404 } - ); - } - if (maybeUser.Chapter != maybeSenior.chapter) { - return NextResponse.json( - seniorDeleteResponse.parse({ - code: "ERROR", - message: "User is not of same chapter of senior", - }), - { status: 404 } - ); - } - - const disconnectSenior = await prisma.senior.update({ - where: { - id: seniorId, - }, - data: { - Students: { - set: [], - }, - }, - }); - const deleteSenior = await prisma.senior.delete({ - where: { - id: seniorId, - }, - }); - - return NextResponse.json({ code: "SUCCESS" }); - } catch { + const maybeSenior = prisma.senior.findFirst({ where: { id: seniorId } }); + if (maybeSenior == null) { return NextResponse.json( seniorDeleteResponse.parse({ - code: "UNKNOWN", - message: "Network error", + code: "NOT_FOUND", + message: "Senior not found", }), - { status: 500 } + { status: 404 } ); } - } -); -export const PATCH = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { - try { - const body = await request.req.json(); - const nextParams: { userid: string; seniorid: string } = request.params; - const { userid: userId, seniorid: seniorId } = nextParams; + const maybeUser = prisma.user.findUnique({ + where: { id: session.user.id }, + }); - const maybeBody = patchSeniorSchema.safeParse(body); - if (!maybeBody.success) { + if (maybeUser == null) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "NOT_FOUND", + message: "User not found", + }), + { status: 404 } + ); + } + if (maybeUser.Chapter != maybeSenior.chapter) { return NextResponse.json( - seniorPatchResponse.parse({ code: "INVALID_EDIT" }), - { status: 400 } + seniorDeleteResponse.parse({ + code: "UNAUTHORIZED", + message: "User is not of same chapter of senior", + }), + { status: 404 } ); - } else { - const seniorBody = maybeBody.data; + } - const maybeSenior = await prisma.senior.findUnique({ - where: { id: seniorId }, - select: { StudentIDs: true }, - }); - if (maybeSenior == null) { - return NextResponse.json( - seniorPatchResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } - const maybeUser = await prisma.user.findFirst({ - where: { - id: userId, + const disconnectSenior = await prisma.senior.update({ + where: { + id: seniorId, + }, + data: { + Students: { + set: [], }, - }); + }, + }); + const deleteSenior = await prisma.senior.delete({ + where: { + id: seniorId, + }, + }); + + return NextResponse.json({ code: "SUCCESS" }); + } +); - if (!maybeUser) { - return NextResponse.json( - seniorDeleteResponse.parse({ - code: "NOT_FOUND", - message: "User not found", - }), - { status: 404 } - ); - } +export const PATCH = withSessionAndRole( + ["CHAPTER_LEADER"], + async ({ req, session, params }) => { + try { + const body = await req.json(); + const nextParams: { seniorid: string } = params; + const { seniorid: seniorId } = nextParams; - if (maybeUser?.ChapterID != seniorBody.ChapterID) { + const maybeBody = patchSeniorSchema.safeParse(body); + if (!maybeBody.success) { return NextResponse.json( - seniorDeleteResponse.parse({ - code: "ERROR", - message: "User is not of same chapter of senior", - }), - { status: 404 } + seniorPatchResponse.parse({ code: "INVALID_EDIT" }), + { status: 400 } ); - } + } else { + const seniorBody = maybeBody.data; - const senior = await prisma.senior.update({ - where: { - id: seniorId, - }, - data: { - ...seniorBody, - }, - }); + const maybeSenior = await prisma.senior.findUnique({ + where: { id: seniorId }, + select: { StudentIDs: true }, + }); + if (maybeSenior == null) { + return NextResponse.json( + seniorPatchResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + const maybeUser = await prisma.user.findFirst({ + where: { + id: session.user.id, + }, + }); - // Remove if senior.studentIds is not contained in body.studentIds - const studentsToRemove = maybeSenior.StudentIDs.filter( - (id) => !seniorBody.StudentIDs.includes(id) - ); - const studentsToAdd = seniorBody.StudentIDs; + if (!maybeUser) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "NOT_FOUND", + message: "User not found", + }), + { status: 404 } + ); + } - const prismaStudentsToRemove = await prisma.user.findMany({ - where: { id: { in: studentsToRemove } }, - }); - const prismaStudentsToAdd = await prisma.user.findMany({ - where: { id: { in: studentsToAdd } }, - }); + if (maybeUser?.ChapterID != seniorBody.ChapterID) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "ERROR", + message: "User is not of same chapter of senior", + }), + { status: 404 } + ); + } - for (const student of prismaStudentsToRemove) { - await prisma.user.update({ + const senior = await prisma.senior.update({ where: { - id: student.id, + id: seniorId, }, data: { - SeniorIDs: student.SeniorIDs.filter((id) => id !== seniorId), + ...seniorBody, }, }); - } - for (const student of prismaStudentsToAdd) { - //Checks if student has already been added - if (!student.SeniorIDs.includes(seniorId)) { + // Remove if senior.studentIds is not contained in body.studentIds + const studentsToRemove = maybeSenior.StudentIDs.filter( + (id) => !seniorBody.StudentIDs.includes(id) + ); + const studentsToAdd = seniorBody.StudentIDs; + + const prismaStudentsToRemove = await prisma.user.findMany({ + where: { id: { in: studentsToRemove } }, + }); + const prismaStudentsToAdd = await prisma.user.findMany({ + where: { id: { in: studentsToAdd } }, + }); + + for (const student of prismaStudentsToRemove) { await prisma.user.update({ where: { id: student.id, }, data: { - SeniorIDs: [...student.SeniorIDs, seniorId], + SeniorIDs: student.SeniorIDs.filter((id) => id !== seniorId), }, }); } - } - return NextResponse.json( - seniorPatchResponse.parse({ - code: "SUCCESS", - data: senior, - }) - ); - } - } catch (e: any) { - console.log("Error", e); - return NextResponse.json( - seniorPatchResponse.parse({ code: "UNKNOWN", message: "Network error" }), - { status: 500 } - ); - } -}); - -export const POST = withSessionAndRole(["CHAPTER_LEADER"], async (request) => { - try { - const body = await request.req.json(); - const nextParams: { id: string } = request.params; - const { id: userId } = nextParams; - const newSenior = postSeniorSchema.safeParse(body); - - if (!newSenior.success) { - return NextResponse.json( - seniorPostResponse.parse({ - code: "UNKNOWN", - message: "Invalid senior template", - }), - { status: 500 } - ); - } else { - const user = await prisma.user.findFirst({ - where: { - id: userId, - }, - }); - - if (!user) { - return NextResponse.json( - seniorPatchResponse.parse({ - code: "UNKNOWN", - message: "User was not found", - }) - ); - } - const newSeniorData = newSenior.data; - - if (user?.ChapterID != newSeniorData.ChapterID) { - return NextResponse.json( - seniorPatchResponse.parse({ - code: "ERROR", - message: "User has no authority to add", - }) - ); - } - - const baseFolder = "1MVyWBeKCd1erNe9gkwBf7yz3wGa40g9a"; // TODO: make env variable - const fileMetadata = { - name: [`${body.name}-${randomUUID()}`], - mimeType: "application/vnd.google-apps.folder", - parents: [baseFolder], - }; - const fileCreateData = { - resource: fileMetadata, - fields: "id", - }; - - const { access_token, refresh_token } = (await prisma.account.findFirst({ - where: { - userId: request.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 file = await (service as NonNullable).files.create( - fileCreateData - ); - const googleFolderId = (file as any).data.id; - - const senior = await prisma.senior.create({ - data: { - name: body.name, - location: body.location, - description: body.description, - StudentIDs: body.StudentIDs, - ChapterID: body.ChapterID, - folder: googleFolderId, - }, - }); + for (const student of prismaStudentsToAdd) { + //Checks if student has already been added + if (!student.SeniorIDs.includes(seniorId)) { + await prisma.user.update({ + where: { + id: student.id, + }, + data: { + SeniorIDs: [...student.SeniorIDs, seniorId], + }, + }); + } + } - if (!senior) { return NextResponse.json( seniorPatchResponse.parse({ - code: "UNKNOWN", - message: "Adding senior failed", + code: "SUCCESS", + data: senior, }) ); } - + } catch (e: any) { + console.log("Error", e); return NextResponse.json( seniorPatchResponse.parse({ - code: "SUCCESS", - data: senior, - }) + code: "UNKNOWN", + message: "Network error", + }), + { status: 500 } ); } - } catch { - return NextResponse.json( - seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), - { status: 500 } - ); } -}); +); diff --git a/src/app/api/senior/route.client.ts b/src/app/api/senior/route.client.ts new file mode 100644 index 00000000..54a8ff22 --- /dev/null +++ b/src/app/api/senior/route.client.ts @@ -0,0 +1,16 @@ +import { seniorPostResponse, IPostSeniorRequestSchema } from "./route.schema"; + +interface IPostSeniorRequest extends Omit { + body: IPostSeniorRequestSchema; +} + +export const postSenior = async (request: IPostSeniorRequest) => { + const { body, ...options } = request; + const response = await fetch(`/api/senior/`, { + method: "POST", + body: JSON.stringify(body), + ...options, + }); + const json = await response.json(); + return seniorPostResponse.parse(json); +}; diff --git a/src/app/api/senior/route.schema.ts b/src/app/api/senior/route.schema.ts new file mode 100644 index 00000000..5989ec01 --- /dev/null +++ b/src/app/api/senior/route.schema.ts @@ -0,0 +1,18 @@ +import { z } from "zod"; +import { unauthorizedErrorSchema, unknownErrorSchema } from "../route.schema"; + +export const postSeniorSchema = z.object({ + name: z.string(), + location: z.string(), + description: z.string(), + StudentIDs: z.array(z.string()), + ChapterID: z.string(), +}); + +export type IPostSeniorRequestSchema = z.infer; + +export const seniorPostResponse = z.discriminatedUnion("code", [ + z.object({ code: z.literal("SUCCESS") }), + unknownErrorSchema, + unauthorizedErrorSchema, +]); diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts new file mode 100644 index 00000000..355696ab --- /dev/null +++ b/src/app/api/senior/route.ts @@ -0,0 +1,125 @@ +/** + * @todo Migrate the other routes + */ +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"; + +export const POST = withSessionAndRole( + ["CHAPTER_LEADER"], + async ({ req, session, params }) => { + try { + const body = await req.json(); + const nextParams: { id: string } = params; + const { id: userId } = nextParams; + const newSenior = postSeniorSchema.safeParse(body); + + if (!newSenior.success) { + return NextResponse.json( + seniorPostResponse.parse({ + code: "UNKNOWN", + message: "Invalid senior template", + }), + { status: 500 } + ); + } else { + const user = await prisma.user.findFirst({ + where: { + id: userId, + }, + }); + + if (!user) { + return NextResponse.json( + seniorPostResponse.parse({ + code: "UNKNOWN", + message: "User was not found", + }) + ); + } + const newSeniorData = newSenior.data; + + if (user?.ChapterID != newSeniorData.ChapterID) { + return NextResponse.json( + seniorPostResponse.parse({ + code: "ERROR", + message: "User has no authority to add", + }) + ); + } + + const baseFolder = "1MVyWBeKCd1erNe9gkwBf7yz3wGa40g9a"; // TODO: make env variable + const fileMetadata = { + name: [`${body.name}-${randomUUID()}`], + mimeType: "application/vnd.google-apps.folder", + parents: [baseFolder], + }; + const fileCreateData = { + resource: fileMetadata, + 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 file = await ( + service as NonNullable + ).files.create(fileCreateData); + const googleFolderId = (file as any).data.id; + + const senior = await prisma.senior.create({ + data: { + name: body.name, + location: body.location, + description: body.description, + StudentIDs: body.StudentIDs, + ChapterID: body.ChapterID, + folder: googleFolderId, + }, + }); + + if (!senior) { + return NextResponse.json( + seniorPostResponse.parse({ + code: "UNKNOWN", + message: "Adding senior failed", + }) + ); + } + + return NextResponse.json( + seniorPostResponse.parse({ + code: "SUCCESS", + data: senior, + }) + ); + } + } catch { + return NextResponse.json( + seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), + { status: 500 } + ); + } + } +); diff --git a/src/app/private/[uid]/admin/elist/test/page.tsx b/src/app/private/[uid]/admin/elist/test/page.tsx index 0698d514..68cab69a 100644 --- a/src/app/private/[uid]/admin/elist/test/page.tsx +++ b/src/app/private/[uid]/admin/elist/test/page.tsx @@ -3,18 +3,18 @@ import { deleteSenior, patchSenior, - postSenior, } from "src/app/api/senior/[id]/route.client"; -import { IPostSeniorRequestSchema } from "@api/senior/[id]/route.schema"; +import { postSenior } from "@api/senior/route.client"; +import { IPostSeniorRequestSchema } from "@api/senior/route.schema"; function Test() { async function Poo() { postSenior({ - userId: "6587829665af0d81089c42fb", + // userId: "6587829665af0d81089c42fb", body: { - name: "Stephen", - location: "Boston", - description: "He is cool", + name: "Nathan", + location: "Nevada", + description: "Cool beans", StudentIDs: ["6587829665af0d81089c42fb"], ChapterID: "65878595b94284e0c3e02d55", }, @@ -24,24 +24,29 @@ function Test() { } async function Delete() { - deleteSenior({ - seniorId: "65bfe4fe217927ba65687658", + deleteSenior({ seniorId: "65c4263cdcb4f869ef2f2ba9" }); + } + + async function Patch() { + patchSenior({ + seniorId: "65c4263cdcb4f869ef2f2ba9", body: { - userId: "6587829665af0d81089c42fb", + name: "Nathan", + location: "Arkansas", + description: "Cool beans", + StudentIDs: ["6587829665af0d81089c42fb"], + ChapterID: "65878595b94284e0c3e02d55", }, + }).then((res) => { + console.log(res); }); } - // async function Patch() { - // patchSenior({ - - // }) - // } - return (
- + +
); } From 49a9a43ef1bb8a75d7eb79e4b811d04f3edf7943 Mon Sep 17 00:00:00 2001 From: sburchfield33 Date: Wed, 7 Feb 2024 21:50:36 -0500 Subject: [PATCH 06/77] Finished up POST, PATCH, DELETE routes Co-authored-by: nathan-j-edwards --- src/app/api/senior/[id]/route.ts | 187 +++++++----------- src/app/api/senior/route.ts | 174 ++++++++-------- .../private/[uid]/admin/elist/test/page.tsx | 54 ----- src/pages/api/seniors/add.tsx | 108 ---------- 4 files changed, 150 insertions(+), 373 deletions(-) delete mode 100644 src/app/private/[uid]/admin/elist/test/page.tsx delete mode 100644 src/pages/api/seniors/add.tsx diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 694d4c4d..64f7b8db 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -1,23 +1,13 @@ -/** - * @todo Migrate the other routes - */ -import { withSession, withSessionAndRole } from "@server/decorator"; +import { withSessionAndRole } from "@server/decorator"; import { NextResponse } from "next/server"; import { seniorDeleteResponse, seniorPatchResponse, patchSeniorSchema, - seniorSchema, } from "./route.schema"; import { prisma } from "@server/db/client"; -import { request } from "http"; -import { randomUUID } from "crypto"; -import { drive } from "googleapis/build/src/apis/drive"; -import { google } from "googleapis"; -import { env } from "process"; /** - * @todo Enforces that this API request be made with a Chapter leader from the university * @todo Should senior/[id] be nested under /university/[uid]? This design decision is unclear... which is why we want * to wrap call into a client facing function. */ @@ -27,7 +17,9 @@ export const DELETE = withSessionAndRole( const nextParams: { id: string } = params.params; const { id: seniorId } = nextParams; - const maybeSenior = prisma.senior.findFirst({ where: { id: seniorId } }); + const maybeSenior = await prisma.senior.findFirst({ + where: { id: seniorId }, + }); if (maybeSenior == null) { return NextResponse.json( seniorDeleteResponse.parse({ @@ -38,20 +30,7 @@ export const DELETE = withSessionAndRole( ); } - const maybeUser = prisma.user.findUnique({ - where: { id: session.user.id }, - }); - - if (maybeUser == null) { - return NextResponse.json( - seniorDeleteResponse.parse({ - code: "NOT_FOUND", - message: "User not found", - }), - { status: 404 } - ); - } - if (maybeUser.Chapter != maybeSenior.chapter) { + if (session.user.ChapterID != maybeSenior.ChapterID) { return NextResponse.json( seniorDeleteResponse.parse({ code: "UNAUTHORIZED", @@ -84,121 +63,95 @@ export const DELETE = withSessionAndRole( export const PATCH = withSessionAndRole( ["CHAPTER_LEADER"], async ({ req, session, params }) => { - try { - const body = await req.json(); - const nextParams: { seniorid: string } = params; - const { seniorid: seniorId } = nextParams; + const body = await req.json(); + const nextParams: { id: string } = params.params; + const { id: seniorId } = nextParams; - const maybeBody = patchSeniorSchema.safeParse(body); - if (!maybeBody.success) { + const maybeBody = patchSeniorSchema.safeParse(body); + if (!maybeBody.success) { + return NextResponse.json( + seniorPatchResponse.parse({ code: "INVALID_EDIT" }), + { status: 400 } + ); + } else { + const seniorBody = maybeBody.data; + + const maybeSenior = await prisma.senior.findUnique({ + where: { id: seniorId }, + select: { StudentIDs: true }, + }); + if (maybeSenior == null) { return NextResponse.json( - seniorPatchResponse.parse({ code: "INVALID_EDIT" }), - { status: 400 } + seniorPatchResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } ); - } else { - const seniorBody = maybeBody.data; + } - const maybeSenior = await prisma.senior.findUnique({ - where: { id: seniorId }, - select: { StudentIDs: true }, - }); - if (maybeSenior == null) { - return NextResponse.json( - seniorPatchResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } - const maybeUser = await prisma.user.findFirst({ - where: { - id: session.user.id, - }, - }); + if (session.user.ChapterID != seniorBody.ChapterID) { + return NextResponse.json( + seniorDeleteResponse.parse({ + code: "ERROR", + message: "User is not of same chapter of senior", + }), + { status: 404 } + ); + } - if (!maybeUser) { - return NextResponse.json( - seniorDeleteResponse.parse({ - code: "NOT_FOUND", - message: "User not found", - }), - { status: 404 } - ); - } + const senior = await prisma.senior.update({ + where: { + id: seniorId, + }, + data: { + ...seniorBody, + }, + }); - if (maybeUser?.ChapterID != seniorBody.ChapterID) { - return NextResponse.json( - seniorDeleteResponse.parse({ - code: "ERROR", - message: "User is not of same chapter of senior", - }), - { status: 404 } - ); - } + // Remove if senior.studentIds is not contained in body.studentIds + const studentsToRemove = maybeSenior.StudentIDs.filter( + (id) => !seniorBody.StudentIDs.includes(id) + ); + const studentsToAdd = seniorBody.StudentIDs; + + const prismaStudentsToRemove = await prisma.user.findMany({ + where: { id: { in: studentsToRemove } }, + }); + const prismaStudentsToAdd = await prisma.user.findMany({ + where: { id: { in: studentsToAdd } }, + }); - const senior = await prisma.senior.update({ + for (const student of prismaStudentsToRemove) { + await prisma.user.update({ where: { - id: seniorId, + id: student.id, }, data: { - ...seniorBody, + SeniorIDs: student.SeniorIDs.filter((id) => id !== seniorId), }, }); + } - // Remove if senior.studentIds is not contained in body.studentIds - const studentsToRemove = maybeSenior.StudentIDs.filter( - (id) => !seniorBody.StudentIDs.includes(id) - ); - const studentsToAdd = seniorBody.StudentIDs; - - const prismaStudentsToRemove = await prisma.user.findMany({ - where: { id: { in: studentsToRemove } }, - }); - const prismaStudentsToAdd = await prisma.user.findMany({ - where: { id: { in: studentsToAdd } }, - }); - - for (const student of prismaStudentsToRemove) { + for (const student of prismaStudentsToAdd) { + //Checks if student has already been added + if (!student.SeniorIDs.includes(seniorId)) { await prisma.user.update({ where: { id: student.id, }, data: { - SeniorIDs: student.SeniorIDs.filter((id) => id !== seniorId), + SeniorIDs: [...student.SeniorIDs, seniorId], }, }); } - - for (const student of prismaStudentsToAdd) { - //Checks if student has already been added - if (!student.SeniorIDs.includes(seniorId)) { - await prisma.user.update({ - where: { - id: student.id, - }, - data: { - SeniorIDs: [...student.SeniorIDs, seniorId], - }, - }); - } - } - - return NextResponse.json( - seniorPatchResponse.parse({ - code: "SUCCESS", - data: senior, - }) - ); } - } catch (e: any) { - console.log("Error", e); + return NextResponse.json( seniorPatchResponse.parse({ - code: "UNKNOWN", - message: "Network error", - }), - { status: 500 } + code: "SUCCESS", + data: senior, + }) ); } } diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts index 355696ab..784d8824 100644 --- a/src/app/api/senior/route.ts +++ b/src/app/api/senior/route.ts @@ -1,6 +1,3 @@ -/** - * @todo Migrate the other routes - */ import { withSessionAndRole } from "@server/decorator"; import { NextResponse } from "next/server"; import { seniorPostResponse, postSeniorSchema } from "./route.schema"; @@ -11,114 +8,103 @@ import { env } from "process"; export const POST = withSessionAndRole( ["CHAPTER_LEADER"], - async ({ req, session, params }) => { - try { - const body = await req.json(); - const nextParams: { id: string } = params; - const { id: userId } = nextParams; - const newSenior = postSeniorSchema.safeParse(body); + async ({ req, session }) => { + const body = await req.json(); + const newSenior = postSeniorSchema.safeParse(body); - if (!newSenior.success) { + if (!newSenior.success) { + return NextResponse.json( + seniorPostResponse.parse({ + code: "UNKNOWN", + message: "Invalid senior template", + }), + { status: 500 } + ); + } else { + const user = await prisma.user.findFirst({ + where: { + id: session.user.id, + }, + }); + + if (!user) { return NextResponse.json( seniorPostResponse.parse({ code: "UNKNOWN", - message: "Invalid senior template", - }), - { status: 500 } + message: "User was not found", + }) ); - } else { - const user = await prisma.user.findFirst({ - where: { - id: userId, - }, - }); - - if (!user) { - return NextResponse.json( - seniorPostResponse.parse({ - code: "UNKNOWN", - message: "User was not found", - }) - ); - } - const newSeniorData = newSenior.data; - - if (user?.ChapterID != newSeniorData.ChapterID) { - return NextResponse.json( - seniorPostResponse.parse({ - code: "ERROR", - message: "User has no authority to add", - }) - ); - } + } + const newSeniorData = newSenior.data; - const baseFolder = "1MVyWBeKCd1erNe9gkwBf7yz3wGa40g9a"; // TODO: make env variable - const fileMetadata = { - name: [`${body.name}-${randomUUID()}`], - mimeType: "application/vnd.google-apps.folder", - parents: [baseFolder], - }; - const fileCreateData = { - resource: fileMetadata, - fields: "id", - }; + if (session.user.ChapterID != newSeniorData.ChapterID) { + return NextResponse.json( + seniorPostResponse.parse({ + code: "ERROR", + message: "User has no authority to add", + }) + ); + } - 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 baseFolder = "1MVyWBeKCd1erNe9gkwBf7yz3wGa40g9a"; // TODO: make env variable + const fileMetadata = { + name: [`${body.name}-${randomUUID()}`], + mimeType: "application/vnd.google-apps.folder", + parents: [baseFolder], + }; + const fileCreateData = { + resource: fileMetadata, + fields: "id", + }; - const file = await ( - service as NonNullable - ).files.create(fileCreateData); - const googleFolderId = (file as any).data.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 senior = await prisma.senior.create({ - data: { - name: body.name, - location: body.location, - description: body.description, - StudentIDs: body.StudentIDs, - ChapterID: body.ChapterID, - folder: googleFolderId, - }, - }); + const file = await (service as NonNullable).files.create( + fileCreateData + ); + const googleFolderId = (file as any).data.id; - if (!senior) { - return NextResponse.json( - seniorPostResponse.parse({ - code: "UNKNOWN", - message: "Adding senior failed", - }) - ); - } + const senior = await prisma.senior.create({ + data: { + name: body.name, + location: body.location, + description: body.description, + StudentIDs: body.StudentIDs, + ChapterID: body.ChapterID, + folder: googleFolderId, + }, + }); + if (!senior) { return NextResponse.json( seniorPostResponse.parse({ - code: "SUCCESS", - data: senior, + code: "UNKNOWN", + message: "Adding senior failed", }) ); } - } catch { + return NextResponse.json( - seniorPostResponse.parse({ code: "UNKNOWN", message: "Network error" }), - { status: 500 } + seniorPostResponse.parse({ + code: "SUCCESS", + data: senior, + }) ); } } diff --git a/src/app/private/[uid]/admin/elist/test/page.tsx b/src/app/private/[uid]/admin/elist/test/page.tsx deleted file mode 100644 index 68cab69a..00000000 --- a/src/app/private/[uid]/admin/elist/test/page.tsx +++ /dev/null @@ -1,54 +0,0 @@ -"use client"; - -import { - deleteSenior, - patchSenior, -} from "src/app/api/senior/[id]/route.client"; -import { postSenior } from "@api/senior/route.client"; -import { IPostSeniorRequestSchema } from "@api/senior/route.schema"; - -function Test() { - async function Poo() { - postSenior({ - // userId: "6587829665af0d81089c42fb", - body: { - name: "Nathan", - location: "Nevada", - description: "Cool beans", - StudentIDs: ["6587829665af0d81089c42fb"], - ChapterID: "65878595b94284e0c3e02d55", - }, - }).then((res) => { - console.log(res); - }); - } - - async function Delete() { - deleteSenior({ seniorId: "65c4263cdcb4f869ef2f2ba9" }); - } - - async function Patch() { - patchSenior({ - seniorId: "65c4263cdcb4f869ef2f2ba9", - body: { - name: "Nathan", - location: "Arkansas", - description: "Cool beans", - StudentIDs: ["6587829665af0d81089c42fb"], - ChapterID: "65878595b94284e0c3e02d55", - }, - }).then((res) => { - console.log(res); - }); - } - - return ( -
- - - -
- ); -} - -export default Test; diff --git a/src/pages/api/seniors/add.tsx b/src/pages/api/seniors/add.tsx deleted file mode 100644 index 99d3beb0..00000000 --- a/src/pages/api/seniors/add.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { prisma } from "@server/db/client"; -import { z } from "zod"; -import { getServerAuthSession } from "@server/common/get-server-auth-session"; -import drive from "../drive/drive"; -import { randomUUID } from "crypto"; - -const add = async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerAuthSession({ req, res }); - - if (!session || !session.user) { - res.status(401).json({ - error: "This route is protected. In order to access it, please sign in.", - }); - return; - } - - const userId = session.user.id; - - switch (req.method) { - case "POST": - try { - const { admin } = (await prisma.user.findUnique({ - where: { - id: userId, - }, - select: { - admin: true, - }, - })) ?? { admin: false }; - - if (admin) { - const bodySchema = z.object({ - name: z.string(), - location: z.string(), - description: z.string(), - StudentIDs: z.array(z.string()), - }); - - const body = bodySchema.parse(JSON.parse(req.body)); - const baseFolder = "1MVyWBeKCd1erNe9gkwBf7yz3wGa40g9a"; // TODO: make env variable - const fileMetadata = { - name: [`${body.name}-${randomUUID()}`], - mimeType: "application/vnd.google-apps.folder", - parents: [baseFolder], - }; - const fileCreateData = { - resource: fileMetadata, - fields: "id", - }; - - const service = await drive(req, res); - const file = await ( - service as NonNullable - ).files.create(fileCreateData); - const googleFolderId = (file as any).data.id; - - console.log("Before creating senior..."); - const senior = await prisma.senior.create({ - data: { - name: body.name, - location: body.location, - description: body.description, - StudentIDs: body.StudentIDs, - folder: googleFolderId, - }, - }); - - const prismaStudentsToAdd = await prisma.user.findMany({ - where: { id: { in: body.StudentIDs } }, - }); - - for (const student of prismaStudentsToAdd) { - await prisma.user.update({ - where: { - id: student.id, - }, - data: { - SeniorIDs: [...student.SeniorIDs, senior.id], - }, - }); - } - - res.status(200).json(senior); - } else { - res.status(500).json({ - error: - "This route is protected. In order to access it, please sign in as admin.", - }); - return; - } - } catch (error) { - console.log("Error", error); - res.status(500).json({ - error: `Failed to create senior: ${error}`, - }); - } - break; - - default: - res.status(500).json({ - error: `Method ${req.method} not implemented`, - }); - break; - } -}; - -export default add; From 5f6cab2559f30dee49dd0b302083a3c75f013f54 Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Sun, 11 Feb 2024 11:50:27 -0500 Subject: [PATCH 07/77] Add drive service TODOs --- src/app/api/senior/[id]/route.ts | 3 +-- src/app/api/senior/route.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 64f7b8db..dc810881 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -8,8 +8,7 @@ import { import { prisma } from "@server/db/client"; /** - * @todo Should senior/[id] be nested under /university/[uid]? This design decision is unclear... which is why we want - * to wrap call into a client facing function. + * @TODO - Delete folder belonging to the senior */ export const DELETE = withSessionAndRole( ["CHAPTER_LEADER"], diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts index 784d8824..7d58cb98 100644 --- a/src/app/api/senior/route.ts +++ b/src/app/api/senior/route.ts @@ -6,6 +6,7 @@ import { randomUUID } from "crypto"; import { google } from "googleapis"; import { env } from "process"; +// @TODO - Use google drive service to create folder export const POST = withSessionAndRole( ["CHAPTER_LEADER"], async ({ req, session }) => { From aa8f6b58b59e477f5f662198825165b1dbce4a50 Mon Sep 17 00:00:00 2001 From: sburchfield33 Date: Sun, 11 Feb 2024 15:07:43 -0500 Subject: [PATCH 08/77] Corrected accoringly to PR comments and attempted to fix the POST request Co-authored-by: nathan-j-edwards --- src/app/api/senior/[id]/route.client.ts | 1 - src/app/api/senior/[id]/route.schema.ts | 20 +------ src/app/api/senior/[id]/route.ts | 7 ++- src/app/api/senior/route.schema.ts | 13 ++--- src/app/api/senior/route.ts | 23 +++----- .../private/[uid]/admin/elist/test/page.tsx | 53 +++++++++++++++++++ src/server/model/index.ts | 12 +++++ 7 files changed, 87 insertions(+), 42 deletions(-) create mode 100644 src/app/private/[uid]/admin/elist/test/page.tsx create mode 100644 src/server/model/index.ts diff --git a/src/app/api/senior/[id]/route.client.ts b/src/app/api/senior/[id]/route.client.ts index 719741bb..509a3004 100644 --- a/src/app/api/senior/[id]/route.client.ts +++ b/src/app/api/senior/[id]/route.client.ts @@ -8,7 +8,6 @@ interface IDeleteSeniorRequest extends Omit { seniorId: string; } -/* Note talk to nick about how to best pass the userId and the seniorId */ interface IPatchSeniorRequest extends Omit { seniorId: string; body: IPatchSeniorRequestSchema; diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index 1a710fd3..c52dc80b 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -4,6 +4,7 @@ import { unknownErrorSchema, } from "../../route.schema"; import { Senior } from "@prisma/client"; +import { seniorSchema } from "@server/model"; export const seniorDeleteResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS") }), @@ -15,32 +16,15 @@ export const seniorDeleteResponse = z.discriminatedUnion("code", [ unauthorizedErrorSchema, ]); -export const seniorSchema = z.object({ - id: z.string(), - name: z.string(), - location: z.string(), - description: z.string(), - StudentIDs: z.array(z.string()), - folder: z.string(), - ChapterID: z.string(), -}) satisfies z.ZodType; + export const patchSeniorSchema = z.object({ name: z.string(), location: z.string(), description: z.string(), StudentIDs: z.array(z.string()), - ChapterID: z.string(), }); -/* export const postSeniorSchema = z.object({ - name: z.string(), - location: z.string(), - description: z.string(), - StudentIDs: z.array(z.string()), - folder: z.string(), - chapterID: z.string(), -}); */ export type ISeniorSchema = z.infer; diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 64f7b8db..f72edfc1 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -6,6 +6,7 @@ import { patchSeniorSchema, } from "./route.schema"; import { prisma } from "@server/db/client"; +import { seniorSchema } from "@server/model"; /** * @todo Should senior/[id] be nested under /university/[uid]? This design decision is unclear... which is why we want @@ -78,7 +79,7 @@ export const PATCH = withSessionAndRole( const maybeSenior = await prisma.senior.findUnique({ where: { id: seniorId }, - select: { StudentIDs: true }, + include: { Students: true }, }); if (maybeSenior == null) { return NextResponse.json( @@ -90,7 +91,9 @@ export const PATCH = withSessionAndRole( ); } - if (session.user.ChapterID != seniorBody.ChapterID) { + + + if (session.user.ChapterID != maybeSenior.ChapterID) { return NextResponse.json( seniorDeleteResponse.parse({ code: "ERROR", diff --git a/src/app/api/senior/route.schema.ts b/src/app/api/senior/route.schema.ts index 5989ec01..f6527a13 100644 --- a/src/app/api/senior/route.schema.ts +++ b/src/app/api/senior/route.schema.ts @@ -1,18 +1,19 @@ import { z } from "zod"; import { unauthorizedErrorSchema, unknownErrorSchema } from "../route.schema"; +import { seniorSchema } from "@server/model"; export const postSeniorSchema = z.object({ - name: z.string(), - location: z.string(), - description: z.string(), - StudentIDs: z.array(z.string()), - ChapterID: z.string(), + name: z.string(), + location: z.string(), + description: z.string(), + StudentIDs: z.array(z.string()), + ChapterID: z.string(), }); export type IPostSeniorRequestSchema = z.infer; export const seniorPostResponse = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS") }), + z.object({ code: z.literal("SUCCESS"), data: seniorSchema }), unknownErrorSchema, unauthorizedErrorSchema, ]); diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts index 784d8824..5b0167ea 100644 --- a/src/app/api/senior/route.ts +++ b/src/app/api/senior/route.ts @@ -15,10 +15,10 @@ export const POST = withSessionAndRole( if (!newSenior.success) { return NextResponse.json( seniorPostResponse.parse({ - code: "UNKNOWN", + code: "INVALID_REQUEST", message: "Invalid senior template", }), - { status: 500 } + { status: 400 } ); } else { const user = await prisma.user.findFirst({ @@ -30,9 +30,10 @@ export const POST = withSessionAndRole( if (!user) { return NextResponse.json( seniorPostResponse.parse({ - code: "UNKNOWN", - message: "User was not found", - }) + code: "INVALID_REQUEST", + message: "User not found", + }), + { status: 400 } ); } const newSeniorData = newSenior.data; @@ -40,7 +41,7 @@ export const POST = withSessionAndRole( if (session.user.ChapterID != newSeniorData.ChapterID) { return NextResponse.json( seniorPostResponse.parse({ - code: "ERROR", + code: "UNAUTHORIZED", message: "User has no authority to add", }) ); @@ -85,20 +86,12 @@ export const POST = withSessionAndRole( name: body.name, location: body.location, description: body.description, - StudentIDs: body.StudentIDs, ChapterID: body.ChapterID, folder: googleFolderId, }, }); - if (!senior) { - return NextResponse.json( - seniorPostResponse.parse({ - code: "UNKNOWN", - message: "Adding senior failed", - }) - ); - } + /* Run push for each of the student IDs */ return NextResponse.json( seniorPostResponse.parse({ diff --git a/src/app/private/[uid]/admin/elist/test/page.tsx b/src/app/private/[uid]/admin/elist/test/page.tsx new file mode 100644 index 00000000..010a7aa3 --- /dev/null +++ b/src/app/private/[uid]/admin/elist/test/page.tsx @@ -0,0 +1,53 @@ +"use client"; +import { + deleteSenior, + patchSenior, +} from "src/app/api/senior/[id]/route.client"; +import { postSenior } from "@api/senior/route.client"; + +function Test() { + async function Post() { + postSenior({ + body: { + name: "Stephen", + location: "Boston", + description: "He is cool", + StudentIDs: ["6587829665af0d81089c42fb"], + ChapterID: "65878595b94284e0c3e02d55", + }, + }).then((res) => { + console.log(res); + }); + } + async function Delete() { + deleteSenior({ + seniorId: "65bfe4fe217927ba65687658", + }).then((res) => { + console.log(res); + }); + } + + async function Patch() { + patchSenior({ + seniorId: "65c4263cdcb4f869ef2f2ba9", + body: { + name: "Nathan", + location: "Arkansas", + description: "Cool beans", + StudentIDs: ["6587829665af0d81089c42fb"], + }, + }).then((res) => { + console.log(res); + }); + } + + return ( +
+ + + +
+ ); +} + +export default Test; diff --git a/src/server/model/index.ts b/src/server/model/index.ts new file mode 100644 index 00000000..ca04b7b6 --- /dev/null +++ b/src/server/model/index.ts @@ -0,0 +1,12 @@ +import { Senior } from "@prisma/client"; +import { z } from "zod"; + +export const seniorSchema = z.object({ + id: z.string(), + name: z.string(), + location: z.string(), + description: z.string(), + StudentIDs: z.array(z.string()), + folder: z.string(), + ChapterID: z.string(), +}) satisfies z.ZodType; \ No newline at end of file From 9fcea9da783408b898c60a7904e0bedafe46c38c Mon Sep 17 00:00:00 2001 From: sburchfield33 Date: Wed, 14 Feb 2024 16:58:10 -0500 Subject: [PATCH 09/77] Dealt with comments on PR and confirmed the connection between seniors and students --- src/app/api/senior/route.ts | 13 ++++- .../private/[uid]/admin/elist/test/page.tsx | 53 ------------------- 2 files changed, 12 insertions(+), 54 deletions(-) delete mode 100644 src/app/private/[uid]/admin/elist/test/page.tsx diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts index e9589013..4de8ba6b 100644 --- a/src/app/api/senior/route.ts +++ b/src/app/api/senior/route.ts @@ -88,11 +88,22 @@ export const POST = withSessionAndRole( location: body.location, description: body.description, ChapterID: body.ChapterID, + StudentIDs: body.StudentIDs, folder: googleFolderId, }, }); - /* Run push for each of the student IDs */ + body.StudentIDs.map(async (studentID: string) => { + await prisma.user.update({ + where: { id: studentID }, + data: { + SeniorIDs: { + push: senior.id, + }, + }, + }); + }); + return NextResponse.json( seniorPostResponse.parse({ diff --git a/src/app/private/[uid]/admin/elist/test/page.tsx b/src/app/private/[uid]/admin/elist/test/page.tsx deleted file mode 100644 index 010a7aa3..00000000 --- a/src/app/private/[uid]/admin/elist/test/page.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; -import { - deleteSenior, - patchSenior, -} from "src/app/api/senior/[id]/route.client"; -import { postSenior } from "@api/senior/route.client"; - -function Test() { - async function Post() { - postSenior({ - body: { - name: "Stephen", - location: "Boston", - description: "He is cool", - StudentIDs: ["6587829665af0d81089c42fb"], - ChapterID: "65878595b94284e0c3e02d55", - }, - }).then((res) => { - console.log(res); - }); - } - async function Delete() { - deleteSenior({ - seniorId: "65bfe4fe217927ba65687658", - }).then((res) => { - console.log(res); - }); - } - - async function Patch() { - patchSenior({ - seniorId: "65c4263cdcb4f869ef2f2ba9", - body: { - name: "Nathan", - location: "Arkansas", - description: "Cool beans", - StudentIDs: ["6587829665af0d81089c42fb"], - }, - }).then((res) => { - console.log(res); - }); - } - - return ( -
- - - -
- ); -} - -export default Test; From 6f8a57fc6663e8c4b8c46b12a8d5ff118fe02721 Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Fri, 16 Feb 2024 00:37:24 -0500 Subject: [PATCH 10/77] Add styling fix --- src/app/api/senior/[id]/route.schema.ts | 16 ---------------- src/app/api/senior/[id]/route.ts | 3 +-- src/app/api/senior/route.schema.ts | 10 +++++----- 3 files changed, 6 insertions(+), 23 deletions(-) diff --git a/src/app/api/senior/[id]/route.schema.ts b/src/app/api/senior/[id]/route.schema.ts index c52dc80b..66418971 100644 --- a/src/app/api/senior/[id]/route.schema.ts +++ b/src/app/api/senior/[id]/route.schema.ts @@ -3,7 +3,6 @@ import { unauthorizedErrorSchema, unknownErrorSchema, } from "../../route.schema"; -import { Senior } from "@prisma/client"; import { seniorSchema } from "@server/model"; export const seniorDeleteResponse = z.discriminatedUnion("code", [ @@ -16,8 +15,6 @@ export const seniorDeleteResponse = z.discriminatedUnion("code", [ unauthorizedErrorSchema, ]); - - export const patchSeniorSchema = z.object({ name: z.string(), location: z.string(), @@ -25,7 +22,6 @@ export const patchSeniorSchema = z.object({ StudentIDs: z.array(z.string()), }); - export type ISeniorSchema = z.infer; export type IPatchSeniorRequestSchema = z.infer; @@ -40,15 +36,3 @@ export const seniorPatchResponse = z.discriminatedUnion("code", [ unknownErrorSchema, unauthorizedErrorSchema, ]); - -// export const seniorGetResponse = z.discriminatedUnion("code", [ - -// z.object({ code: z.literal("SUCCESS"), data: getSeniorSchema }), -// z.object({ -// code: z.literal("NOT_FOUND"), -// message: z.string(), -// }), -// unknownErrorSchema, -// unauthorizedErrorSchema, - -// ]); diff --git a/src/app/api/senior/[id]/route.ts b/src/app/api/senior/[id]/route.ts index 993ad294..6a619581 100644 --- a/src/app/api/senior/[id]/route.ts +++ b/src/app/api/senior/[id]/route.ts @@ -80,6 +80,7 @@ export const PATCH = withSessionAndRole( where: { id: seniorId }, include: { Students: true }, }); + if (maybeSenior == null) { return NextResponse.json( seniorPatchResponse.parse({ @@ -90,8 +91,6 @@ export const PATCH = withSessionAndRole( ); } - - if (session.user.ChapterID != maybeSenior.ChapterID) { return NextResponse.json( seniorDeleteResponse.parse({ diff --git a/src/app/api/senior/route.schema.ts b/src/app/api/senior/route.schema.ts index f6527a13..c590e479 100644 --- a/src/app/api/senior/route.schema.ts +++ b/src/app/api/senior/route.schema.ts @@ -3,11 +3,11 @@ import { unauthorizedErrorSchema, unknownErrorSchema } from "../route.schema"; import { seniorSchema } from "@server/model"; export const postSeniorSchema = z.object({ - name: z.string(), - location: z.string(), - description: z.string(), - StudentIDs: z.array(z.string()), - ChapterID: z.string(), + name: z.string(), + location: z.string(), + description: z.string(), + StudentIDs: z.array(z.string()), + ChapterID: z.string(), }); export type IPostSeniorRequestSchema = z.infer; From 4ea95d7ee5254d9b99c1fdb7df773dea418c9768 Mon Sep 17 00:00:00 2001 From: sburchfield33 Date: Sat, 17 Feb 2024 17:46:31 -0500 Subject: [PATCH 11/77] Fixed final edits from Nick --- src/app/api/senior/route.schema.ts | 1 - src/app/api/senior/route.ts | 23 ++++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/api/senior/route.schema.ts b/src/app/api/senior/route.schema.ts index c590e479..d525e3b5 100644 --- a/src/app/api/senior/route.schema.ts +++ b/src/app/api/senior/route.schema.ts @@ -7,7 +7,6 @@ export const postSeniorSchema = z.object({ location: z.string(), description: z.string(), StudentIDs: z.array(z.string()), - ChapterID: z.string(), }); export type IPostSeniorRequestSchema = z.infer; diff --git a/src/app/api/senior/route.ts b/src/app/api/senior/route.ts index 4de8ba6b..404e694a 100644 --- a/src/app/api/senior/route.ts +++ b/src/app/api/senior/route.ts @@ -37,9 +37,8 @@ export const POST = withSessionAndRole( { status: 400 } ); } - const newSeniorData = newSenior.data; - if (session.user.ChapterID != newSeniorData.ChapterID) { + if (session.user.ChapterID == null) { return NextResponse.json( seniorPostResponse.parse({ code: "UNAUTHORIZED", @@ -87,21 +86,23 @@ export const POST = withSessionAndRole( name: body.name, location: body.location, description: body.description, - ChapterID: body.ChapterID, + ChapterID: session.user.ChapterID, StudentIDs: body.StudentIDs, folder: googleFolderId, }, }); - body.StudentIDs.map(async (studentID: string) => { - await prisma.user.update({ - where: { id: studentID }, - data: { - SeniorIDs: { - push: senior.id, - }, + await prisma.user.updateMany({ + data: { + SeniorIDs: { + push: senior.id, }, - }); + }, + where: { + id: { + in: senior.StudentIDs, + }, + }, }); From b1938f3e91a2259a07b6495ca8be17fffff3026c Mon Sep 17 00:00:00 2001 From: wkim10 Date: Sun, 28 Jan 2024 13:48:24 -0500 Subject: [PATCH 12/77] finished POST request Co-authored-by: JuliaZel Co-authored-by: Nick Doan --- prisma/schema.prisma | 6 +-- src/app/api/file/route.client.ts | 71 +++++++++++++++++++++++++++ src/app/api/file/route.schema.ts | 51 ++++++++++++++++++++ src/app/api/file/route.ts | 82 ++++++++++++++++++++++++++++++++ 4 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 src/app/api/file/route.client.ts create mode 100644 src/app/api/file/route.schema.ts create mode 100644 src/app/api/file/route.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ea7aec66..79736112 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,14 +99,14 @@ model Senior { model File { id String @id @default(auto()) @map("_id") @db.ObjectId - name String - description String + date DateTime // will restrict hours to midnight filetype String - lastModified DateTime url String seniorId String @db.ObjectId senior Senior @relation(fields: [seniorId], references: [id], onDelete: Cascade) Tags String[] + + @@unique([seniorId, date]) } model ChapterRequest { diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts new file mode 100644 index 00000000..3172171f --- /dev/null +++ b/src/app/api/file/route.client.ts @@ -0,0 +1,71 @@ +import { z } from "zod"; +import { File, FileResponse } from "./route.schema"; + +/** + * Describe the interface of SignInRequest. + */ +type IFile = z.infer; + +type IFileResponse = z.infer; + +/** + * Extract all the values of "code". + */ +type FileResponseCode = z.infer["code"]; + +/** + * Extends the parameters of fetch() function to give types to the RequestBody. + */ +interface IRequest extends Omit { + body: IFile; +} + +const MOCK_SUCCESS: IFileResponse = { + code: "SUCCESS", + message: "File successfully added", +}; + +const MOCK_DUPLICATE_DATE: IFileResponse = { + code: "DUPLICATE_DATE", + message: "A file associated with this date already exists", +}; + +const MOCK_UNKNOWN: IFileResponse = { + code: "UNKNOWN", + message: "Unknown error received", +}; + +const MOCK_INVALID_FILE: IFileResponse = { + code: "INVALID_FILE", + message: "Invalid file added", +}; + +/** + * 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 createFile = async ( + request: IRequest, + mock?: FileResponseCode +) => { + if (mock === "SUCCESS") { + return FileResponse.parse(MOCK_SUCCESS); + } else if (mock === "INVALID_FILE") { + return FileResponse.parse(MOCK_INVALID_FILE); + } else if (mock === "DUPLICATE_DATE") { + return FileResponse.parse(MOCK_DUPLICATE_DATE); + } else if (mock === "UNKNOWN") { + return FileResponse.parse(MOCK_UNKNOWN); + } + const { body, ...options } = request; + const response = await fetch("/api/file", { + method: "POST", + body: JSON.stringify(body), + ...options, + }); + const json = await response.json(); + + return FileResponse.parse(json); +}; diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts new file mode 100644 index 00000000..a4450c15 --- /dev/null +++ b/src/app/api/file/route.schema.ts @@ -0,0 +1,51 @@ +import { z } from "zod"; +import { Prisma } from "@prisma/client"; +import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; +/* + id String @id @default(auto()) @map("_id") @db.ObjectId + date DateTime // will restrict hours to midnight + filetype String + url String + seniorId String @db.ObjectId + senior Senior @relation(fields: [seniorId], references: [id], onDelete: Cascade) + Tags String[] + + @@unique([seniorId, date]) +*/ + +export const File = z.object({ + date: z.date(), + filetype: z.string(), + url: z.string(), + Tags: z.array(z.string()), + seniorId: z.string(), +}); + +export const FileResponse = z.discriminatedUnion("code", [ + z.object({ + code: z.literal("SUCCESS"), + message: z.literal("File successfully added"), + }), + z.object({ + code: z.literal("INVALID_FILE"), + message: z.literal("Invalid file added"), + }), + z.object({ + code: z.literal("UNKNOWN"), + message: z.literal("Unknown error received"), + }), + z.object({ + code: z.literal("DUPLICATE_DATE"), + message: z.literal("A file associated with this date already exists"), + }), +]); + +export const ResponsefileDelete = z.discriminatedUnion("code", [ + z.object({ code: z.literal("SUCCESS") }), + z.object({ + code: z.literal("NOT_FOUND"), + message: z.string(), + }), + unknownErrorSchema, + unauthorizedErrorSchema, +]); diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts new file mode 100644 index 00000000..fc616c64 --- /dev/null +++ b/src/app/api/file/route.ts @@ -0,0 +1,82 @@ +import { NextResponse } from "next/server"; +import { File, FileResponse } from "./route.schema"; +import { prisma } from "@server/db/client"; +import { withSession } from "@server/decorator"; +import { google } from "googleapis"; + +export const POST = withSession(async (request) => { + try { + const auth = new google.auth.OAuth2({ + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET, + }); + + const service = google.drive({ + version: "v3", + auth, + }); + + const fileData = File.parse(request.req); + + // get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); + if (foundSenior == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + + const parentID = foundSenior.folder.split("/").pop(); + + // TODO: date conversion to readable format for title + const fileMetadata = { + name: [fileData.date.toString()], + mimeType: "application/vnd.google-apps.document", + parents: [parentID], + }; + + const fileCreateData = { + resource: fileMetadata, + fields: "id", + }; + + const file = await service?.files.create(fileCreateData); + + const googleFileId = file.data.id; // used to have (file as any) - do we need this? + + // If the data is valid, save it to the database via prisma client + const fileEntry = await prisma?.file.create({ + data: { + date: fileData.date, + filetype: fileData.filetype, + url: `https://docs.google.com/document/d/${googleFileId}`, + seniorId: fileData.seniorId, + Tags: fileData.Tags, + }, + }); + + console.log(fileEntry); + + return NextResponse.json( + FileResponse.parse({ + code: "SUCCESS", + message: "File successfully created", + }), + { status: 200 } + ); + } catch { + return NextResponse.json( + FileResponse.parse({ + code: "UNKNOWN", + message: "Unknown error received", + }), + { status: 500 } + ); + } +}); From 6de66968283039eb57c43853577958b900f1e71a Mon Sep 17 00:00:00 2001 From: wkim10 Date: Tue, 30 Jan 2024 12:16:03 -0500 Subject: [PATCH 13/77] debugging attempts for POST request Co-authored-by: JuliaZel --- prisma/schema.prisma | 2 +- src/app/api/file/route.client.ts | 9 ++ src/app/api/file/route.schema.ts | 2 +- src/app/api/file/route.ts | 140 +++++++++++++++++++++---------- src/components/Sidebar.tsx | 30 +++++++ src/pages/api/file/index.tsx | 55 ------------ src/pages/senior/[id].tsx | 42 +++++----- 7 files changed, 158 insertions(+), 122 deletions(-) delete mode 100644 src/pages/api/file/index.tsx diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 79736112..a214fc8a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,7 +99,7 @@ model Senior { model File { id String @id @default(auto()) @map("_id") @db.ObjectId - date DateTime // will restrict hours to midnight + date String // will restrict hours to midnight filetype String url String seniorId String @db.ObjectId diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index 3172171f..31ba7964 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -59,13 +59,22 @@ export const createFile = async ( } else if (mock === "UNKNOWN") { return FileResponse.parse(MOCK_UNKNOWN); } + console.log("About to start post request:"); const { body, ...options } = request; + console.log(body); + console.log(request); + console.log(JSON.stringify(body)); const response = await fetch("/api/file", { method: "POST", body: JSON.stringify(body), ...options, }); + + console.log("We've made our response"); + const json = await response.json(); + console.log("Made json: ", json); + console.log("Returning from createFile function..."); return FileResponse.parse(json); }; diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index a4450c15..0b0dad27 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -14,7 +14,7 @@ import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; */ export const File = z.object({ - date: z.date(), + date: z.string(), filetype: z.string(), url: z.string(), Tags: z.array(z.string()), diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index fc616c64..6b199b61 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -5,71 +5,121 @@ import { withSession } from "@server/decorator"; import { google } from "googleapis"; export const POST = withSession(async (request) => { + console.log("IN POST"); + try { + console.log("STARTING TRY"); const auth = new google.auth.OAuth2({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }); + console.log("SERVICE"); const service = google.drive({ version: "v3", auth, }); - const fileData = File.parse(request.req); + console.log("FILEDATA"); + // console.log(request); + // console.log(request.req); + // console.log(request.req.body); + // const fileData = File.parse(request.req); - // get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: fileData.seniorId }, - }); - if (foundSenior == null) { + const fileRequest = File.safeParse(await request.req.json()); + console.log("RIGHT AFTER PROCESSING FILE DATA"); + + console.log("!fileRequest.success --> ", !fileRequest.success); + if (!fileRequest.success) { + console.log("Not successful file data"); + console.log(fileRequest.error); return NextResponse.json( FileResponse.parse({ code: "NOT_FOUND", - message: "Senior not found", + message: "Unsuccessful request creation", }), - { status: 404 } + { status: 400 } ); - } + } else { + console.log("success"); + const fileData = fileRequest.data; + console.log(fileRequest); + console.log(fileRequest.data); + // const fileData = File.parse(JSON.parse()); - const parentID = foundSenior.folder.split("/").pop(); - - // TODO: date conversion to readable format for title - const fileMetadata = { - name: [fileData.date.toString()], - mimeType: "application/vnd.google-apps.document", - parents: [parentID], - }; - - const fileCreateData = { - resource: fileMetadata, - fields: "id", - }; - - const file = await service?.files.create(fileCreateData); - - const googleFileId = file.data.id; // used to have (file as any) - do we need this? - - // If the data is valid, save it to the database via prisma client - const fileEntry = await prisma?.file.create({ - data: { - date: fileData.date, - filetype: fileData.filetype, - url: `https://docs.google.com/document/d/${googleFileId}`, - seniorId: fileData.seniorId, - Tags: fileData.Tags, - }, - }); + console.log("SENIOR"); + // get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); + if (foundSenior == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } - console.log(fileEntry); + console.log("PARENTID"); + const parentID = foundSenior.folder.split("/").pop(); - return NextResponse.json( - FileResponse.parse({ - code: "SUCCESS", - message: "File successfully created", - }), - { status: 200 } - ); + console.log("FILEMETADATA"); + // TODO: date conversion to readable format for title + // const fileMetadata = { + // name: [fileData.date.toString()], + // mimeType: "application/vnd.google-apps.document", + // parents: [parentID], + // }; + + const fileMetadata = { + name: [fileData.date], + mimeType: "application/vnd.google-apps.document", + parents: [parentID], + }; + + console.log("FILE CREATE DATA"); + const fileCreateData = { + resource: fileMetadata, + fields: "id", + }; + + console.log("FILE"); + // console.log(service); + // console.log(service == null); + console.log(fileCreateData); + + const file = await (service as NonNullable).files.create( + fileCreateData + ); + console.log(file); + + console.log("GOOGLE FIELD ID"); + const googleFileId = file.data.id; // used to have (file as any) - do we need this? + + console.log("FILE ENTRY"); + // If the data is valid, save it to the database via prisma client + const fileEntry = await prisma?.file.create({ + data: { + date: fileData.date, + filetype: fileData.filetype, + url: `https://docs.google.com/document/d/${googleFileId}`, + seniorId: fileData.seniorId, + Tags: fileData.Tags, + }, + }); + + console.log(fileEntry); + console.log("SENDING SUCCESS"); + return NextResponse.json( + FileResponse.parse({ + code: "SUCCESS", + message: "File successfully created", + }), + { status: 200 } + ); + } } catch { return NextResponse.json( FileResponse.parse({ diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 192b552c..f300e7e9 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,6 +12,9 @@ import { UserContext } from "src/context/UserProvider"; import Logo from "@public/icons/logo.svg"; import Image from "next/image"; +// todo: DELETE this (for testing purposes ) +import { createFile } from "src/app/api/file/route.client"; + interface Button { name: string; icon: IconDefinition; @@ -22,6 +25,30 @@ export interface ISideBar { buttons: Button[]; } +interface IRequest extends Omit { + body: File; +} + +export interface File { + date: string; + filetype: string; + url: string; + Tags: string[]; + seniorId: string; +} + +const myFile: File = { + date: "Jan 30 2024", + filetype: "Google Document", + url: "", + Tags: ["Adolescence", "Marriage", "Early childhood"], + seniorId: "65ad4d19a029b78419e9265c", +}; + +const myRequest: IRequest = { + body: myFile, +}; + const SidebarItem = ({ label, iconName, @@ -75,6 +102,9 @@ const Sidebar = ({ buttons }: ISideBar) => {
Tufts University
*/} +
{user.name ?? ""} diff --git a/src/pages/api/file/index.tsx b/src/pages/api/file/index.tsx deleted file mode 100644 index 441816f3..00000000 --- a/src/pages/api/file/index.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { getServerAuthSession } from "@server/common/get-server-auth-session"; -import { prisma } from "@server/db/client"; -import type { NextApiRequest, NextApiResponse } from "next"; - -const files = async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerAuthSession({ req, res }); - - if (!session || !session.user) { - res.status(401).json({ - error: "This route is protected. In order to access it, please sign in.", - }); - return; - } - - const { id: seniorId } = req.query; - if (typeof seniorId !== "string") { - res.status(500).json({ - error: `seniorId must be a string`, - }); - return; - } - - switch (req.method) { - case "GET": - try { - const files = await prisma.file.findMany({ - where: { - seniorId: seniorId, - }, - }); - - if (!files) { - res.status(404).json({ - error: `cannot find any files associated with ${seniorId}`, - }); - return; - } - - res.status(200).json(files); - } catch (error) { - res.status(500).json({ - error: `failed to fetch student: ${error}`, - }); - } - break; - - default: - res.status(500).json({ - error: `method ${req.method} not implemented`, - }); - break; - } -}; - -export default files; diff --git a/src/pages/senior/[id].tsx b/src/pages/senior/[id].tsx index 07d30136..68100297 100644 --- a/src/pages/senior/[id].tsx +++ b/src/pages/senior/[id].tsx @@ -58,13 +58,13 @@ const SeniorProfile = ({ senior }: ISeniorProfileProps) => { /> ) : null}
-

+

{senior.name}

{senior.location}

-
-

- {senior.description} +

+

+ {senior.description}

@@ -78,22 +78,24 @@ const SeniorProfile = ({ senior }: ISeniorProfileProps) => {
- + {filteredFiles.map((file, key) => (
Date: Wed, 31 Jan 2024 20:04:49 -0500 Subject: [PATCH 14/77] complete first iteration of file POST request Co-authored-by: JuliaZel --- prisma/schema.prisma | 2 +- src/app/api/file/route.client.ts | 2 +- src/app/api/file/route.schema.ts | 2 +- src/app/api/file/route.ts | 73 +++++++++++++------------------- src/components/Sidebar.tsx | 4 +- 5 files changed, 35 insertions(+), 48 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a214fc8a..79736112 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,7 +99,7 @@ model Senior { model File { id String @id @default(auto()) @map("_id") @db.ObjectId - date String // will restrict hours to midnight + date DateTime // will restrict hours to midnight filetype String url String seniorId String @db.ObjectId diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index 31ba7964..a711e9d2 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -63,7 +63,7 @@ export const createFile = async ( const { body, ...options } = request; console.log(body); console.log(request); - console.log(JSON.stringify(body)); + // console.log(JSON.stringify(body)); const response = await fetch("/api/file", { method: "POST", body: JSON.stringify(body), diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index 0b0dad27..a4450c15 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -14,7 +14,7 @@ import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; */ export const File = z.object({ - date: z.string(), + date: z.date(), filetype: z.string(), url: z.string(), Tags: z.array(z.string()), diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 6b199b61..f94e6f92 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -1,37 +1,38 @@ import { NextResponse } from "next/server"; import { File, FileResponse } from "./route.schema"; -import { prisma } from "@server/db/client"; -import { withSession } from "@server/decorator"; import { google } from "googleapis"; +import { prisma } from "@server/db/client"; +import { withSession } from "../../../server/decorator"; export const POST = withSession(async (request) => { - console.log("IN POST"); - try { - console.log("STARTING TRY"); + const { access_token, refresh_token } = (await prisma.account.findFirst({ + where: { + userId: request.session.user.id, + }, + })) ?? { access_token: null }; + const auth = new google.auth.OAuth2({ clientId: process.env.GOOGLE_CLIENT_ID, clientSecret: process.env.GOOGLE_CLIENT_SECRET, }); - console.log("SERVICE"); + auth.setCredentials({ + access_token, + refresh_token, + }); + const service = google.drive({ version: "v3", auth, }); - console.log("FILEDATA"); - // console.log(request); - // console.log(request.req); - // console.log(request.req.body); - // const fileData = File.parse(request.req); + const body = await request.req.json(); + body.date = new Date(body.date); - const fileRequest = File.safeParse(await request.req.json()); - console.log("RIGHT AFTER PROCESSING FILE DATA"); + const fileRequest = File.safeParse(body); - console.log("!fileRequest.success --> ", !fileRequest.success); if (!fileRequest.success) { - console.log("Not successful file data"); console.log(fileRequest.error); return NextResponse.json( FileResponse.parse({ @@ -41,13 +42,8 @@ export const POST = withSession(async (request) => { { status: 400 } ); } else { - console.log("success"); const fileData = fileRequest.data; - console.log(fileRequest); - console.log(fileRequest.data); - // const fileData = File.parse(JSON.parse()); - console.log("SENIOR"); // get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, @@ -62,43 +58,35 @@ export const POST = withSession(async (request) => { ); } - console.log("PARENTID"); const parentID = foundSenior.folder.split("/").pop(); - - console.log("FILEMETADATA"); - // TODO: date conversion to readable format for title - // const fileMetadata = { - // name: [fileData.date.toString()], - // mimeType: "application/vnd.google-apps.document", - // parents: [parentID], - // }; + const formatted_date = + (fileData.date.getMonth() > 8 + ? fileData.date.getMonth() + 1 + : "0" + (fileData.date.getMonth() + 1)) + + "/" + + (fileData.date.getDate() > 9 + ? fileData.date.getDate() + : "0" + fileData.date.getDate()) + + "/" + + fileData.date.getFullYear(); const fileMetadata = { - name: [fileData.date], + name: [formatted_date], mimeType: "application/vnd.google-apps.document", parents: [parentID], }; - console.log("FILE CREATE DATA"); const fileCreateData = { resource: fileMetadata, fields: "id", }; - console.log("FILE"); - // console.log(service); - // console.log(service == null); - console.log(fileCreateData); - const file = await (service as NonNullable).files.create( fileCreateData ); - console.log(file); - console.log("GOOGLE FIELD ID"); const googleFileId = file.data.id; // used to have (file as any) - do we need this? - console.log("FILE ENTRY"); // If the data is valid, save it to the database via prisma client const fileEntry = await prisma?.file.create({ data: { @@ -110,17 +98,16 @@ export const POST = withSession(async (request) => { }, }); - console.log(fileEntry); - console.log("SENDING SUCCESS"); return NextResponse.json( FileResponse.parse({ code: "SUCCESS", - message: "File successfully created", + message: "File successfully added", }), { status: 200 } ); } - } catch { + } catch (e) { + console.log("Error:", e); return NextResponse.json( FileResponse.parse({ code: "UNKNOWN", diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index f300e7e9..0e8d599c 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -30,7 +30,7 @@ interface IRequest extends Omit { } export interface File { - date: string; + date: Date; filetype: string; url: string; Tags: string[]; @@ -38,7 +38,7 @@ export interface File { } const myFile: File = { - date: "Jan 30 2024", + date: new Date(), filetype: "Google Document", url: "", Tags: ["Adolescence", "Marriage", "Early childhood"], From a19d9b1b1653c9ff5a18faf82df8957542122b9b Mon Sep 17 00:00:00 2001 From: wkim10 Date: Wed, 31 Jan 2024 20:49:46 -0500 Subject: [PATCH 15/77] address Nick PR comments --- prisma/schema.prisma | 2 +- src/app/api/file/route.client.ts | 17 ++--------------- src/app/api/file/route.schema.ts | 4 ++-- src/app/api/file/route.ts | 28 +++++++++++++++++++++++++--- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 79736112..c9af638e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -99,7 +99,7 @@ model Senior { model File { id String @id @default(auto()) @map("_id") @db.ObjectId - date DateTime // will restrict hours to midnight + date DateTime // will zero out the hours filetype String url String seniorId String @db.ObjectId diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index a711e9d2..05a9b64f 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -25,11 +25,6 @@ const MOCK_SUCCESS: IFileResponse = { message: "File successfully added", }; -const MOCK_DUPLICATE_DATE: IFileResponse = { - code: "DUPLICATE_DATE", - message: "A file associated with this date already exists", -}; - const MOCK_UNKNOWN: IFileResponse = { code: "UNKNOWN", message: "Unknown error received", @@ -54,27 +49,19 @@ export const createFile = async ( return FileResponse.parse(MOCK_SUCCESS); } else if (mock === "INVALID_FILE") { return FileResponse.parse(MOCK_INVALID_FILE); - } else if (mock === "DUPLICATE_DATE") { - return FileResponse.parse(MOCK_DUPLICATE_DATE); } else if (mock === "UNKNOWN") { return FileResponse.parse(MOCK_UNKNOWN); } - console.log("About to start post request:"); + const { body, ...options } = request; - console.log(body); - console.log(request); - // console.log(JSON.stringify(body)); + const response = await fetch("/api/file", { method: "POST", body: JSON.stringify(body), ...options, }); - console.log("We've made our response"); - const json = await response.json(); - console.log("Made json: ", json); - console.log("Returning from createFile function..."); return FileResponse.parse(json); }; diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index a4450c15..d16cd673 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -35,8 +35,8 @@ export const FileResponse = z.discriminatedUnion("code", [ message: z.literal("Unknown error received"), }), z.object({ - code: z.literal("DUPLICATE_DATE"), - message: z.literal("A file associated with this date already exists"), + code: z.literal("NOT_AUTHORIZED"), + message: z.literal("Senior not assigned to user"), }), ]); diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index f94e6f92..b59f7542 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -3,6 +3,7 @@ import { File, FileResponse } from "./route.schema"; import { google } from "googleapis"; import { prisma } from "@server/db/client"; import { withSession } from "../../../server/decorator"; +import { env } from "../../../env/server.mjs"; export const POST = withSession(async (request) => { try { @@ -13,8 +14,8 @@ export const POST = withSession(async (request) => { })) ?? { access_token: null }; const auth = new google.auth.OAuth2({ - clientId: process.env.GOOGLE_CLIENT_ID, - clientSecret: process.env.GOOGLE_CLIENT_SECRET, + clientId: env.GOOGLE_CLIENT_ID, + clientSecret: env.GOOGLE_CLIENT_SECRET, }); auth.setCredentials({ @@ -29,6 +30,7 @@ export const POST = withSession(async (request) => { const body = await request.req.json(); body.date = new Date(body.date); + body.date.setHours(0, 0, 0, 0); const fileRequest = File.safeParse(body); @@ -44,6 +46,25 @@ export const POST = withSession(async (request) => { } else { const fileData = fileRequest.data; + /* Check that user has this senior assigned to them */ + const { SeniorIDs } = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if ( + !SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + ) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_AUTHORIZED", + message: "Senior not assigned to user", + }), + { status: 404 } + ); + } + // get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, @@ -81,6 +102,7 @@ export const POST = withSession(async (request) => { fields: "id", }; + /* NOTE: File will still be created on Drive even if it fails on MongoDB */ const file = await (service as NonNullable).files.create( fileCreateData ); @@ -88,7 +110,7 @@ export const POST = withSession(async (request) => { const googleFileId = file.data.id; // used to have (file as any) - do we need this? // If the data is valid, save it to the database via prisma client - const fileEntry = await prisma?.file.create({ + await prisma?.file.create({ data: { date: fileData.date, filetype: fileData.filetype, From c4b726d66692b4560dd7c5c3afde4b37cbcc36b5 Mon Sep 17 00:00:00 2001 From: wkim10 Date: Tue, 6 Feb 2024 11:23:20 -0500 Subject: [PATCH 16/77] finish first draft of PATCH Co-authored-by: JuliaZel --- src/app/api/file/route.client.ts | 42 ++++----- src/app/api/file/route.schema.ts | 9 ++ src/app/api/file/route.ts | 144 +++++++++++++++++++++++++++++++ src/components/Sidebar.tsx | 17 +++- 4 files changed, 184 insertions(+), 28 deletions(-) diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index 05a9b64f..8fa4a1bf 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -20,39 +20,13 @@ interface IRequest extends Omit { body: IFile; } -const MOCK_SUCCESS: IFileResponse = { - code: "SUCCESS", - message: "File successfully added", -}; - -const MOCK_UNKNOWN: IFileResponse = { - code: "UNKNOWN", - message: "Unknown error received", -}; - -const MOCK_INVALID_FILE: IFileResponse = { - code: "INVALID_FILE", - message: "Invalid file added", -}; - /** * 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 createFile = async ( - request: IRequest, - mock?: FileResponseCode -) => { - if (mock === "SUCCESS") { - return FileResponse.parse(MOCK_SUCCESS); - } else if (mock === "INVALID_FILE") { - return FileResponse.parse(MOCK_INVALID_FILE); - } else if (mock === "UNKNOWN") { - return FileResponse.parse(MOCK_UNKNOWN); - } - +export const createFile = async (request: IRequest) => { const { body, ...options } = request; const response = await fetch("/api/file", { @@ -65,3 +39,17 @@ export const createFile = async ( return FileResponse.parse(json); }; + +export const updateFile = async (request: IRequest) => { + const { body, ...options } = request; + + const response = await fetch("/api/file", { + method: "PATCH", + body: JSON.stringify(body), + ...options, + }); + + const json = await response.json(); + + return FileResponse.parse(json); +}; diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index d16cd673..302390ce 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -15,6 +15,11 @@ import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; export const File = z.object({ date: z.date(), + // .transform((val) => { + // const date = new Date(val); + // date.setHours(0, 0, 0, 0); + // return date; + // }), filetype: z.string(), url: z.string(), Tags: z.array(z.string()), @@ -38,6 +43,10 @@ export const FileResponse = z.discriminatedUnion("code", [ code: z.literal("NOT_AUTHORIZED"), message: z.literal("Senior not assigned to user"), }), + z.object({ + code: z.literal("SUCCESS_UPDATE"), + message: z.literal("File successfully updated"), + }), ]); export const ResponsefileDelete = z.discriminatedUnion("code", [ diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index b59f7542..fc89a0e1 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -5,6 +5,7 @@ import { prisma } from "@server/db/client"; import { withSession } from "../../../server/decorator"; import { env } from "../../../env/server.mjs"; +// todo: add functions to reduce repeated code export const POST = withSession(async (request) => { try { const { access_token, refresh_token } = (await prisma.account.findFirst({ @@ -139,3 +140,146 @@ export const POST = withSession(async (request) => { ); } }); + +export const PATCH = withSession(async (request) => { + try { + const { access_token, refresh_token } = (await prisma.account.findFirst({ + where: { + userId: request.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 body = await request.req.json(); + body.date = new Date(body.date); + body.date.setHours(0, 0, 0, 0); + + const fileRequest = File.safeParse(body); + + if (!fileRequest.success) { + console.log(fileRequest.error); + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Unsuccessful request creation", + }), + { status: 400 } + ); + } else { + const fileData = fileRequest.data; + + /* Check that user has this senior assigned to them */ + const { SeniorIDs } = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if ( + !SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + ) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_AUTHORIZED", + message: "Senior not assigned to user", + }), + { status: 404 } + ); + } + + // get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); + if (foundSenior == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + + const formatted_date = + (fileData.date.getMonth() > 8 + ? fileData.date.getMonth() + 1 + : "0" + (fileData.date.getMonth() + 1)) + + "/" + + (fileData.date.getDate() > 9 + ? fileData.date.getDate() + : "0" + fileData.date.getDate()) + + "/" + + fileData.date.getFullYear(); + + const pattern = + /https:\/\/docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/; + const matches = pattern.exec(fileData.url); + + if (matches && matches[1]) { + const googleFileId = matches[1]; + + const body = { name: formatted_date }; + + const fileUpdateData = { + fileId: googleFileId, + resource: body, + }; + + await (service as NonNullable).files.update( + fileUpdateData + ); + + const { id } = await prisma.file.findFirst({ + where: { + url: fileData.url, + }, + }); + + await prisma.file.update({ + where: { id: id }, + data: { date: fileData.date, Tags: fileData.Tags }, + }); + + return NextResponse.json( + FileResponse.parse({ + code: "SUCCESS_UPDATE", + message: "File successfully updated", + }), + { status: 200 } + ); + } else { + return NextResponse.json( + FileResponse.parse({ + code: "UNKNOWN", + message: "Unknown error received", + }), + { status: 500 } + ); + } + } + } catch (e) { + console.log("Error:", e); + return NextResponse.json( + FileResponse.parse({ + code: "UNKNOWN", + message: "Unknown error received", + }), + { status: 500 } + ); + } +}); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0e8d599c..d484b181 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -13,7 +13,7 @@ import Logo from "@public/icons/logo.svg"; import Image from "next/image"; // todo: DELETE this (for testing purposes ) -import { createFile } from "src/app/api/file/route.client"; +import { createFile, updateFile } from "src/app/api/file/route.client"; interface Button { name: string; @@ -45,10 +45,22 @@ const myFile: File = { seniorId: "65ad4d19a029b78419e9265c", }; +const updatedFile: File = { + date: new Date("December 17, 1995 00:00:00"), + filetype: "Google Document", + url: "https://docs.google.com/document/d/1Re2pHQ_5HbyzzwBk5etH-acjZJ7wmFK6ukYlkDm-Nzk", + Tags: ["Getting to know you", "Marriage", "Early childhood"], + seniorId: "65ad4d19a029b78419e9265c", +}; + const myRequest: IRequest = { body: myFile, }; +const myUpdateRequest: IRequest = { + body: updatedFile, +}; + const SidebarItem = ({ label, iconName, @@ -105,6 +117,9 @@ const Sidebar = ({ buttons }: ISideBar) => { +
{user.name ?? ""} From 62209a94f2d3100afc5f50c30b3b708a4402f25e Mon Sep 17 00:00:00 2001 From: wkim10 Date: Tue, 6 Feb 2024 19:00:02 -0500 Subject: [PATCH 17/77] add delete first draft (still fixing some bugs) --- src/app/api/file/route.client.ts | 27 +++--- src/app/api/file/route.schema.ts | 9 +- src/app/api/file/route.ts | 157 +++++++++++++++++++++++++++++++ src/components/Sidebar.tsx | 25 ++++- 4 files changed, 201 insertions(+), 17 deletions(-) diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index 8fa4a1bf..685f26f3 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -6,13 +6,6 @@ import { File, FileResponse } from "./route.schema"; */ type IFile = z.infer; -type IFileResponse = z.infer; - -/** - * Extract all the values of "code". - */ -type FileResponseCode = z.infer["code"]; - /** * Extends the parameters of fetch() function to give types to the RequestBody. */ @@ -20,12 +13,6 @@ interface IRequest extends Omit { body: IFile; } -/** - * 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 createFile = async (request: IRequest) => { const { body, ...options } = request; @@ -53,3 +40,17 @@ export const updateFile = async (request: IRequest) => { return FileResponse.parse(json); }; + +export const deleteFile = async (request: IRequest) => { + const { body, ...options } = request; + + const response = await fetch("/api/file", { + method: "DELETE", + body: JSON.stringify(body), + ...options, + }); + + const json = await response.json(); + + return FileResponse.parse(json); +}; diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index 302390ce..5339b492 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -1,5 +1,4 @@ import { z } from "zod"; -import { Prisma } from "@prisma/client"; import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; /* id String @id @default(auto()) @map("_id") @db.ObjectId @@ -47,6 +46,14 @@ export const FileResponse = z.discriminatedUnion("code", [ code: z.literal("SUCCESS_UPDATE"), message: z.literal("File successfully updated"), }), + z.object({ + code: z.literal("SUCCESS_DELETE"), + message: z.literal("File successfully deleted"), + }), + z.object({ + code: z.literal("INVALID_URL"), + message: z.literal("Invalid file ID parsed from url"), + }), ]); export const ResponsefileDelete = z.discriminatedUnion("code", [ diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index fc89a0e1..8a93009c 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -149,27 +149,39 @@ export const PATCH = withSession(async (request) => { }, })) ?? { access_token: null }; + console.log("AUTH"); + const auth = new google.auth.OAuth2({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }); + console.log("SET CREDENTIALS"); + auth.setCredentials({ access_token, refresh_token, }); + console.log("SERVICE"); + const service = google.drive({ version: "v3", auth, }); + console.log("BODY"); + const body = await request.req.json(); body.date = new Date(body.date); body.date.setHours(0, 0, 0, 0); + console.log("FILE REQUEST"); + const fileRequest = File.safeParse(body); + console.log("SUCCESS"); + if (!fileRequest.success) { console.log(fileRequest.error); return NextResponse.json( @@ -180,8 +192,12 @@ export const PATCH = withSession(async (request) => { { status: 400 } ); } else { + console.log("FILE DATA"); + const fileData = fileRequest.data; + console.log("SENIOR IDS"); + /* Check that user has this senior assigned to them */ const { SeniorIDs } = await prisma.user.findFirst({ where: { @@ -201,6 +217,8 @@ export const PATCH = withSession(async (request) => { ); } + console.log("FOUND SENIOR"); + // get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, @@ -215,6 +233,8 @@ export const PATCH = withSession(async (request) => { ); } + console.log("FORMATTED DATE"); + const formatted_date = (fileData.date.getMonth() > 8 ? fileData.date.getMonth() + 1 @@ -231,25 +251,39 @@ export const PATCH = withSession(async (request) => { const matches = pattern.exec(fileData.url); if (matches && matches[1]) { + console.log("GOOGLE FILE ID"); + const googleFileId = matches[1]; + console.log("BODY"); + const body = { name: formatted_date }; + console.log("FILE UPDATE DATA"); + const fileUpdateData = { fileId: googleFileId, resource: body, }; + console.log("UPDATE IN DRIVE"); + await (service as NonNullable).files.update( fileUpdateData ); + console.log("RIGHT BEFORE GETTING ID"); + + // TODO: FIX ID ISSUE - url is different in mongo vs google drive ? + const { id } = await prisma.file.findFirst({ where: { url: fileData.url, }, }); + console.log("UPDATE IN PRISMA"); + await prisma.file.update({ where: { id: id }, data: { date: fileData.date, Tags: fileData.Tags }, @@ -283,3 +317,126 @@ export const PATCH = withSession(async (request) => { ); } }); + +export const DELETE = withSession(async (request) => { + try { + const { access_token, refresh_token } = (await prisma.account.findFirst({ + where: { + userId: request.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 body = await request.req.json(); + body.date = new Date(body.date); + + const fileRequest = File.safeParse(body); + + if (!fileRequest.success) { + console.log(fileRequest.error); + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Unsuccessful request creation", + }), + { status: 400 } + ); + } else { + const fileData = fileRequest.data; + + /* Check that user has this senior assigned to them */ + const { SeniorIDs } = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if ( + !SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + ) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_AUTHORIZED", + message: "Senior not assigned to user", + }), + { status: 404 } + ); + } + + // get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); + if (foundSenior == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + + const pattern = + /https:\/\/docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/; + const matches = pattern.exec(fileData.url); + + if (matches && matches[1]) { + const googleFileId = matches[1]; + + await (service as NonNullable).files.delete({ + fileId: googleFileId, + }); + + const { id } = await prisma.file.findFirst({ + where: { + url: fileData.url, + }, + }); + + await prisma.file.delete({ + where: { id: id }, + }); + + return NextResponse.json( + FileResponse.parse({ + code: "SUCCESS_DELETE", + message: "File successfully deleted", + }), + { status: 200 } + ); + } else { + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_URL", + message: "Invalid file ID parsed from url", + }), + { status: 500 } + ); + } + } + } catch (e) { + console.log("Error:", e); + return NextResponse.json( + FileResponse.parse({ + code: "UNKNOWN", + message: "Unknown error received", + }), + { status: 500 } + ); + } +}); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d484b181..667c5678 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,8 +12,11 @@ import { UserContext } from "src/context/UserProvider"; import Logo from "@public/icons/logo.svg"; import Image from "next/image"; -// todo: DELETE this (for testing purposes ) -import { createFile, updateFile } from "src/app/api/file/route.client"; +import { + createFile, + deleteFile, + updateFile, +} from "src/app/api/file/route.client"; interface Button { name: string; @@ -37,6 +40,7 @@ export interface File { seniorId: string; } +// Mock data to test functionality (todo: delete) const myFile: File = { date: new Date(), filetype: "Google Document", @@ -48,7 +52,15 @@ const myFile: File = { const updatedFile: File = { date: new Date("December 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1Re2pHQ_5HbyzzwBk5etH-acjZJ7wmFK6ukYlkDm-Nzk", + url: "https://docs.google.com/document/d/1VxNeHLsbuap0ckUvMs5lCMBDVAPF_xRFz4ZEd4mEASY", + Tags: ["Getting to know you", "Marriage", "Early childhood"], + seniorId: "65ad4d19a029b78419e9265c", +}; + +const toDeleteFile: File = { + date: new Date("December 17, 1995 00:00:00"), + filetype: "Google Document", + url: "https://docs.google.com/document/d/1VxNeHLsbuap0ckUvMs5lCMBDVAPF_xRFz4ZEd4mEASY", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; @@ -61,6 +73,10 @@ const myUpdateRequest: IRequest = { body: updatedFile, }; +const myDeleteRequest: IRequest = { + body: toDeleteFile, +}; + const SidebarItem = ({ label, iconName, @@ -120,6 +136,9 @@ const Sidebar = ({ buttons }: ISideBar) => { +
{user.name ?? ""} From cfaf80f2b10df2d1164c209cd429cc4280c3308b Mon Sep 17 00:00:00 2001 From: wkim10 Date: Wed, 7 Feb 2024 19:24:18 -0500 Subject: [PATCH 18/77] fix update bug Co-authored-by: JuliaZel --- src/app/api/file/route.schema.ts | 2 +- src/app/api/file/route.ts | 35 +------------------------------- src/components/Sidebar.tsx | 4 ++-- 3 files changed, 4 insertions(+), 37 deletions(-) diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index 5339b492..3b010474 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -18,7 +18,7 @@ export const File = z.object({ // const date = new Date(val); // date.setHours(0, 0, 0, 0); // return date; - // }), + // }), // note: the transform is not working filetype: z.string(), url: z.string(), Tags: z.array(z.string()), diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 8a93009c..522f072c 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -149,39 +149,27 @@ export const PATCH = withSession(async (request) => { }, })) ?? { access_token: null }; - console.log("AUTH"); - const auth = new google.auth.OAuth2({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET, }); - console.log("SET CREDENTIALS"); - auth.setCredentials({ access_token, refresh_token, }); - console.log("SERVICE"); - const service = google.drive({ version: "v3", auth, }); - console.log("BODY"); - const body = await request.req.json(); body.date = new Date(body.date); body.date.setHours(0, 0, 0, 0); - console.log("FILE REQUEST"); - const fileRequest = File.safeParse(body); - console.log("SUCCESS"); - if (!fileRequest.success) { console.log(fileRequest.error); return NextResponse.json( @@ -192,12 +180,8 @@ export const PATCH = withSession(async (request) => { { status: 400 } ); } else { - console.log("FILE DATA"); - const fileData = fileRequest.data; - console.log("SENIOR IDS"); - /* Check that user has this senior assigned to them */ const { SeniorIDs } = await prisma.user.findFirst({ where: { @@ -217,8 +201,6 @@ export const PATCH = withSession(async (request) => { ); } - console.log("FOUND SENIOR"); - // get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, @@ -233,8 +215,6 @@ export const PATCH = withSession(async (request) => { ); } - console.log("FORMATTED DATE"); - const formatted_date = (fileData.date.getMonth() > 8 ? fileData.date.getMonth() + 1 @@ -246,44 +226,31 @@ export const PATCH = withSession(async (request) => { "/" + fileData.date.getFullYear(); + // used for extracting out the fileId from the url const pattern = /https:\/\/docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/; const matches = pattern.exec(fileData.url); if (matches && matches[1]) { - console.log("GOOGLE FILE ID"); - const googleFileId = matches[1]; - console.log("BODY"); - const body = { name: formatted_date }; - console.log("FILE UPDATE DATA"); - const fileUpdateData = { fileId: googleFileId, resource: body, }; - console.log("UPDATE IN DRIVE"); - await (service as NonNullable).files.update( fileUpdateData ); - console.log("RIGHT BEFORE GETTING ID"); - - // TODO: FIX ID ISSUE - url is different in mongo vs google drive ? - const { id } = await prisma.file.findFirst({ where: { url: fileData.url, }, }); - console.log("UPDATE IN PRISMA"); - await prisma.file.update({ where: { id: id }, data: { date: fileData.date, Tags: fileData.Tags }, diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 667c5678..96dff071 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -52,7 +52,7 @@ const myFile: File = { const updatedFile: File = { date: new Date("December 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1VxNeHLsbuap0ckUvMs5lCMBDVAPF_xRFz4ZEd4mEASY", + url: "https://docs.google.com/document/d/1J4qbpILQjFj92iIntq-4xWRH6XnAZN7LG738F-uI4lM", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; @@ -60,7 +60,7 @@ const updatedFile: File = { const toDeleteFile: File = { date: new Date("December 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1VxNeHLsbuap0ckUvMs5lCMBDVAPF_xRFz4ZEd4mEASY", + url: "https://docs.google.com/document/d/1J4qbpILQjFj92iIntq-4xWRH6XnAZN7LG738F-uI4lM", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; From 609d67b366d26875066f05179ceee1131ee4da72 Mon Sep 17 00:00:00 2001 From: wkim10 Date: Wed, 7 Feb 2024 19:31:03 -0500 Subject: [PATCH 19/77] fix transform string to date --- src/app/api/file/route.schema.ts | 11 +++++------ src/app/api/file/route.ts | 5 ----- src/components/Sidebar.tsx | 4 ++-- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index 3b010474..8e586bce 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -13,12 +13,11 @@ import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; */ export const File = z.object({ - date: z.date(), - // .transform((val) => { - // const date = new Date(val); - // date.setHours(0, 0, 0, 0); - // return date; - // }), // note: the transform is not working + date: z.string().transform((val) => { + const date = new Date(val); + date.setHours(0, 0, 0, 0); + return date; + }), filetype: z.string(), url: z.string(), Tags: z.array(z.string()), diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 522f072c..e75e56e3 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -30,8 +30,6 @@ export const POST = withSession(async (request) => { }); const body = await request.req.json(); - body.date = new Date(body.date); - body.date.setHours(0, 0, 0, 0); const fileRequest = File.safeParse(body); @@ -165,8 +163,6 @@ export const PATCH = withSession(async (request) => { }); const body = await request.req.json(); - body.date = new Date(body.date); - body.date.setHours(0, 0, 0, 0); const fileRequest = File.safeParse(body); @@ -309,7 +305,6 @@ export const DELETE = withSession(async (request) => { }); const body = await request.req.json(); - body.date = new Date(body.date); const fileRequest = File.safeParse(body); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 96dff071..cb90f5fa 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -52,7 +52,7 @@ const myFile: File = { const updatedFile: File = { date: new Date("December 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1J4qbpILQjFj92iIntq-4xWRH6XnAZN7LG738F-uI4lM", + url: "https://docs.google.com/document/d/1ZPcMalMol83VMBVC-0Z-LFoxt2Ij3bohRb9xefr417k", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; @@ -60,7 +60,7 @@ const updatedFile: File = { const toDeleteFile: File = { date: new Date("December 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1J4qbpILQjFj92iIntq-4xWRH6XnAZN7LG738F-uI4lM", + url: "https://docs.google.com/document/d/1ZPcMalMol83VMBVC-0Z-LFoxt2Ij3bohRb9xefr417k", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; From 34d71d973d12faa9fc35eb449cad134edc704282 Mon Sep 17 00:00:00 2001 From: wkim10 Date: Wed, 7 Feb 2024 20:19:28 -0500 Subject: [PATCH 20/77] modularized a little + clean up :) --- src/app/api/file/route.ts | 122 ++++++++++++------------------------- src/components/Sidebar.tsx | 8 +-- 2 files changed, 44 insertions(+), 86 deletions(-) diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index e75e56e3..0078dc92 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -5,33 +5,37 @@ import { prisma } from "@server/db/client"; import { withSession } from "../../../server/decorator"; import { env } from "../../../env/server.mjs"; -// todo: add functions to reduce repeated code +const initializeDriveAuth = async (request) => { + const { access_token, refresh_token } = (await prisma.account.findFirst({ + where: { + userId: request.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 body = await request.req.json(); + const fileRequest = File.safeParse(body); + + return { service, fileRequest }; +}; + export const POST = withSession(async (request) => { try { - const { access_token, refresh_token } = (await prisma.account.findFirst({ - where: { - userId: request.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 body = await request.req.json(); - - const fileRequest = File.safeParse(body); + const { service, fileRequest } = await initializeDriveAuth(request); if (!fileRequest.success) { console.log(fileRequest.error); @@ -45,7 +49,7 @@ export const POST = withSession(async (request) => { } else { const fileData = fileRequest.data; - /* Check that user has this senior assigned to them */ + // Check that user has this senior assigned to them const { SeniorIDs } = await prisma.user.findFirst({ where: { id: request.session.user.id, @@ -64,7 +68,7 @@ export const POST = withSession(async (request) => { ); } - // get senior from database + // Get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, }); @@ -101,12 +105,12 @@ export const POST = withSession(async (request) => { fields: "id", }; - /* NOTE: File will still be created on Drive even if it fails on MongoDB */ + // NOTE: File will still be created on Drive even if it fails on MongoDB const file = await (service as NonNullable).files.create( fileCreateData ); - const googleFileId = file.data.id; // used to have (file as any) - do we need this? + const googleFileId = file.data.id; // If the data is valid, save it to the database via prisma client await prisma?.file.create({ @@ -141,30 +145,7 @@ export const POST = withSession(async (request) => { export const PATCH = withSession(async (request) => { try { - const { access_token, refresh_token } = (await prisma.account.findFirst({ - where: { - userId: request.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 body = await request.req.json(); - - const fileRequest = File.safeParse(body); + const { service, fileRequest } = await initializeDriveAuth(request); if (!fileRequest.success) { console.log(fileRequest.error); @@ -178,7 +159,7 @@ export const PATCH = withSession(async (request) => { } else { const fileData = fileRequest.data; - /* Check that user has this senior assigned to them */ + // Check that user has this senior assigned to them const { SeniorIDs } = await prisma.user.findFirst({ where: { id: request.session.user.id, @@ -197,7 +178,7 @@ export const PATCH = withSession(async (request) => { ); } - // get senior from database + // Get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, }); @@ -222,7 +203,7 @@ export const PATCH = withSession(async (request) => { "/" + fileData.date.getFullYear(); - // used for extracting out the fileId from the url + // Used for extracting out the fileId from the url const pattern = /https:\/\/docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/; const matches = pattern.exec(fileData.url); @@ -283,30 +264,7 @@ export const PATCH = withSession(async (request) => { export const DELETE = withSession(async (request) => { try { - const { access_token, refresh_token } = (await prisma.account.findFirst({ - where: { - userId: request.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 body = await request.req.json(); - - const fileRequest = File.safeParse(body); + const { service, fileRequest } = await initializeDriveAuth(request); if (!fileRequest.success) { console.log(fileRequest.error); @@ -320,7 +278,7 @@ export const DELETE = withSession(async (request) => { } else { const fileData = fileRequest.data; - /* Check that user has this senior assigned to them */ + // Check that user has this senior assigned to them const { SeniorIDs } = await prisma.user.findFirst({ where: { id: request.session.user.id, @@ -339,7 +297,7 @@ export const DELETE = withSession(async (request) => { ); } - // get senior from database + // Get senior from database const foundSenior = await prisma.senior.findUnique({ where: { id: fileData.seniorId }, }); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index cb90f5fa..e7954614 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -50,17 +50,17 @@ const myFile: File = { }; const updatedFile: File = { - date: new Date("December 17, 1995 00:00:00"), + date: new Date("September 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1ZPcMalMol83VMBVC-0Z-LFoxt2Ij3bohRb9xefr417k", + url: "https://docs.google.com/document/d/12zGeaY_e3TIDXTzqmU3ulINWozcdCogR0Fr9Ek2aVFM", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; const toDeleteFile: File = { - date: new Date("December 17, 1995 00:00:00"), + date: new Date("September 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1ZPcMalMol83VMBVC-0Z-LFoxt2Ij3bohRb9xefr417k", + url: "https://docs.google.com/document/d/12zGeaY_e3TIDXTzqmU3ulINWozcdCogR0Fr9Ek2aVFM", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; From 13e37df63f91b2952411338eef176950c438b990 Mon Sep 17 00:00:00 2001 From: wkim10 Date: Sun, 11 Feb 2024 14:57:08 -0500 Subject: [PATCH 21/77] PR fixes :) Co-authored-by: JuliaZel --- src/app/api/file/route.client.ts | 4 +- src/app/api/file/route.schema.ts | 12 +++ src/app/api/file/route.ts | 130 +++++++++++++++++---------- src/components/Sidebar.tsx | 4 +- src/components/TileGrid/FileTile.tsx | 13 +-- src/pages/senior/[id].tsx | 3 +- src/server/service/index.ts | 28 ++++++ 7 files changed, 129 insertions(+), 65 deletions(-) create mode 100644 src/server/service/index.ts diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index 685f26f3..0520951e 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -27,10 +27,10 @@ export const createFile = async (request: IRequest) => { return FileResponse.parse(json); }; -export const updateFile = async (request: IRequest) => { +export const updateFile = async (request: IRequest, id?: string) => { const { body, ...options } = request; - const response = await fetch("/api/file", { + const response = await fetch("/api/file/", { method: "PATCH", body: JSON.stringify(body), ...options, diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index 8e586bce..2058facf 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -53,6 +53,18 @@ export const FileResponse = z.discriminatedUnion("code", [ code: z.literal("INVALID_URL"), message: z.literal("Invalid file ID parsed from url"), }), + z.object({ + code: z.literal("NO_SENIOR"), + message: z.literal("Senior does not exist"), + }), + z.object({ + code: z.literal("NO_FILE"), + message: z.literal("File does not exist"), + }), + z.object({ + code: z.literal("NO_USER"), + message: z.literal("User does not exist"), + }), ]); export const ResponsefileDelete = z.discriminatedUnion("code", [ diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 0078dc92..7af5b8ac 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -1,41 +1,15 @@ import { NextResponse } from "next/server"; import { File, FileResponse } from "./route.schema"; -import { google } from "googleapis"; import { prisma } from "@server/db/client"; -import { withSession } from "../../../server/decorator"; -import { env } from "../../../env/server.mjs"; - -const initializeDriveAuth = async (request) => { - const { access_token, refresh_token } = (await prisma.account.findFirst({ - where: { - userId: request.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 body = await request.req.json(); - const fileRequest = File.safeParse(body); - - return { service, fileRequest }; -}; +import { withSession } from "@server/decorator"; +import { createDriveService } from "@server/service"; export const POST = withSession(async (request) => { try { - const { service, fileRequest } = await initializeDriveAuth(request); + const service = await createDriveService(request.session.user.id); + + const body = await request.req.json(); + const fileRequest = File.safeParse(body); if (!fileRequest.success) { console.log(fileRequest.error); @@ -50,14 +24,25 @@ export const POST = withSession(async (request) => { const fileData = fileRequest.data; // Check that user has this senior assigned to them - const { SeniorIDs } = await prisma.user.findFirst({ + const user = await prisma.user.findFirst({ where: { id: request.session.user.id, }, }); + if (user === null || user.SeniorIDs === null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_USER", + message: "User does not exist", + }) + ); + } + if ( - !SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + !user.SeniorIDs.some( + (seniorId: string) => seniorId === fileData.seniorId + ) ) { return NextResponse.json( FileResponse.parse({ @@ -82,7 +67,8 @@ export const POST = withSession(async (request) => { ); } - const parentID = foundSenior.folder.split("/").pop(); + const parentID = foundSenior.folder; + const formatted_date = (fileData.date.getMonth() > 8 ? fileData.date.getMonth() + 1 @@ -113,7 +99,7 @@ export const POST = withSession(async (request) => { const googleFileId = file.data.id; // If the data is valid, save it to the database via prisma client - await prisma?.file.create({ + await prisma.file.create({ data: { date: fileData.date, filetype: fileData.filetype, @@ -145,7 +131,10 @@ export const POST = withSession(async (request) => { export const PATCH = withSession(async (request) => { try { - const { service, fileRequest } = await initializeDriveAuth(request); + const service = await createDriveService(request.session.user.id); + + const body = await request.req.json(); + const fileRequest = File.safeParse(body); if (!fileRequest.success) { console.log(fileRequest.error); @@ -160,19 +149,30 @@ export const PATCH = withSession(async (request) => { const fileData = fileRequest.data; // Check that user has this senior assigned to them - const { SeniorIDs } = await prisma.user.findFirst({ + const user = await prisma.user.findFirst({ where: { id: request.session.user.id, }, }); + if (user === null || user.SeniorIDs === null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_USER", + message: "User does not exist", + }) + ); + } + if ( - !SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + !user.SeniorIDs.some( + (seniorId: string) => seniorId === fileData.seniorId + ) ) { return NextResponse.json( FileResponse.parse({ - code: "NOT_AUTHORIZED", - message: "Senior not assigned to user", + code: "NO_SENIOR", + message: "Senior does not exist", }), { status: 404 } ); @@ -222,14 +222,24 @@ export const PATCH = withSession(async (request) => { fileUpdateData ); - const { id } = await prisma.file.findFirst({ + const file = await prisma.file.findFirst({ where: { url: fileData.url, }, }); + if (file == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_FILE", + message: "File does not exist", + }), + { status: 404 } + ); + } + await prisma.file.update({ - where: { id: id }, + where: { id: file.id }, data: { date: fileData.date, Tags: fileData.Tags }, }); @@ -264,7 +274,10 @@ export const PATCH = withSession(async (request) => { export const DELETE = withSession(async (request) => { try { - const { service, fileRequest } = await initializeDriveAuth(request); + const service = await createDriveService(request.session.user.id); + + const body = await request.req.json(); + const fileRequest = File.safeParse(body); if (!fileRequest.success) { console.log(fileRequest.error); @@ -279,14 +292,25 @@ export const DELETE = withSession(async (request) => { const fileData = fileRequest.data; // Check that user has this senior assigned to them - const { SeniorIDs } = await prisma.user.findFirst({ + const user = await prisma.user.findFirst({ where: { id: request.session.user.id, }, }); + if (user === null || user.SeniorIDs === null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_USER", + message: "User does not exist", + }) + ); + } + if ( - !SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + !user.SeniorIDs.some( + (seniorId: string) => seniorId === fileData.seniorId + ) ) { return NextResponse.json( FileResponse.parse({ @@ -322,14 +346,24 @@ export const DELETE = withSession(async (request) => { fileId: googleFileId, }); - const { id } = await prisma.file.findFirst({ + const file = await prisma.file.findFirst({ where: { url: fileData.url, }, }); + if (file == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_FILE", + message: "File does not exist", + }), + { status: 404 } + ); + } + await prisma.file.delete({ - where: { id: id }, + where: { id: file.id }, }); return NextResponse.json( diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e7954614..03acc9f4 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -52,7 +52,7 @@ const myFile: File = { const updatedFile: File = { date: new Date("September 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/12zGeaY_e3TIDXTzqmU3ulINWozcdCogR0Fr9Ek2aVFM", + url: "https://docs.google.com/document/d/1yQSBUFmxVlSCOg_NHV20ura7G4OKiOvFsC9b1nmLwfA", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; @@ -60,7 +60,7 @@ const updatedFile: File = { const toDeleteFile: File = { date: new Date("September 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/12zGeaY_e3TIDXTzqmU3ulINWozcdCogR0Fr9Ek2aVFM", + url: "https://docs.google.com/document/d/1yQSBUFmxVlSCOg_NHV20ura7G4OKiOvFsC9b1nmLwfA", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; diff --git a/src/components/TileGrid/FileTile.tsx b/src/components/TileGrid/FileTile.tsx index ea334a1a..3e112783 100644 --- a/src/components/TileGrid/FileTile.tsx +++ b/src/components/TileGrid/FileTile.tsx @@ -7,18 +7,9 @@ import Link from "next/link"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faFile } from "@fortawesome/free-solid-svg-icons"; -export type IFileTileProps = Pick< - File, - "id" | "name" | "lastModified" | "url" | "Tags" ->; +export type IFileTileProps = Pick; -const FileTile = ({ - id, - name, - lastModified: intialLastModified, - url, - Tags, -}: IFileTileProps) => { +const FileTile = ({ id, date, url, Tags }: IFileTileProps) => { return (
diff --git a/src/pages/senior/[id].tsx b/src/pages/senior/[id].tsx index 68100297..7b96fd19 100644 --- a/src/pages/senior/[id].tsx +++ b/src/pages/senior/[id].tsx @@ -100,8 +100,7 @@ const SeniorProfile = ({ senior }: ISeniorProfileProps) => {
diff --git a/src/server/service/index.ts b/src/server/service/index.ts new file mode 100644 index 00000000..df495fdc --- /dev/null +++ b/src/server/service/index.ts @@ -0,0 +1,28 @@ +import { prisma } from "@server/db/client"; +import { google } from "googleapis"; +import { env } from "@env/server.mjs"; + +export const createDriveService = async (userID: string) => { + const { access_token, refresh_token } = (await prisma.account.findFirst({ + where: { + userId: userID, + }, + })) ?? { 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, + }); + + return service; +}; From b6cf52b4b5dd90d51f786f645732fcda8e6e1dea Mon Sep 17 00:00:00 2001 From: wkim10 Date: Thu, 15 Feb 2024 18:22:13 -0500 Subject: [PATCH 22/77] add dynamic route and finished all functionality (hopefully) ;) (yay) Co-authored-by: JuliaZel --- src/app/api/file/[fileId]/route.client.ts | 43 +++ src/app/api/file/[fileId]/route.schema.ts | 70 +++++ src/app/api/file/[fileId]/route.ts | 242 ++++++++++++++++ src/app/api/file/route.client.ts | 28 -- src/app/api/file/route.schema.ts | 12 - src/app/api/file/route.ts | 266 ------------------ src/components/Sidebar.tsx | 15 +- src/pages/api/drive/addfile.ts | 32 ++- src/pages/api/file/[id]/update.tsx | 58 ---- src/pages/api/file/add.tsx | 60 ---- src/pages/senior/[id].tsx | 325 +++++++++++----------- 11 files changed, 544 insertions(+), 607 deletions(-) create mode 100644 src/app/api/file/[fileId]/route.client.ts create mode 100644 src/app/api/file/[fileId]/route.schema.ts create mode 100644 src/app/api/file/[fileId]/route.ts delete mode 100644 src/pages/api/file/[id]/update.tsx delete mode 100644 src/pages/api/file/add.tsx diff --git a/src/app/api/file/[fileId]/route.client.ts b/src/app/api/file/[fileId]/route.client.ts new file mode 100644 index 00000000..134fb090 --- /dev/null +++ b/src/app/api/file/[fileId]/route.client.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; +import { File, FileResponse } from "./route.schema"; + +/** + * Describe the interface of SignInRequest. + */ +type IFile = z.infer; + +/** + * Extends the parameters of fetch() function to give types to the RequestBody. + */ +interface IRequest extends Omit { + fileId: string; + body: IFile; +} + +export const updateFile = async (request: IRequest) => { + const { fileId, body, ...options } = request; + + const response = await fetch(`/api/file/${fileId}`, { + method: "PATCH", + body: JSON.stringify(body), + ...options, + }); + + const json = await response.json(); + + return FileResponse.parse(json); +}; + +export const deleteFile = async (request: IRequest) => { + const { fileId, body, ...options } = request; + + const response = await fetch(`/api/file/${fileId}`, { + method: "DELETE", + body: JSON.stringify(body), + ...options, + }); + + const json = await response.json(); + + return FileResponse.parse(json); +}; diff --git a/src/app/api/file/[fileId]/route.schema.ts b/src/app/api/file/[fileId]/route.schema.ts new file mode 100644 index 00000000..5b06e13b --- /dev/null +++ b/src/app/api/file/[fileId]/route.schema.ts @@ -0,0 +1,70 @@ +import { z } from "zod"; +import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; +/* + id String @id @default(auto()) @map("_id") @db.ObjectId + date DateTime // will restrict hours to midnight + filetype String + url String + seniorId String @db.ObjectId + senior Senior @relation(fields: [seniorId], references: [id], onDelete: Cascade) + Tags String[] + + @@unique([seniorId, date]) +*/ + +export const File = z.object({ + date: z.string().transform((val) => { + const date = new Date(val); + date.setHours(0, 0, 0, 0); + return date; + }), + filetype: z.string(), + url: z.string(), + Tags: z.array(z.string()), + seniorId: z.string(), +}); + +export const FileResponse = z.discriminatedUnion("code", [ + z.object({ + code: z.literal("UNKNOWN"), + message: z.literal("Unknown error received"), + }), + z.object({ + code: z.literal("NOT_AUTHORIZED"), + message: z.literal("Senior not assigned to user"), + }), + z.object({ + code: z.literal("SUCCESS_UPDATE"), + message: z.literal("File successfully updated"), + }), + z.object({ + code: z.literal("SUCCESS_DELETE"), + message: z.literal("File successfully deleted"), + }), + z.object({ + code: z.literal("INVALID_URL"), + message: z.literal("Invalid file ID parsed from url"), + }), + z.object({ + code: z.literal("NO_SENIOR"), + message: z.literal("Senior does not exist"), + }), + z.object({ + code: z.literal("NO_FILE"), + message: z.literal("File does not exist"), + }), + z.object({ + code: z.literal("NO_USER"), + message: z.literal("User does not exist"), + }), +]); + +export const ResponsefileDelete = z.discriminatedUnion("code", [ + z.object({ code: z.literal("SUCCESS") }), + z.object({ + code: z.literal("NOT_FOUND"), + message: z.string(), + }), + unknownErrorSchema, + unauthorizedErrorSchema, +]); diff --git a/src/app/api/file/[fileId]/route.ts b/src/app/api/file/[fileId]/route.ts new file mode 100644 index 00000000..9da5758e --- /dev/null +++ b/src/app/api/file/[fileId]/route.ts @@ -0,0 +1,242 @@ +import { NextResponse } from "next/server"; +import { File, FileResponse } from "./route.schema"; +import { prisma } from "@server/db/client"; +import { withSession } from "@server/decorator"; +import { createDriveService } from "@server/service"; + +export const PATCH = withSession(async (request) => { + try { + const service = await createDriveService(request.session.user.id); + + const body = await request.req.json(); + const nextParams: { fileId: string } = request.params.params; + const { fileId } = nextParams; + const fileRequest = File.safeParse(body); + + if (!fileRequest.success) { + console.log(fileRequest.error); + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Unsuccessful request creation", + }), + { status: 400 } + ); + } else { + const fileData = fileRequest.data; + + // Check that user has this senior assigned to them + const user = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if (user === null || user.SeniorIDs === null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_USER", + message: "User does not exist", + }) + ); + } + + if ( + !user.SeniorIDs.some( + (seniorId: string) => seniorId === fileData.seniorId + ) + ) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_SENIOR", + message: "Senior does not exist", + }), + { status: 404 } + ); + } + + // Get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); + if (foundSenior == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + + const formatted_date = + (fileData.date.getMonth() > 8 + ? fileData.date.getMonth() + 1 + : "0" + (fileData.date.getMonth() + 1)) + + "/" + + (fileData.date.getDate() > 9 + ? fileData.date.getDate() + : "0" + fileData.date.getDate()) + + "/" + + fileData.date.getFullYear(); + + const body = { name: formatted_date }; + + const fileUpdateData = { + fileId: fileId, + resource: body, + }; + + await (service as NonNullable).files.update( + fileUpdateData + ); + + const file = await prisma.file.findFirst({ + where: { + url: fileData.url, + }, + }); + + if (file == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_FILE", + message: "File does not exist", + }), + { status: 404 } + ); + } + + await prisma.file.update({ + where: { id: file.id }, + data: { date: fileData.date, Tags: fileData.Tags }, + }); + + return NextResponse.json( + FileResponse.parse({ + code: "SUCCESS_UPDATE", + message: "File successfully updated", + }), + { status: 200 } + ); + } + } catch (e) { + console.log("Error:", e); + return NextResponse.json( + FileResponse.parse({ + code: "UNKNOWN", + message: "Unknown error received", + }), + { status: 500 } + ); + } +}); + +export const DELETE = withSession(async (request) => { + try { + const service = await createDriveService(request.session.user.id); + + const body = await request.req.json(); + const nextParams: { fileId: string } = request.params.params; + const { fileId } = nextParams; + const fileRequest = File.safeParse(body); + + if (!fileRequest.success) { + console.log(fileRequest.error); + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Unsuccessful request creation", + }), + { status: 400 } + ); + } else { + const fileData = fileRequest.data; + + // Check that user has this senior assigned to them + const user = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if (user === null || user.SeniorIDs === null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_USER", + message: "User does not exist", + }) + ); + } + + if ( + !user.SeniorIDs.some( + (seniorId: string) => seniorId === fileData.seniorId + ) + ) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_AUTHORIZED", + message: "Senior not assigned to user", + }), + { status: 404 } + ); + } + + // Get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); + if (foundSenior == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_FOUND", + message: "Senior not found", + }), + { status: 404 } + ); + } + + await (service as NonNullable).files.delete({ + fileId: fileId, + }); + + const file = await prisma.file.findFirst({ + where: { + url: fileData.url, + }, + }); + + if (file == null) { + return NextResponse.json( + FileResponse.parse({ + code: "NO_FILE", + message: "File does not exist", + }), + { status: 404 } + ); + } + + await prisma.file.delete({ + where: { id: file.id }, + }); + + return NextResponse.json( + FileResponse.parse({ + code: "SUCCESS_DELETE", + message: "File successfully deleted", + }), + { status: 200 } + ); + } + } catch (e) { + console.log("Error:", e); + return NextResponse.json( + FileResponse.parse({ + code: "UNKNOWN", + message: "Unknown error received", + }), + { status: 500 } + ); + } +}); diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index 0520951e..4a6b2158 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -26,31 +26,3 @@ export const createFile = async (request: IRequest) => { return FileResponse.parse(json); }; - -export const updateFile = async (request: IRequest, id?: string) => { - const { body, ...options } = request; - - const response = await fetch("/api/file/", { - method: "PATCH", - body: JSON.stringify(body), - ...options, - }); - - const json = await response.json(); - - return FileResponse.parse(json); -}; - -export const deleteFile = async (request: IRequest) => { - const { body, ...options } = request; - - const response = await fetch("/api/file", { - method: "DELETE", - body: JSON.stringify(body), - ...options, - }); - - const json = await response.json(); - - return FileResponse.parse(json); -}; diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index 2058facf..810b1e83 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -41,18 +41,6 @@ export const FileResponse = z.discriminatedUnion("code", [ code: z.literal("NOT_AUTHORIZED"), message: z.literal("Senior not assigned to user"), }), - z.object({ - code: z.literal("SUCCESS_UPDATE"), - message: z.literal("File successfully updated"), - }), - z.object({ - code: z.literal("SUCCESS_DELETE"), - message: z.literal("File successfully deleted"), - }), - z.object({ - code: z.literal("INVALID_URL"), - message: z.literal("Invalid file ID parsed from url"), - }), z.object({ code: z.literal("NO_SENIOR"), message: z.literal("Senior does not exist"), diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 7af5b8ac..03b13be5 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -128,269 +128,3 @@ export const POST = withSession(async (request) => { ); } }); - -export const PATCH = withSession(async (request) => { - try { - const service = await createDriveService(request.session.user.id); - - const body = await request.req.json(); - const fileRequest = File.safeParse(body); - - if (!fileRequest.success) { - console.log(fileRequest.error); - return NextResponse.json( - FileResponse.parse({ - code: "NOT_FOUND", - message: "Unsuccessful request creation", - }), - { status: 400 } - ); - } else { - const fileData = fileRequest.data; - - // Check that user has this senior assigned to them - const user = await prisma.user.findFirst({ - where: { - id: request.session.user.id, - }, - }); - - if (user === null || user.SeniorIDs === null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_USER", - message: "User does not exist", - }) - ); - } - - if ( - !user.SeniorIDs.some( - (seniorId: string) => seniorId === fileData.seniorId - ) - ) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_SENIOR", - message: "Senior does not exist", - }), - { status: 404 } - ); - } - - // Get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: fileData.seniorId }, - }); - if (foundSenior == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } - - const formatted_date = - (fileData.date.getMonth() > 8 - ? fileData.date.getMonth() + 1 - : "0" + (fileData.date.getMonth() + 1)) + - "/" + - (fileData.date.getDate() > 9 - ? fileData.date.getDate() - : "0" + fileData.date.getDate()) + - "/" + - fileData.date.getFullYear(); - - // Used for extracting out the fileId from the url - const pattern = - /https:\/\/docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/; - const matches = pattern.exec(fileData.url); - - if (matches && matches[1]) { - const googleFileId = matches[1]; - - const body = { name: formatted_date }; - - const fileUpdateData = { - fileId: googleFileId, - resource: body, - }; - - await (service as NonNullable).files.update( - fileUpdateData - ); - - const file = await prisma.file.findFirst({ - where: { - url: fileData.url, - }, - }); - - if (file == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_FILE", - message: "File does not exist", - }), - { status: 404 } - ); - } - - await prisma.file.update({ - where: { id: file.id }, - data: { date: fileData.date, Tags: fileData.Tags }, - }); - - return NextResponse.json( - FileResponse.parse({ - code: "SUCCESS_UPDATE", - message: "File successfully updated", - }), - { status: 200 } - ); - } else { - return NextResponse.json( - FileResponse.parse({ - code: "UNKNOWN", - message: "Unknown error received", - }), - { status: 500 } - ); - } - } - } catch (e) { - console.log("Error:", e); - return NextResponse.json( - FileResponse.parse({ - code: "UNKNOWN", - message: "Unknown error received", - }), - { status: 500 } - ); - } -}); - -export const DELETE = withSession(async (request) => { - try { - const service = await createDriveService(request.session.user.id); - - const body = await request.req.json(); - const fileRequest = File.safeParse(body); - - if (!fileRequest.success) { - console.log(fileRequest.error); - return NextResponse.json( - FileResponse.parse({ - code: "NOT_FOUND", - message: "Unsuccessful request creation", - }), - { status: 400 } - ); - } else { - const fileData = fileRequest.data; - - // Check that user has this senior assigned to them - const user = await prisma.user.findFirst({ - where: { - id: request.session.user.id, - }, - }); - - if (user === null || user.SeniorIDs === null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_USER", - message: "User does not exist", - }) - ); - } - - if ( - !user.SeniorIDs.some( - (seniorId: string) => seniorId === fileData.seniorId - ) - ) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_AUTHORIZED", - message: "Senior not assigned to user", - }), - { status: 404 } - ); - } - - // Get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: fileData.seniorId }, - }); - if (foundSenior == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } - - const pattern = - /https:\/\/docs\.google\.com\/document\/d\/([a-zA-Z0-9_-]+)/; - const matches = pattern.exec(fileData.url); - - if (matches && matches[1]) { - const googleFileId = matches[1]; - - await (service as NonNullable).files.delete({ - fileId: googleFileId, - }); - - const file = await prisma.file.findFirst({ - where: { - url: fileData.url, - }, - }); - - if (file == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_FILE", - message: "File does not exist", - }), - { status: 404 } - ); - } - - await prisma.file.delete({ - where: { id: file.id }, - }); - - return NextResponse.json( - FileResponse.parse({ - code: "SUCCESS_DELETE", - message: "File successfully deleted", - }), - { status: 200 } - ); - } else { - return NextResponse.json( - FileResponse.parse({ - code: "INVALID_URL", - message: "Invalid file ID parsed from url", - }), - { status: 500 } - ); - } - } - } catch (e) { - console.log("Error:", e); - return NextResponse.json( - FileResponse.parse({ - code: "UNKNOWN", - message: "Unknown error received", - }), - { status: 500 } - ); - } -}); diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 03acc9f4..62362c95 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,11 +12,8 @@ import { UserContext } from "src/context/UserProvider"; import Logo from "@public/icons/logo.svg"; import Image from "next/image"; -import { - createFile, - deleteFile, - updateFile, -} from "src/app/api/file/route.client"; +import { createFile } from "@api/file/route.client"; +import { deleteFile, updateFile } from "@api/file/[fileId]/route.client"; interface Button { name: string; @@ -30,6 +27,7 @@ export interface ISideBar { interface IRequest extends Omit { body: File; + fileId: string; } export interface File { @@ -52,7 +50,7 @@ const myFile: File = { const updatedFile: File = { date: new Date("September 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1yQSBUFmxVlSCOg_NHV20ura7G4OKiOvFsC9b1nmLwfA", + url: "https://docs.google.com/document/d/1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; @@ -60,21 +58,24 @@ const updatedFile: File = { const toDeleteFile: File = { date: new Date("September 17, 1995 00:00:00"), filetype: "Google Document", - url: "https://docs.google.com/document/d/1yQSBUFmxVlSCOg_NHV20ura7G4OKiOvFsC9b1nmLwfA", + url: "https://docs.google.com/document/d/1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", Tags: ["Getting to know you", "Marriage", "Early childhood"], seniorId: "65ad4d19a029b78419e9265c", }; const myRequest: IRequest = { body: myFile, + fileId: "", }; const myUpdateRequest: IRequest = { body: updatedFile, + fileId: "1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", }; const myDeleteRequest: IRequest = { body: toDeleteFile, + fileId: "1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", }; const SidebarItem = ({ diff --git a/src/pages/api/drive/addfile.ts b/src/pages/api/drive/addfile.ts index 33333dff..c49c314a 100644 --- a/src/pages/api/drive/addfile.ts +++ b/src/pages/api/drive/addfile.ts @@ -28,31 +28,33 @@ const uploadToFolder = async (req: NextApiRequest, res: NextApiResponse) => { const fileCreateData = { resource: fileMetadata, - fields: "id" - } + fields: "id", + }; // const media = { // mimeType: "text/plain", // body: fileData.description, // }; try { - const file = await (service as NonNullable).files.create(fileCreateData); + const file = await (service as NonNullable).files.create( + fileCreateData + ); // TODO: FIX ANY TYPE const googleFileId = (file as any).data.id; - const fileEntry = await prisma?.file.create({ - data: { - name: fileData.fileName, - description: fileData.description, - filetype: fileData.fileType, - lastModified: new Date(), - url: `https://docs.google.com/document/d/${googleFileId}`, - seniorId: fileData.seniorId, - Tags: fileData.tags, - }, - }); + // const fileEntry = await prisma?.file.create({ + // data: { + // name: fileData.fileName, + // description: fileData.description, + // filetype: fileData.fileType, + // lastModified: new Date(), + // url: `https://docs.google.com/document/d/${googleFileId}`, + // seniorId: fileData.seniorId, + // Tags: fileData.tags, + // }, + // }); - res.status(200).json(fileEntry); + res.status(200).json(""); return; } catch (err) { // TODO(developer) - Handle error diff --git a/src/pages/api/file/[id]/update.tsx b/src/pages/api/file/[id]/update.tsx deleted file mode 100644 index 9f9d9186..00000000 --- a/src/pages/api/file/[id]/update.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { getServerAuthSession } from "@server/common/get-server-auth-session"; -import { prisma } from "@server/db/client"; -import type { NextApiRequest, NextApiResponse } from "next"; - -const files = async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerAuthSession({ req, res }); - - if (!session || !session.user) { - res.status(401).json({ - error: "This route is protected. In order to access it, please sign in.", - }); - return; - } - - const { id: fileId } = req.query; - if (typeof fileId !== "string") { - res.status(500).json({ - error: `fileId must be a string`, - }); - return; - } - - switch (req.method) { - case "POST": - try { - const file = await prisma.file.update({ - where: { - id: fileId, - }, - data: { - lastModified: new Date(), - }, - }); - - if (!file) { - res.status(404).json({ - error: `cannot find file ${fileId}`, - }); - return; - } - - res.status(200).json(file); - } catch (error) { - res.status(500).json({ - error: `failed to fetch student: ${error}`, - }); - } - break; - - default: - res.status(500).json({ - error: `method ${req.method} not implemented`, - }); - break; - } -}; - -export default files; diff --git a/src/pages/api/file/add.tsx b/src/pages/api/file/add.tsx deleted file mode 100644 index 495d3498..00000000 --- a/src/pages/api/file/add.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import type { NextApiRequest, NextApiResponse } from "next"; -import { prisma } from "@server/db/client"; -import { z } from "zod"; -import { getServerAuthSession } from "@server/common/get-server-auth-session"; -import { File } from "@prisma/client"; - -const add = async (req: NextApiRequest, res: NextApiResponse) => { - const session = await getServerAuthSession({ req, res }); - - if (!session || !session.user) { - res.status(401).json({ - error: "This route is protected. In order to access it, please sign in.", - }); - return; - } - - switch (req.method) { - case "POST": - try { - const bodySchema = z.object({ - name: z.string(), - description: z.string(), - fileType: z.string(), - lastModified: z.string().transform(() => new Date()), - url: z.string(), - seniorId: z.string(), - tags: z.array(z.string()), - }); - - const body = bodySchema.parse(JSON.parse(req.body)); - - const file: File = await prisma.file.create({ - data: { - name: body.name, - description: body.description, - filetype: body.fileType, - lastModified: body.lastModified, - url: body.url, - seniorId: body.seniorId, - Tags: body.tags, - }, - }); - - res.status(200).json(file); - } catch (error) { - res.status(500).json({ - error: `failed to create a file: ${error}`, - }); - } - break; - - default: - res.status(500).json({ - error: `method ${req.method} not implemented`, - }); - break; - } -}; - -export default add; diff --git a/src/pages/senior/[id].tsx b/src/pages/senior/[id].tsx index 7b96fd19..ceb13fa6 100644 --- a/src/pages/senior/[id].tsx +++ b/src/pages/senior/[id].tsx @@ -12,20 +12,20 @@ import { z } from "zod"; import { Approval } from "@prisma/client"; import { prisma } from "@server/db/client"; -type ISeniorProfileProps = Awaited< - ReturnType ->["props"] & { - redirect: undefined; -}; - -type SerialzedFile = ISeniorProfileProps["senior"]["Files"][number]; - -const SeniorProfile = ({ senior }: ISeniorProfileProps) => { - const [files, _] = useState(senior.Files); - const [sortMethod, setSortMethod] = useState("By Name"); - const [filter, setFilter] = useState(""); - const [showAddFilePopUp, setShowAddFilePopUp] = useState(false); - +// type ISeniorProfileProps = Awaited< +// ReturnType +// >["props"] & { +// redirect: undefined; +// }; + +// type SerialzedFile = ISeniorProfileProps["senior"]["Files"][number]; + +const SeniorProfile = ({ senior }: any) => { + // const [files, _] = useState(senior.Files); + // const [sortMethod, setSortMethod] = useState("By Name"); + // const [filter, setFilter] = useState(""); + // const [showAddFilePopUp, setShowAddFilePopUp] = useState(false); + /* const sortFunction = sortMethod === "By Name" ? ({ name: nameA }: SerialzedFile, { name: nameB }: SerialzedFile) => @@ -46,154 +46,157 @@ const SeniorProfile = ({ senior }: ISeniorProfileProps) => { const handlePopUp = () => { setShowAddFilePopUp(!showAddFilePopUp); }; - - return ( -
- {showAddFilePopUp ? ( - - ) : null} -
-

- {senior.name} -

{senior.location}

- -
-

- {senior.description} -

-
-
- -
- -
-
- - - - {filteredFiles.map((file, key) => ( -
- -
- ))} -
-
-
- ); + */ + + return null; + + // return ( + //
+ // {showAddFilePopUp ? ( + // + // ) : null} + //
+ //

+ // {senior.name} + //

{senior.location}

+ // + //
+ //

+ // {senior.description} + //

+ //
+ //
+ // + //
+ // + //
+ //
+ + // + // + // {filteredFiles.map((file, key) => ( + //
+ // + //
+ // ))} + //
+ //
+ //
+ // ); }; export default SeniorProfile; -export const getServerSideProps = async ( - context: GetServerSidePropsContext -) => { - const session = await getServerAuthSession(context); - const seniorId = z.string().parse(context.query.id); - - if (!session || !session.user) { - return { - redirect: { - destination: "/login", - permanent: false, - }, - }; - } - - if (!prisma) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - const user = await prisma.user.findUnique({ - where: { - id: session.user.id, - }, - }); - - if (!user) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - if (user.approved === Approval.PENDING) { - return { - redirect: { - destination: "/pending", - permanent: false, - }, - }; - } - - // await fetch ("/api/senior/" + seniorId, { method: "GET" }); - // TODO: not using our beautiful API routes?? - const senior = await prisma.senior.findUnique({ - where: { - id: seniorId, //get all information for given senior - }, - include: { - Files: true, - }, - }); - - if ( - !senior || - (!user.admin && !senior.StudentIDs.includes(session.user.id)) - ) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - return { - props: { - senior: { - ...senior, - Files: senior.Files.map((file) => ({ - ...file, - lastModified: file.lastModified.getTime(), - })), - }, - }, - }; -}; +// export const getServerSideProps = async ( +// context: GetServerSidePropsContext +// ) => { +// const session = await getServerAuthSession(context); +// const seniorId = z.string().parse(context.query.id); + +// if (!session || !session.user) { +// return { +// redirect: { +// destination: "/login", +// permanent: false, +// }, +// }; +// } + +// if (!prisma) { +// return { +// redirect: { +// destination: "/", +// permanent: false, +// }, +// }; +// } + +// const user = await prisma.user.findUnique({ +// where: { +// id: session.user.id, +// }, +// }); + +// if (!user) { +// return { +// redirect: { +// destination: "/", +// permanent: false, +// }, +// }; +// } + +// if (user.approved === Approval.PENDING) { +// return { +// redirect: { +// destination: "/pending", +// permanent: false, +// }, +// }; +// } + +// // await fetch ("/api/senior/" + seniorId, { method: "GET" }); +// // TODO: not using our beautiful API routes?? +// const senior = await prisma.senior.findUnique({ +// where: { +// id: seniorId, //get all information for given senior +// }, +// include: { +// Files: true, +// }, +// }); + +// if ( +// !senior || +// (!user.admin && !senior.StudentIDs.includes(session.user.id)) +// ) { +// return { +// redirect: { +// destination: "/", +// permanent: false, +// }, +// }; +// } + +// return { +// // props: { +// // senior: { +// // ...senior, +// // Files: senior.Files.map((file) => ({ +// // ...file, +// // lastModified: file.lastModified.getTime(), +// // })), +// // }, +// // }, +// }; +// }; From abf140cc858c1afd393947dd219d519edbf63350 Mon Sep 17 00:00:00 2001 From: wkim10 Date: Thu, 15 Feb 2024 18:32:38 -0500 Subject: [PATCH 23/77] remove testing items from sidebar --- src/components/Sidebar.tsx | 57 -------------------------------------- 1 file changed, 57 deletions(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 62362c95..f47a8509 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -12,9 +12,6 @@ import { UserContext } from "src/context/UserProvider"; import Logo from "@public/icons/logo.svg"; import Image from "next/image"; -import { createFile } from "@api/file/route.client"; -import { deleteFile, updateFile } from "@api/file/[fileId]/route.client"; - interface Button { name: string; icon: IconDefinition; @@ -25,11 +22,6 @@ export interface ISideBar { buttons: Button[]; } -interface IRequest extends Omit { - body: File; - fileId: string; -} - export interface File { date: Date; filetype: string; @@ -38,46 +30,6 @@ export interface File { seniorId: string; } -// Mock data to test functionality (todo: delete) -const myFile: File = { - date: new Date(), - filetype: "Google Document", - url: "", - Tags: ["Adolescence", "Marriage", "Early childhood"], - seniorId: "65ad4d19a029b78419e9265c", -}; - -const updatedFile: File = { - date: new Date("September 17, 1995 00:00:00"), - filetype: "Google Document", - url: "https://docs.google.com/document/d/1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", - Tags: ["Getting to know you", "Marriage", "Early childhood"], - seniorId: "65ad4d19a029b78419e9265c", -}; - -const toDeleteFile: File = { - date: new Date("September 17, 1995 00:00:00"), - filetype: "Google Document", - url: "https://docs.google.com/document/d/1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", - Tags: ["Getting to know you", "Marriage", "Early childhood"], - seniorId: "65ad4d19a029b78419e9265c", -}; - -const myRequest: IRequest = { - body: myFile, - fileId: "", -}; - -const myUpdateRequest: IRequest = { - body: updatedFile, - fileId: "1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", -}; - -const myDeleteRequest: IRequest = { - body: toDeleteFile, - fileId: "1it-DKhXUiiVb_m36Nw-v4fl8GUfWuVHGEqRIZuTQHzM", -}; - const SidebarItem = ({ label, iconName, @@ -131,15 +83,6 @@ const Sidebar = ({ buttons }: ISideBar) => {
Tufts University
*/} - - -
{user.name ?? ""} From 2b1a45094c2167b7639af44d46af8fba837a7969 Mon Sep 17 00:00:00 2001 From: wkim10 Date: Fri, 16 Feb 2024 10:27:47 -0500 Subject: [PATCH 24/77] change FileTile to get correct props --- .../[chapterId]/users/[userId]/seniors/[seniorId]/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/private/[uid]/admin/home/chapters/[chapterId]/users/[userId]/seniors/[seniorId]/page.tsx b/src/app/private/[uid]/admin/home/chapters/[chapterId]/users/[userId]/seniors/[seniorId]/page.tsx index 8c740b83..f5289eba 100644 --- a/src/app/private/[uid]/admin/home/chapters/[chapterId]/users/[userId]/seniors/[seniorId]/page.tsx +++ b/src/app/private/[uid]/admin/home/chapters/[chapterId]/users/[userId]/seniors/[seniorId]/page.tsx @@ -58,8 +58,7 @@ const SeniorPage = async ({ params }: Params) => { From dd3a703204db5d7c4cf9301894dc809aba606c43 Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Fri, 16 Feb 2024 12:48:35 -0500 Subject: [PATCH 25/77] Remove test interface --- src/components/Sidebar.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index f47a8509..192b552c 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -22,14 +22,6 @@ export interface ISideBar { buttons: Button[]; } -export interface File { - date: Date; - filetype: string; - url: string; - Tags: string[]; - seniorId: string; -} - const SidebarItem = ({ label, iconName, From ea241f19c8ad7bc74a3fac7efc842036ab262372 Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Fri, 16 Feb 2024 13:34:39 -0500 Subject: [PATCH 26/77] Remove redundant type guard --- src/app/api/file/route.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 03b13be5..6843131b 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -92,9 +92,7 @@ export const POST = withSession(async (request) => { }; // NOTE: File will still be created on Drive even if it fails on MongoDB - const file = await (service as NonNullable).files.create( - fileCreateData - ); + const file = await service.files.create(fileCreateData); const googleFileId = file.data.id; From 92e1145f2c100a80f654739913e63bc05c0c4d06 Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Fri, 16 Feb 2024 13:36:27 -0500 Subject: [PATCH 27/77] Remove comments --- src/app/api/file/[fileId]/route.schema.ts | 11 ----------- src/app/api/file/route.schema.ts | 11 ----------- 2 files changed, 22 deletions(-) diff --git a/src/app/api/file/[fileId]/route.schema.ts b/src/app/api/file/[fileId]/route.schema.ts index 5b06e13b..83c707df 100644 --- a/src/app/api/file/[fileId]/route.schema.ts +++ b/src/app/api/file/[fileId]/route.schema.ts @@ -1,16 +1,5 @@ import { z } from "zod"; import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; -/* - id String @id @default(auto()) @map("_id") @db.ObjectId - date DateTime // will restrict hours to midnight - filetype String - url String - seniorId String @db.ObjectId - senior Senior @relation(fields: [seniorId], references: [id], onDelete: Cascade) - Tags String[] - - @@unique([seniorId, date]) -*/ export const File = z.object({ date: z.string().transform((val) => { diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index 810b1e83..bfc50c04 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -1,16 +1,5 @@ import { z } from "zod"; import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; -/* - id String @id @default(auto()) @map("_id") @db.ObjectId - date DateTime // will restrict hours to midnight - filetype String - url String - seniorId String @db.ObjectId - senior Senior @relation(fields: [seniorId], references: [id], onDelete: Cascade) - Tags String[] - - @@unique([seniorId, date]) -*/ export const File = z.object({ date: z.string().transform((val) => { From d7e193c789bb5d47024d038d36cbb4ff262f295c Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Fri, 16 Feb 2024 13:43:25 -0500 Subject: [PATCH 28/77] Remove redundant type guard --- src/app/api/file/[fileId]/route.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/api/file/[fileId]/route.ts b/src/app/api/file/[fileId]/route.ts index 9da5758e..d417c52f 100644 --- a/src/app/api/file/[fileId]/route.ts +++ b/src/app/api/file/[fileId]/route.ts @@ -87,9 +87,7 @@ export const PATCH = withSession(async (request) => { resource: body, }; - await (service as NonNullable).files.update( - fileUpdateData - ); + await service.files.update(fileUpdateData); const file = await prisma.file.findFirst({ where: { @@ -197,7 +195,7 @@ export const DELETE = withSession(async (request) => { ); } - await (service as NonNullable).files.delete({ + await service.files.delete({ fileId: fileId, }); From a5428cd3a4bd1715f9fe838b9d1fc83a0836733c Mon Sep 17 00:00:00 2001 From: wkim10 Date: Tue, 20 Feb 2024 18:27:30 -0500 Subject: [PATCH 29/77] address PR comments Co-authored-by: JuliaZel --- package.json | 1 + src/app/api/file/[fileId]/route.client.ts | 16 +- src/app/api/file/[fileId]/route.schema.ts | 39 +-- src/app/api/file/[fileId]/route.ts | 371 ++++++++++------------ src/app/api/file/route.client.ts | 3 +- src/app/api/file/route.schema.ts | 43 +-- src/app/api/file/route.ts | 179 +++++------ src/server/model/index.ts | 14 +- src/server/service/index.ts | 14 +- 9 files changed, 292 insertions(+), 388 deletions(-) diff --git a/package.json b/package.json index a63e1494..4fd77d6f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@sendgrid/mail": "^7.7.0", "classnames": "^2.3.2", "googleapis": "^117.0.0", + "moment": "^2.30.1", "mongo": "^0.1.0", "mongodb": "^6.0.0", "next": "^13.5.3", diff --git a/src/app/api/file/[fileId]/route.client.ts b/src/app/api/file/[fileId]/route.client.ts index 134fb090..16461c1e 100644 --- a/src/app/api/file/[fileId]/route.client.ts +++ b/src/app/api/file/[fileId]/route.client.ts @@ -1,5 +1,6 @@ import { z } from "zod"; -import { File, FileResponse } from "./route.schema"; +import { FileResponse } from "./route.schema"; +import { File } from "@server/model"; /** * Describe the interface of SignInRequest. @@ -9,12 +10,16 @@ type IFile = z.infer; /** * Extends the parameters of fetch() function to give types to the RequestBody. */ -interface IRequest extends Omit { +interface IUpdateRequest extends Omit { fileId: string; body: IFile; } -export const updateFile = async (request: IRequest) => { +interface IDeleteRequest extends Omit { + fileId: string; +} + +export const updateFile = async (request: IUpdateRequest) => { const { fileId, body, ...options } = request; const response = await fetch(`/api/file/${fileId}`, { @@ -28,12 +33,11 @@ export const updateFile = async (request: IRequest) => { return FileResponse.parse(json); }; -export const deleteFile = async (request: IRequest) => { - const { fileId, body, ...options } = request; +export const deleteFile = async (request: IDeleteRequest) => { + const { fileId, ...options } = request; const response = await fetch(`/api/file/${fileId}`, { method: "DELETE", - body: JSON.stringify(body), ...options, }); diff --git a/src/app/api/file/[fileId]/route.schema.ts b/src/app/api/file/[fileId]/route.schema.ts index 83c707df..c5cd7943 100644 --- a/src/app/api/file/[fileId]/route.schema.ts +++ b/src/app/api/file/[fileId]/route.schema.ts @@ -1,17 +1,4 @@ import { z } from "zod"; -import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; - -export const File = z.object({ - date: z.string().transform((val) => { - const date = new Date(val); - date.setHours(0, 0, 0, 0); - return date; - }), - filetype: z.string(), - url: z.string(), - Tags: z.array(z.string()), - seniorId: z.string(), -}); export const FileResponse = z.discriminatedUnion("code", [ z.object({ @@ -31,29 +18,7 @@ export const FileResponse = z.discriminatedUnion("code", [ message: z.literal("File successfully deleted"), }), z.object({ - code: z.literal("INVALID_URL"), - message: z.literal("Invalid file ID parsed from url"), - }), - z.object({ - code: z.literal("NO_SENIOR"), - message: z.literal("Senior does not exist"), - }), - z.object({ - code: z.literal("NO_FILE"), - message: z.literal("File does not exist"), - }), - z.object({ - code: z.literal("NO_USER"), - message: z.literal("User does not exist"), - }), -]); - -export const ResponsefileDelete = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS") }), - z.object({ - code: z.literal("NOT_FOUND"), - message: z.string(), + code: z.literal("INVALID_REQUEST"), + message: z.literal("Not a valid request"), }), - unknownErrorSchema, - unauthorizedErrorSchema, ]); diff --git a/src/app/api/file/[fileId]/route.ts b/src/app/api/file/[fileId]/route.ts index d417c52f..37dd5e82 100644 --- a/src/app/api/file/[fileId]/route.ts +++ b/src/app/api/file/[fileId]/route.ts @@ -1,240 +1,209 @@ import { NextResponse } from "next/server"; -import { File, FileResponse } from "./route.schema"; +import { FileResponse } from "./route.schema"; +import { File } from "@server/model"; import { prisma } from "@server/db/client"; import { withSession } from "@server/decorator"; import { createDriveService } from "@server/service"; +import moment from "moment"; export const PATCH = withSession(async (request) => { - try { - const service = await createDriveService(request.session.user.id); + const service = await createDriveService(request.session.user.id); - const body = await request.req.json(); - const nextParams: { fileId: string } = request.params.params; - const { fileId } = nextParams; - const fileRequest = File.safeParse(body); + const body = await request.req.json(); + const nextParams: { fileId: string } = request.params.params; + const { fileId } = nextParams; + const fileRequest = File.safeParse(body); - if (!fileRequest.success) { - console.log(fileRequest.error); + if (!fileRequest.success) { + console.log(fileRequest.error); + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_REQUEST", + message: "Not a valid request", + }), + { status: 400 } + ); + } else { + const fileData = fileRequest.data; + + // Check that user has this senior assigned to them + const user = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if (user === null || user.SeniorIDs === null) { return NextResponse.json( FileResponse.parse({ - code: "NOT_FOUND", - message: "Unsuccessful request creation", + code: "INVALID_REQUEST", + message: "Not a valid request", }), { status: 400 } ); - } else { - const fileData = fileRequest.data; - - // Check that user has this senior assigned to them - const user = await prisma.user.findFirst({ - where: { - id: request.session.user.id, - }, - }); - - if (user === null || user.SeniorIDs === null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_USER", - message: "User does not exist", - }) - ); - } - - if ( - !user.SeniorIDs.some( - (seniorId: string) => seniorId === fileData.seniorId - ) - ) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_SENIOR", - message: "Senior does not exist", - }), - { status: 404 } - ); - } - - // Get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: fileData.seniorId }, - }); - if (foundSenior == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } - - const formatted_date = - (fileData.date.getMonth() > 8 - ? fileData.date.getMonth() + 1 - : "0" + (fileData.date.getMonth() + 1)) + - "/" + - (fileData.date.getDate() > 9 - ? fileData.date.getDate() - : "0" + fileData.date.getDate()) + - "/" + - fileData.date.getFullYear(); - - const body = { name: formatted_date }; - - const fileUpdateData = { - fileId: fileId, - resource: body, - }; - - await service.files.update(fileUpdateData); - - const file = await prisma.file.findFirst({ - where: { - url: fileData.url, - }, - }); - - if (file == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_FILE", - message: "File does not exist", - }), - { status: 404 } - ); - } - - await prisma.file.update({ - where: { id: file.id }, - data: { date: fileData.date, Tags: fileData.Tags }, - }); + } + if ( + !user.SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + ) { return NextResponse.json( FileResponse.parse({ - code: "SUCCESS_UPDATE", - message: "File successfully updated", + code: "INVALID_REQUEST", + message: "Not a valid request", }), - { status: 200 } + { status: 400 } ); } - } catch (e) { - console.log("Error:", e); - return NextResponse.json( - FileResponse.parse({ - code: "UNKNOWN", - message: "Unknown error received", - }), - { status: 500 } - ); - } -}); -export const DELETE = withSession(async (request) => { - try { - const service = await createDriveService(request.session.user.id); + // Get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); - const body = await request.req.json(); - const nextParams: { fileId: string } = request.params.params; - const { fileId } = nextParams; - const fileRequest = File.safeParse(body); + if (foundSenior === null) { + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_REQUEST", + message: "Not a valid request", + }), + { status: 400 } + ); + } + + // query that date doesn't already exist + const foundFile = await prisma.file.findFirst({ + where: { date: fileData.date, seniorId: fileData.seniorId }, + }); - if (!fileRequest.success) { - console.log(fileRequest.error); + if (foundFile !== null) { return NextResponse.json( FileResponse.parse({ - code: "NOT_FOUND", - message: "Unsuccessful request creation", + code: "INVALID_REQUEST", + message: "Not a valid request", }), { status: 400 } ); - } else { - const fileData = fileRequest.data; - - // Check that user has this senior assigned to them - const user = await prisma.user.findFirst({ - where: { - id: request.session.user.id, - }, - }); - - if (user === null || user.SeniorIDs === null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_USER", - message: "User does not exist", - }) - ); - } - - if ( - !user.SeniorIDs.some( - (seniorId: string) => seniorId === fileData.seniorId - ) - ) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_AUTHORIZED", - message: "Senior not assigned to user", - }), - { status: 404 } - ); - } - - // Get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: fileData.seniorId }, - }); - if (foundSenior == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } - - await service.files.delete({ - fileId: fileId, - }); - - const file = await prisma.file.findFirst({ - where: { - url: fileData.url, - }, - }); - - if (file == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_FILE", - message: "File does not exist", - }), - { status: 404 } - ); - } - - await prisma.file.delete({ - where: { id: file.id }, - }); + } + + const formatted_date = moment(fileData.date).format("L"); + const body = { name: formatted_date }; + + const fileUpdateData = { + fileId: fileId, + resource: body, + }; + + await service.files.update(fileUpdateData); + + const file = await prisma.file.findFirst({ + where: { + url: fileData.url, + }, + }); + + if (file === null) { return NextResponse.json( FileResponse.parse({ - code: "SUCCESS_DELETE", - message: "File successfully deleted", + code: "INVALID_REQUEST", + message: "Not a valid request", }), - { status: 200 } + { status: 400 } ); } - } catch (e) { - console.log("Error:", e); + + await prisma.file.update({ + where: { id: file.id }, + data: { date: fileData.date, Tags: fileData.Tags }, + }); + return NextResponse.json( FileResponse.parse({ - code: "UNKNOWN", - message: "Unknown error received", + code: "SUCCESS_UPDATE", + message: "File successfully updated", }), - { status: 500 } + { status: 200 } ); } }); + +export const DELETE = withSession(async (request) => { + const service = await createDriveService(request.session.user.id); + + // const body = await request.req.json(); + const nextParams: { fileId: string } = request.params.params; + const { fileId } = nextParams; + // const fileRequest = File.safeParse(body); + + // Check that user has this senior assigned to them + const user = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if (user === null || user.SeniorIDs === null) { + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_REQUEST", + message: "Not a valid request", + }), + { status: 400 } + ); + } + const file = await prisma.file.findFirst({ + where: { + url: `https://docs.google.com/document/d/${fileId}`, + }, + }); + + if (file === null) { + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_REQUEST", + message: "Not a valid request", + }), + { status: 400 } + ); + } + + if (!user.SeniorIDs.some((seniorId: string) => seniorId === file.seniorId)) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_AUTHORIZED", + message: "Senior not assigned to user", + }), + { status: 404 } + ); + } + + // Get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: file.seniorId }, + }); + if (foundSenior === null) { + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_REQUEST", + message: "Not a valid request", + }), + { status: 400 } + ); + } + + await service.files.delete({ + fileId: fileId, + }); + + await prisma.file.delete({ + where: { id: file.id }, + }); + + return NextResponse.json( + FileResponse.parse({ + code: "SUCCESS_DELETE", + message: "File successfully deleted", + }), + { status: 200 } + ); +}); diff --git a/src/app/api/file/route.client.ts b/src/app/api/file/route.client.ts index 4a6b2158..85c983e3 100644 --- a/src/app/api/file/route.client.ts +++ b/src/app/api/file/route.client.ts @@ -1,5 +1,6 @@ import { z } from "zod"; -import { File, FileResponse } from "./route.schema"; +import { FileResponse } from "./route.schema"; +import { File } from "@server/model"; /** * Describe the interface of SignInRequest. diff --git a/src/app/api/file/route.schema.ts b/src/app/api/file/route.schema.ts index bfc50c04..001837ae 100644 --- a/src/app/api/file/route.schema.ts +++ b/src/app/api/file/route.schema.ts @@ -1,55 +1,16 @@ import { z } from "zod"; -import { unauthorizedErrorSchema, unknownErrorSchema } from "@api/route.schema"; - -export const File = z.object({ - date: z.string().transform((val) => { - const date = new Date(val); - date.setHours(0, 0, 0, 0); - return date; - }), - filetype: z.string(), - url: z.string(), - Tags: z.array(z.string()), - seniorId: z.string(), -}); export const FileResponse = z.discriminatedUnion("code", [ z.object({ code: z.literal("SUCCESS"), message: z.literal("File successfully added"), }), - z.object({ - code: z.literal("INVALID_FILE"), - message: z.literal("Invalid file added"), - }), - z.object({ - code: z.literal("UNKNOWN"), - message: z.literal("Unknown error received"), - }), z.object({ code: z.literal("NOT_AUTHORIZED"), message: z.literal("Senior not assigned to user"), }), z.object({ - code: z.literal("NO_SENIOR"), - message: z.literal("Senior does not exist"), - }), - z.object({ - code: z.literal("NO_FILE"), - message: z.literal("File does not exist"), - }), - z.object({ - code: z.literal("NO_USER"), - message: z.literal("User does not exist"), - }), -]); - -export const ResponsefileDelete = z.discriminatedUnion("code", [ - z.object({ code: z.literal("SUCCESS") }), - z.object({ - code: z.literal("NOT_FOUND"), - message: z.string(), + code: z.literal("INVALID_REQUEST"), + message: z.literal("Not a valid request"), }), - unknownErrorSchema, - unauthorizedErrorSchema, ]); diff --git a/src/app/api/file/route.ts b/src/app/api/file/route.ts index 6843131b..9ef23583 100644 --- a/src/app/api/file/route.ts +++ b/src/app/api/file/route.ts @@ -1,128 +1,109 @@ import { NextResponse } from "next/server"; -import { File, FileResponse } from "./route.schema"; +import { FileResponse } from "./route.schema"; +import { File } from "@server/model"; import { prisma } from "@server/db/client"; import { withSession } from "@server/decorator"; import { createDriveService } from "@server/service"; +import moment from "moment"; export const POST = withSession(async (request) => { - try { - const service = await createDriveService(request.session.user.id); + const service = await createDriveService(request.session.user.id); - const body = await request.req.json(); - const fileRequest = File.safeParse(body); + const body = await request.req.json(); + const fileRequest = File.safeParse(body); - if (!fileRequest.success) { - console.log(fileRequest.error); + if (!fileRequest.success) { + console.log(fileRequest.error); + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_REQUEST", + message: "Not a valid request", + }), + { status: 400 } + ); + } else { + const fileData = fileRequest.data; + + // Check that user has this senior assigned to them + const user = await prisma.user.findFirst({ + where: { + id: request.session.user.id, + }, + }); + + if (user === null || user.SeniorIDs === null) { return NextResponse.json( FileResponse.parse({ - code: "NOT_FOUND", - message: "Unsuccessful request creation", + code: "INVALID_REQUEST", + message: "Not a valid request", }), { status: 400 } ); - } else { - const fileData = fileRequest.data; - - // Check that user has this senior assigned to them - const user = await prisma.user.findFirst({ - where: { - id: request.session.user.id, - }, - }); - - if (user === null || user.SeniorIDs === null) { - return NextResponse.json( - FileResponse.parse({ - code: "NO_USER", - message: "User does not exist", - }) - ); - } + } - if ( - !user.SeniorIDs.some( - (seniorId: string) => seniorId === fileData.seniorId - ) - ) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_AUTHORIZED", - message: "Senior not assigned to user", - }), - { status: 404 } - ); - } + if ( + !user.SeniorIDs.some((seniorId: string) => seniorId === fileData.seniorId) + ) { + return NextResponse.json( + FileResponse.parse({ + code: "NOT_AUTHORIZED", + message: "Senior not assigned to user", + }), + { status: 404 } + ); + } - // Get senior from database - const foundSenior = await prisma.senior.findUnique({ - where: { id: fileData.seniorId }, - }); - if (foundSenior == null) { - return NextResponse.json( - FileResponse.parse({ - code: "NOT_FOUND", - message: "Senior not found", - }), - { status: 404 } - ); - } + // Get senior from database + const foundSenior = await prisma.senior.findUnique({ + where: { id: fileData.seniorId }, + }); + if (foundSenior == null) { + return NextResponse.json( + FileResponse.parse({ + code: "INVALID_REQUEST", + message: "Not a valid request", + }), + { status: 404 } + ); + } - const parentID = foundSenior.folder; + const parentID = foundSenior.folder; - const formatted_date = - (fileData.date.getMonth() > 8 - ? fileData.date.getMonth() + 1 - : "0" + (fileData.date.getMonth() + 1)) + - "/" + - (fileData.date.getDate() > 9 - ? fileData.date.getDate() - : "0" + fileData.date.getDate()) + - "/" + - fileData.date.getFullYear(); + const formatted_date = moment(fileData.date).format("L"); - const fileMetadata = { - name: [formatted_date], - mimeType: "application/vnd.google-apps.document", - parents: [parentID], - }; + const fileMetadata = { + name: [formatted_date], + mimeType: "application/vnd.google-apps.document", + parents: [parentID], + }; - const fileCreateData = { - resource: fileMetadata, - fields: "id", - }; + const fileCreateData = { + resource: fileMetadata, + fields: "id", + }; - // NOTE: File will still be created on Drive even if it fails on MongoDB - const file = await service.files.create(fileCreateData); + // NOTE: File will still be created on Drive even if it fails on MongoDB + const file = await service.files.create(fileCreateData); - const googleFileId = file.data.id; + const googleFileId = file.data.id; - // If the data is valid, save it to the database via prisma client - await prisma.file.create({ - data: { - date: fileData.date, - filetype: fileData.filetype, - url: `https://docs.google.com/document/d/${googleFileId}`, - seniorId: fileData.seniorId, - Tags: fileData.Tags, - }, - }); + // If the data is valid, save it to the database via prisma client + await prisma.file.create({ + data: { + date: fileData.date, + filetype: fileData.filetype, + url: `https://docs.google.com/document/d/${googleFileId}`, + seniorId: fileData.seniorId, + Tags: fileData.Tags, + }, + }); - return NextResponse.json( - FileResponse.parse({ - code: "SUCCESS", - message: "File successfully added", - }), - { status: 200 } - ); - } - } catch (e) { - console.log("Error:", e); return NextResponse.json( FileResponse.parse({ - code: "UNKNOWN", - message: "Unknown error received", + code: "SUCCESS", + message: "File successfully added", }), - { status: 500 } + { status: 200 } ); } }); diff --git a/src/server/model/index.ts b/src/server/model/index.ts index ca04b7b6..a3b46594 100644 --- a/src/server/model/index.ts +++ b/src/server/model/index.ts @@ -9,4 +9,16 @@ export const seniorSchema = z.object({ StudentIDs: z.array(z.string()), folder: z.string(), ChapterID: z.string(), -}) satisfies z.ZodType; \ No newline at end of file +}) satisfies z.ZodType; + +export const File = z.object({ + date: z.string().transform((val) => { + const date = new Date(val); + date.setHours(0, 0, 0, 0); + return date; + }), + filetype: z.string(), + url: z.string(), + Tags: z.array(z.string()), + seniorId: z.string(), +}); diff --git a/src/server/service/index.ts b/src/server/service/index.ts index df495fdc..addbbb0a 100644 --- a/src/server/service/index.ts +++ b/src/server/service/index.ts @@ -3,11 +3,21 @@ import { google } from "googleapis"; import { env } from "@env/server.mjs"; export const createDriveService = async (userID: string) => { - const { access_token, refresh_token } = (await prisma.account.findFirst({ + const account = await prisma.account.findFirst({ where: { userId: userID, }, - })) ?? { access_token: null }; + }); + + if ( + account === null || + account.access_token === null || + account.refresh_token === null + ) { + throw new Error("Invalid google drive authentication"); + } + + const { access_token, refresh_token } = account; const auth = new google.auth.OAuth2({ clientId: env.GOOGLE_CLIENT_ID, From cd7d9969e52123cc87add359cad93407e5022c33 Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Wed, 21 Feb 2024 12:40:43 -0500 Subject: [PATCH 30/77] Add implementation notes --- package-lock.json | 9 +++++++++ src/app/api/file/[fileId]/route.ts | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c268809..ea647c9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@sendgrid/mail": "^7.7.0", "classnames": "^2.3.2", "googleapis": "^117.0.0", + "moment": "^2.30.1", "mongo": "^0.1.0", "mongodb": "^6.0.0", "next": "^13.5.3", @@ -4697,6 +4698,14 @@ "ufo": "^1.3.0" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, "node_modules/mongo": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/mongo/-/mongo-0.1.0.tgz", diff --git a/src/app/api/file/[fileId]/route.ts b/src/app/api/file/[fileId]/route.ts index 37dd5e82..42bac318 100644 --- a/src/app/api/file/[fileId]/route.ts +++ b/src/app/api/file/[fileId]/route.ts @@ -1,3 +1,6 @@ +/** + * @note [fileId] segment represents the id for the Google Doc and not the MongoDB document _id. + */ import { NextResponse } from "next/server"; import { FileResponse } from "./route.schema"; import { File } from "@server/model"; @@ -130,10 +133,8 @@ export const PATCH = withSession(async (request) => { export const DELETE = withSession(async (request) => { const service = await createDriveService(request.session.user.id); - // const body = await request.req.json(); const nextParams: { fileId: string } = request.params.params; const { fileId } = nextParams; - // const fileRequest = File.safeParse(body); // Check that user has this senior assigned to them const user = await prisma.user.findFirst({ From 297ffaec72f6a62974db2d7b458e66274167423a Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Wed, 21 Feb 2024 19:43:53 -0500 Subject: [PATCH 31/77] Fix tests --- src/app/api/resources/route.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/api/resources/route.spec.ts b/src/app/api/resources/route.spec.ts index 9279580f..d383505d 100644 --- a/src/app/api/resources/route.spec.ts +++ b/src/app/api/resources/route.spec.ts @@ -1,10 +1,10 @@ import { describe, expect, test } from "vitest"; -import { resourceSchema } from "./route.schema"; +import { createResourceSchema } from "./route.schema"; describe("TestBatchCreateRequestSchema", () => { test("ValidResource", () => { expect( - resourceSchema.parse({ + createResourceSchema.parse({ link: "https://google.com", title: "Hello world", access: ["USER"], @@ -18,7 +18,7 @@ describe("TestBatchCreateRequestSchema", () => { test("InvalidResourceAccess", () => { expect(() => - resourceSchema.parse({ + createResourceSchema.parse({ link: "https://google.com", title: "Hello world", access: ["USER", "USER"], From 87fbce779a36504851d4d1837b28ae78d09b639f Mon Sep 17 00:00:00 2001 From: nickbar01234 Date: Sun, 18 Feb 2024 23:43:07 -0500 Subject: [PATCH 32/77] Add input feedback --- src/components/DisplayResources.tsx | 80 +++++++++++++------ src/components/ResourceTile.tsx | 81 +++++++++++++++----- src/components/Sidebar.tsx | 5 +- src/components/container/HeaderContainer.tsx | 2 +- src/components/skeleton/Spinner.tsx | 11 +++ src/components/skeleton/index.tsx | 1 + src/context/UserProvider.tsx | 7 +- 7 files changed, 134 insertions(+), 53 deletions(-) create mode 100644 src/components/skeleton/Spinner.tsx create mode 100644 src/components/skeleton/index.tsx diff --git a/src/components/DisplayResources.tsx b/src/components/DisplayResources.tsx index ed0e8966..6379b5ff 100644 --- a/src/components/DisplayResources.tsx +++ b/src/components/DisplayResources.tsx @@ -12,6 +12,8 @@ import { batchDeleteResources, } from "@api/resources/route.client"; import { compareResource } from "@utils"; +import { Spinner } from "./skeleton"; +import { createResourceSchema } from "@api/resources/route.schema"; interface IDisplayResources { resources: Resource[]; @@ -23,6 +25,7 @@ interface ResourceState extends Resource { } const DisplayResources = (props: IDisplayResources) => { + const [loading, setLoading] = React.useState(false); const [edit, setEdit] = React.useState(false); const [stateResources, setStateResources] = React.useState( () => @@ -30,6 +33,7 @@ const DisplayResources = (props: IDisplayResources) => { .sort(compareResource) .map((resource) => ({ state: "UNEDITED", ...resource })) ); + const [canSubmit, setCanSubmit] = React.useState(true); const onAddResource = () => { setStateResources((prev) => [ @@ -66,9 +70,12 @@ const DisplayResources = (props: IDisplayResources) => { }); }; - const getResourceByState = (state: ResourceState["state"]) => { - return stateResources - .filter((curr) => curr.state === state) + const getResourceByStates = ( + resources: ResourceState[], + states: ResourceState["state"][] + ) => { + return resources + .filter((curr) => states.includes(curr.state)) .map((curr) => { const { state, ...rest } = curr; return rest; @@ -76,11 +83,17 @@ const DisplayResources = (props: IDisplayResources) => { }; const onSaveResources = async () => { - const deletedResources = getResourceByState("DELETED").map( - (curr) => curr.id - ); - const updatedResources = getResourceByState("UPDATED"); - const createdResources = getResourceByState("CREATED"); + if (loading) { + return false; + } + + setLoading(true); + + const deletedResources = getResourceByStates(stateResources, [ + "DELETED", + ]).map((curr) => curr.id); + const updatedResources = getResourceByStates(stateResources, ["UPDATED"]); + const createdResources = getResourceByStates(stateResources, ["CREATED"]); await Promise.all([ batchCreateResources({ body: createdResources }), @@ -88,7 +101,7 @@ const DisplayResources = (props: IDisplayResources) => { batchDeleteResources({ body: deletedResources }), ]).then((res) => { const newResources: ResourceState[] = [ - ...getResourceByState("UNEDITED"), + ...getResourceByStates(stateResources, ["UNEDITED"]), ...res[0].data, ...res[1].data, ] @@ -100,9 +113,19 @@ const DisplayResources = (props: IDisplayResources) => { setStateResources(newResources); setEdit(false); + setLoading(false); + setCanSubmit(true); }); }; + React.useEffect(() => { + setCanSubmit( + getResourceByStates(stateResources, ["UPDATED", "CREATED"]) + .map((resource) => createResourceSchema.safeParse(resource)) + .every((state) => state.success) + ); + }, [stateResources, setCanSubmit]); + return (
@@ -115,8 +138,11 @@ const DisplayResources = (props: IDisplayResources) => { {edit ? ( @@ -130,20 +156,26 @@ const DisplayResources = (props: IDisplayResources) => { )}
-
- {stateResources - .filter((resource) => resource.state !== "DELETED") - .map((eachResource) => ( - - ))} -
+ {loading ? ( +
+ +
+ ) : ( +
+ {stateResources + .filter((resource) => resource.state !== "DELETED") + .map((eachResource) => ( + + ))} +
+ )}
); }; diff --git a/src/components/ResourceTile.tsx b/src/components/ResourceTile.tsx index d79b1652..c99f50cc 100644 --- a/src/components/ResourceTile.tsx +++ b/src/components/ResourceTile.tsx @@ -6,7 +6,11 @@ import { faArrowUpRightFromSquare, faTrashCan, } from "@fortawesome/free-solid-svg-icons"; -import Link from "next/link"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { createResourceSchema } from "@api/resources/route.schema"; +import { z } from "zod"; +import React, { useEffect } from "react"; interface IResourceProp { resource: Resource; @@ -23,31 +27,68 @@ const ResourceTile = ({ onDelete, onEdit, }: IResourceProp) => { + const { + register, + trigger, + clearErrors, + formState: { errors }, + } = useForm>({ + resolver: zodResolver(createResourceSchema), + defaultValues: { + title: resource.title, + link: resource.link, + }, + }); + const displayRow = showRole && resource.access.length === 1 && resource.access[0] === "CHAPTER_LEADER"; + useEffect(() => { + trigger(); + }, [trigger]); + return isEdit ? ( -
- { - resource.title = e.target.value; - onEdit(resource); - }} - /> - { - resource.link = e.target.value; - onEdit(resource); - }} - /> +
+
+ trigger("title"), + onChange: () => clearErrors("title"), + })} + className="w-full rounded-xl bg-tan px-4 py-2.5" + defaultValue={resource.title} + placeholder="How to grow my chapter" + onChange={(e) => { + resource.title = e.target.value; + onEdit(resource); + }} + autoComplete="off" + /> +

+ {errors.title && "Title is required"} +

+
+
+ trigger("link"), + onChange: () => clearErrors("link"), + })} + className="w-full rounded-xl bg-tan px-4 py-2.5" + defaultValue={resource.link} + placeholder="https://www.google.com/" + onChange={(e) => { + resource.link = e.target.value; + onEdit(resource); + }} + autoComplete="off" + /> +

+ {errors.link && "Link must be valid"} +

+