From 127163885e3f75015d8c4e92ed80b3ae9f125247 Mon Sep 17 00:00:00 2001 From: Ethan Chew <50659708+Ethan-Chew@users.noreply.github.com> Date: Mon, 25 Sep 2023 01:28:50 +1000 Subject: [PATCH] feat(api): event routes (#1) Co-authored-by: Qin Guan --- nuxt.config.ts | 2 +- server/api/event/[id].delete.ts | 29 ++++++--------- server/api/event/[id].get.ts | 53 +++++++++++++++++++-------- server/api/event/[id].post.ts | 65 ++++++++++++++++++++++++--------- server/api/event/index.get.ts | 56 +++++++++++++++++++--------- server/api/event/index.post.ts | 45 +++++++++++++++++++++++ server/api/user/[id].delete.ts | 2 +- server/db/schema.ts | 2 +- server/utils/handlers.ts | 10 +++++ 9 files changed, 194 insertions(+), 70 deletions(-) create mode 100644 server/api/event/index.post.ts diff --git a/nuxt.config.ts b/nuxt.config.ts index 84d894c..f18eda5 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -109,7 +109,7 @@ export default defineNuxtConfig({ debug: process.env.FIREBASE_APP_CHECK_DEBUG_TOKEN || isDevelopment, provider: 'ReCaptchaEnterprise' || process.env.FIREBASE_APP_CHECK_PROVIDER, key: '6LfNWy8oAAAAAG9GdaqR-X8t8721YyHyILD_C6Pu' || process.env.FIREBASE_APP_CHECK_KEY, - isTokenAutoRefreshEnabled: false, + isTokenAutoRefreshEnabled: true, }, }, diff --git a/server/api/event/[id].delete.ts b/server/api/event/[id].delete.ts index e2c83d2..4fa6a39 100644 --- a/server/api/event/[id].delete.ts +++ b/server/api/event/[id].delete.ts @@ -1,18 +1,13 @@ -import dayjs from 'dayjs' +import { eq } from 'drizzle-orm' +import { events } from '~/server/db/schema' -export default defineProtectedEventHandler(event => ({ - id: event.context.params!.id, - name: 'SST Homecoming 2024', - description: 'SST Homecoming 2024', - location: 'SST', - badgeImage: 'https://www.sst.edu.sg/images/default-source/album/2019-2020/2020-01-24-homecoming/20200124_182000.jpg?sfvrsn=2', - startDateTime: dayjs(Date.now()).valueOf(), - endDateTime: dayjs(Date.now()).valueOf(), - attendees: [ - { - id: '123', - name: 'Qin Guan', - admissionKey: '123', - }, - ], -})) +export default defineProtectedEventHandler(async (event) => { + const eventId = event.context.params!.id + + await event.context.database.delete(events) + .where(eq(events.id, eventId)) + + return sendNoContent(event) +}, { + restrictTo: ['exco'], +}) diff --git a/server/api/event/[id].get.ts b/server/api/event/[id].get.ts index e2c83d2..67a417c 100644 --- a/server/api/event/[id].get.ts +++ b/server/api/event/[id].get.ts @@ -1,18 +1,39 @@ -import dayjs from 'dayjs' +export default defineProtectedEventHandler(async (event) => { + const eventId = event.context.params!.id -export default defineProtectedEventHandler(event => ({ - id: event.context.params!.id, - name: 'SST Homecoming 2024', - description: 'SST Homecoming 2024', - location: 'SST', - badgeImage: 'https://www.sst.edu.sg/images/default-source/album/2019-2020/2020-01-24-homecoming/20200124_182000.jpg?sfvrsn=2', - startDateTime: dayjs(Date.now()).valueOf(), - endDateTime: dayjs(Date.now()).valueOf(), - attendees: [ - { - id: '123', - name: 'Qin Guan', - admissionKey: '123', + const result = await event.context.database.query.events.findFirst({ + where: (events, { eq }) => eq(events.id, eventId), + with: { + usersToEvents: { + with: { + user: { + columns: { + id: true, + name: true, + }, + }, + }, + columns: { + admissionKey: true, + }, + }, }, - ], -})) + }) + + if (!result) { + throw createError({ + status: 400, + statusMessage: 'Bad request', + }) + } + + const { usersToEvents, ...data } = result + + return { + ...data, + attendees: usersToEvents.map(({ admissionKey, user }) => ({ + ...user, + admissionKey, + })), + } +}) diff --git a/server/api/event/[id].post.ts b/server/api/event/[id].post.ts index e2c83d2..ad0b377 100644 --- a/server/api/event/[id].post.ts +++ b/server/api/event/[id].post.ts @@ -1,18 +1,49 @@ -import dayjs from 'dayjs' +import { eq } from 'drizzle-orm' +import { z } from 'zod' +import { events } from '~/server/db/schema' -export default defineProtectedEventHandler(event => ({ - id: event.context.params!.id, - name: 'SST Homecoming 2024', - description: 'SST Homecoming 2024', - location: 'SST', - badgeImage: 'https://www.sst.edu.sg/images/default-source/album/2019-2020/2020-01-24-homecoming/20200124_182000.jpg?sfvrsn=2', - startDateTime: dayjs(Date.now()).valueOf(), - endDateTime: dayjs(Date.now()).valueOf(), - attendees: [ - { - id: '123', - name: 'Qin Guan', - admissionKey: '123', - }, - ], -})) +const updateEventRequestBody = z.object({ + name: z.string(), + description: z.string(), + location: z.string(), + badgeImage: z.string().url(), + startDateTime: z.string().datetime(), + endDateTime: z.string().datetime(), +}) + +export default defineProtectedEventHandler(async (event) => { + const eventId = event.context.params!.id + + const result = await updateEventRequestBody.safeParseAsync(await readBody(event)) + if (!result.success) { + throw createError({ + status: 400, + statusMessage: 'Bad request', + }) + } + + const { data } = result + + const updatedEvent = await event.context.database.update(events) + .set({ + name: data.name, + description: data.description, + location: data.location, + badgeImage: data.badgeImage, + startDateTime: data.startDateTime, + endDateTime: data.endDateTime, + }) + .where(eq(events.id, eventId)) + .returning() + + if (updatedEvent.length > 1) { + throw createError({ + status: 500, + statusMessage: 'Internal server error', + }) + } + + return updatedEvent[0] +}, { + restrictTo: ['exco'], +}) diff --git a/server/api/event/index.get.ts b/server/api/event/index.get.ts index dd95486..b002fe1 100644 --- a/server/api/event/index.get.ts +++ b/server/api/event/index.get.ts @@ -1,18 +1,40 @@ -import dayjs from 'dayjs' - -export default defineProtectedEventHandler(event => [{ - id: event.context.params!.id, - name: 'SST Homecoming 2024', - description: 'SST Homecoming 2024', - location: 'SST', - badgeImage: 'https://www.sst.edu.sg/images/default-source/album/2019-2020/2020-01-24-homecoming/20200124_182000.jpg?sfvrsn=2', - startDateTime: dayjs(Date.now()).valueOf(), - endDateTime: dayjs(Date.now()).valueOf(), - attendees: [ - { - id: '123', - name: 'Qin Guan', - admissionKey: '123', +export default defineProtectedEventHandler(async (event) => { + const result = await event.context.database.query.events.findMany({ + with: { + usersToEvents: { + with: { + user: { + columns: { + id: true, + name: true, + }, + }, + }, + columns: { + admissionKey: true, + }, + }, }, - ], -}]) + }) + + if (!result) { + throw createError({ + status: 400, + statusMessage: 'Bad request', + }) + } + + return result.map((item) => { + const { usersToEvents, ...data } = item + + return { + ...data, + attendees: usersToEvents.map(({ admissionKey, user }) => ({ + ...user, + admissionKey, + })), + } + }) +}, { + restrictTo: ['exco'], +}) diff --git a/server/api/event/index.post.ts b/server/api/event/index.post.ts new file mode 100644 index 0000000..af96549 --- /dev/null +++ b/server/api/event/index.post.ts @@ -0,0 +1,45 @@ +import { z } from 'zod' +import { events } from '~/server/db/schema' + +const createEventRequestBody = z.object({ + name: z.string(), + description: z.string(), + location: z.string(), + badgeImage: z.string().url(), + startDateTime: z.string().datetime(), + endDateTime: z.string().datetime(), +}) + +export default defineProtectedEventHandler(async (event) => { + const result = await createEventRequestBody.safeParseAsync(await readBody(event)) + if (!result.success) { + throw createError({ + status: 400, + statusMessage: 'Bad request', + }) + } + + const { data } = result + + const createdEvent = await event.context.database.insert(events) + .values({ + name: data.name, + description: data.description, + location: data.location, + badgeImage: data.badgeImage, + startDateTime: data.startDateTime, + endDateTime: data.endDateTime, + }) + .returning() + + if (createdEvent.length > 1) { + throw createError({ + status: 500, + statusMessage: 'Internal server error', + }) + } + + return createdEvent[0] +}, { + restrictTo: ['exco'], +}) diff --git a/server/api/user/[id].delete.ts b/server/api/user/[id].delete.ts index 1a0d309..165cc15 100644 --- a/server/api/user/[id].delete.ts +++ b/server/api/user/[id].delete.ts @@ -17,5 +17,5 @@ export default defineProtectedEventHandler(async (event) => { eq(users.id, event.context.params!.id), ) - return { ok: true } + return sendNoContent(event) }) diff --git a/server/db/schema.ts b/server/db/schema.ts index 3e6a7d1..1c11429 100644 --- a/server/db/schema.ts +++ b/server/db/schema.ts @@ -23,7 +23,7 @@ export const users = sqliteTable('users', { }) export const events = sqliteTable('events', { - id: text('id').primaryKey(), + id: text('id').primaryKey().$defaultFn(() => createId()), name: text('name').notNull(), description: text('description').notNull(), location: text('location').notNull(), diff --git a/server/utils/handlers.ts b/server/utils/handlers.ts index 5cfab4c..04d9742 100644 --- a/server/utils/handlers.ts +++ b/server/utils/handlers.ts @@ -16,6 +16,7 @@ declare module 'h3' { export interface DefineProtectedEventHandlerOptions { cache?: Pick allowUnlinkedUser?: boolean // Allow users which do not have a `firebaseId` linked in database + restrictTo?: Array } const defaultOptions: DefineProtectedEventHandlerOptions = { @@ -61,6 +62,15 @@ export function defineProtectedEventHandler( }) } + if (options.restrictTo) { + if (!user?.memberType || !options.restrictTo.includes(user.memberType)) { + throw createError({ + status: 403, + statusMessage: 'Forbidden', + }) + } + } + event.context.user = user event.context.firebaseId = sub