diff --git a/pages/api/events/[slug].ts b/pages/api/events/[slug].ts index 75a9f57..95aa242 100644 --- a/pages/api/events/[slug].ts +++ b/pages/api/events/[slug].ts @@ -1,22 +1,34 @@ -import { prisma } from "../../../utils/prismaClient"; +import { client } from "../../../utils/prismaClient"; import { NextApiRequest, NextApiResponse } from "next"; import { ApiResult } from "../../../types/api"; import { restInPeace } from "../../../utils/restInPeace"; +import { default as cryptoRandomString} from "crypto-random-string"; +import { handleInterrupts } from "../../../utils/interrupt"; +import { authorizeUser } from "../../../utils/apiAuth"; +import { User } from "@prisma/client"; + export default async function handler(req: NextApiRequest, res: NextApiResponse>) { - restInPeace(req, res, { - model: prisma.event, - isList: false, - whereList: {}, - whereDetail: { - slug: req.query.slug, - }, - create: { - slug: req.query.slug, - // TODO - }, - update: { - // TODO - }, + handleInterrupts(res, async () => { + const user = await authorizeUser(req, res) + + const which = { + slug: req.query.slug + } + const create = { + slug: cryptoRandomString({length: 12, type: "url-safe"}), + creatorId: user.id, + name: req.body.name + } + const update = { + name: req.body.name + } + + restInPeace(req, res, { + model: client.event, + retrieve: {which}, + create: {create}, + // TODO: this might prove problematic + }) }) } \ No newline at end of file diff --git a/pages/api/login/index.ts b/pages/api/login/index.ts index d0360a5..4aedcbc 100644 --- a/pages/api/login/index.ts +++ b/pages/api/login/index.ts @@ -1,5 +1,5 @@ import { NextApiRequest, NextApiResponse } from "next" -import { prisma } from "../../../utils/prismaClient" +import { client } from "../../../utils/prismaClient" import { TelegramUserDataClass } from "../../../utils/TelegramUserDataClass" import { default as cryptoRandomString } from "crypto-random-string" import { ApiResult } from "../../../types/api" @@ -54,7 +54,7 @@ async function loginTelegram(req: NextApiRequest, res: NextApiResponse { + const authorization = req.headers.authorization + + if (!authorization) { + throw new Interrupt(401, {error: "Missing Authorization header" }) + } + + const token = authorization.match(/^Bearer (\S+)$/)?.[1] + + if(!(token)) { + throw new Interrupt(401, {error: "Invalid Authorization header" }) + } + + const dbToken = await client.token.findUnique({where: {token}, include: {user: true}}) + + if(!(dbToken)) { + throw new Interrupt(401, {error: "No such Authorization token" }) + } + + return dbToken.user +} \ No newline at end of file diff --git a/utils/interrupt.ts b/utils/interrupt.ts new file mode 100644 index 0000000..9a27d21 --- /dev/null +++ b/utils/interrupt.ts @@ -0,0 +1,30 @@ +import { NextApiResponse } from "next" + +/** + * Pseudo-decorator which intercepts thrown {@link Interrupt}s and turns them into HTTP responses. + */ +export async function handleInterrupts(res: NextApiResponse, f: () => Promise) { + try { + return await f() + } + catch(e) { + if(e instanceof Interrupt) { + return res.status(e.status).json(e.response) + } + } +} + +/** + * Error which interrupts the regular flow of a function to return a specific HTTP response. + * + * Caught by {@link interruptHandler}. + */ +export class Interrupt { + status: number + response: R + + constructor(status: number, response: R) { + this.status = status + this.response = response + } +} \ No newline at end of file diff --git a/utils/prismaClient.ts b/utils/prismaClient.ts index 47a110b..8b8c42f 100644 --- a/utils/prismaClient.ts +++ b/utils/prismaClient.ts @@ -1,3 +1,3 @@ import { PrismaClient } from "@prisma/client"; -export const prisma = new PrismaClient() +export const client = new PrismaClient() diff --git a/utils/restInPeace.ts b/utils/restInPeace.ts index 49aa61c..67c7199 100644 --- a/utils/restInPeace.ts +++ b/utils/restInPeace.ts @@ -1,252 +1,309 @@ -/** - * 3 AM coding - */ - import { NextApiRequest, NextApiResponse } from "next"; import { ApiError, ApiResult } from "../types/api"; -type RestInOptions = { +// I don't know what the typing of a Prisma model is. +type Model = any + + +type RestInPeaceOptions = { /** * Prisma delegate to operate on. */ - model: any, + model: Model, /** - * What kind of request is being performed: `true` for list, `false` for detail. + * Options for the "head" operation. */ - isList: boolean + head?: HeadOptions /** - * Where clause for Prisma queries about multiple objects. - * - * Cannot be set together with `whereDetail`. + * Options for the "options" operation. */ - whereList: object, + options?: OptionsOptions /** - * Where clause for Prisma queries about a single object. - * - * Cannot be set together with `whereList`. + * Options for the "list" operation. */ - whereDetail: object, + list?: ListOptions /** - * The same as Prisma's `create`. + * Options for the "retrieve" operation. */ - create: any, + retrieve?: RetrieveOptions /** - * The same as Prisma's `update`. + * Options for the "create" operation. */ - update: any, + create?: CreateOptions /** - * Operations not allowed. + * Options for the "upsert" operation. */ - disallow?: { - head?: boolean, - options?: boolean, - retrieve?: boolean, - list?: boolean, - create?: boolean, - upsert?: boolean, - update?: boolean, - destroy?: boolean, - } + upsert?: UpsertOptions /** - * Hooks ran after a specific operation is completed. - * - * If a {@link BreakYourBones} is thrown, it will be caught and returned to the user. - * - * ``` - * throw new BreakYourBones(403, {error: "Not allowed"}) - * ``` + * Options for the "update" operation. */ - hooks?: { - head?: () => T, - options?: () => T, - retrieve?: (obj: T) => T, - list?: (obj: T[]) => T[], - create?: (obj: T) => T, - upsert?: (obj: T) => T, - update?: (obj: T) => T, - destroy?: () => T, - } + update?: UpdateOptions + + /** + * Options for the "destroy" operation. + */ + destroy?: DestroyOptions } /** * Handle an API route in a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)ful way. */ -export function restInPeace(req: NextApiRequest, res: NextApiResponse>, options: RestInOptions) { - // Ensure the wheres are set correctly - if (options.whereList && options.whereDetail) { - return res.status(500).json({ error: "Request is being handled as both a list operation and a detail operation" }) - } - else if (!(options.whereList || options.whereDetail)) { - return res.status(500).json({ error: "Request is not being handled as any kind of operation" }) - } - +export async function restInPeace(req: NextApiRequest, res: NextApiResponse>, options: RestInPeaceOptions) { // Handle HEAD by returning an empty body - else if (req.method === "HEAD") { - return restInHead(res, options) + if (options.head && req.method === "HEAD") { + return await handleHead(res, options.model, options.head) } // Same thing for OPTIONS, but beware of weird CORS things! - else if (req.method === "OPTIONS") { - return restInOptions(res, options) + else if (options.options && req.method === "OPTIONS") { + return await handleOptions(res, options.model, options.options) } // GET can be both "list" and "retrieve" - else if (req.method === "GET") { - return options.isList ? restInList(res, options) : restInRetrieve(res, options) + else if (options.list && options.list.where && req.method === "GET") { + return await handleList(res, options.model, options.list) + } + else if(options.retrieve && options.retrieve.which && req.method === "GET") { + return await handleRetrieve(res, options.model, options.retrieve) } // POST is always "create" - else if (req.method === "POST") { - return options.isList ? noRestForTheWicked(res) : restInCreate(res, options) + else if (options.create && req.method === "POST") { + return await handleCreate(res, options.model, options.create) } // PUT is always "upsert" - else if (req.method === "PUT") { - return options.isList ? noRestForTheWicked(res) : restInUpsert(res, options) + else if (options.upsert && options.upsert.which && req.method === "PUT") { + return await handleUpsert(res, options.model, options.upsert) } // PATCH is always "update" - else if (req.method === "PATCH") { - return options.isList ? noRestForTheWicked(res) : restInUpdate(res, options) + else if (options.update && options.update.which && req.method === "PATCH") { + return await handleUpdate(res, options.model, options.update) } // DELETE is always "destroy" - else if (req.method === "DELETE") { - return options.isList ? noRestForTheWicked(res) : restInDestroy(res, options) + else if (options.destroy && options.destroy.which && req.method === "DELETE") { + return await handleDestroy(res, options.model, options.destroy) } // What kind of weird HTTP methods are you using?! else { - return noRestForTheWicked(res) + return res.status(405).json({ error: "Method not allowed" }) } } -/** - * @returns Method not allowed. - */ -function noRestForTheWicked(res: NextApiResponse) { - return res.status(405).json({ error: "Method not allowed" }) + +interface OperationOptions { + before?: (model: T) => Promise, + after?: (model: T, obj?: any) => Promise, } -/** - * Error which interrupts the regular flow of a hook to return something different. - * - * Caught by {@link theButcher}. - */ -export class BreakYourBones { - status: number - response: AT - constructor(status: number, response: AT) { - this.status = status - this.response = response - } -} +// === HEAD === -/** - * Handle a {@link restInPeace} hook, catching possible {@link BreakYourBones}. - */ -function theButcher(obj: any, res: NextApiResponse, options: RestInOptions, method: keyof RestInOptions["hooks"]) { - try { - var mutated = options?.hooks?.[method]?.(obj) ?? obj - } - catch (e) { - if (e instanceof BreakYourBones) { - return res.status(e.status).json(e.response) - } - throw e - } - return mutated + +interface HeadOptions extends OperationOptions { + after?: (model: T) => Promise, } /** * Handle an `HEAD` HTTP request. */ -function restInHead(res: NextApiResponse<"">, options: RestInOptions) { - if (options.disallow?.head) return noRestForTheWicked(res) - theButcher(undefined, res, options, "head") +async function handleHead(res: NextApiResponse<"">, model: Model, options: HeadOptions) { + await options.before?.(model) + await options.after?.(model) return res.status(200).send("") } + +// === OPTIONS === + + +interface OptionsOptions extends OperationOptions { + after?: (model: T) => Promise, +} + /** * Handle an `OPTIONS` HTTP request. */ -function restInOptions(res: NextApiResponse>, options: RestInOptions) { - if (options.disallow?.options) return noRestForTheWicked(res) - theButcher(undefined, res, options, "options") +async function handleOptions(res: NextApiResponse<"">, model: Model, options: OptionsOptions) { + await options.before?.(model) + await options.after?.(model) return res.status(200).send("") } + +// === LIST === + + +interface ListOptions extends OperationOptions { + /** + * Prisma Where clause used to list objects available in a API route. + */ + where?: object, + + after?: (model: T, obj: T[]) => Promise, +} + /** * Handle a `GET` HTTP request where a list of items is requested. */ -function restInList(res: NextApiResponse>, options: RestInOptions) { - if (options.disallow?.list) return noRestForTheWicked(res) - const objs = options.model.findMany({ where: options.whereList }) - const mutatedObjs = theButcher(objs, res, options, "list") +async function handleList(res: NextApiResponse>, model: Model, options: ListOptions) { + await options.before?.(model) + const objs = await model.findMany({ where: options.where }) + const mutatedObjs = await options.after?.(model, objs) ?? objs return res.status(200).json(mutatedObjs) } + +// === RETRIEVE === + + +interface RetrieveOptions extends OperationOptions { + /** + * Prisma Where clause used to select the object to display. + * + * See also `findUnique`. + */ + which?: object, + + after?: (model: T, obj: T) => Promise, +} + /** * Handle a `GET` HTTP request where a single item is requested. */ -function restInRetrieve(res: NextApiResponse>, options: RestInOptions) { - if (options.disallow?.retrieve) return noRestForTheWicked(res) - const obj = options.model.findUnique({ where: options.whereDetail }) - const mutatedObj = theButcher(obj, res, options, "retrieve") - if (!obj) { +async function handleRetrieve(res: NextApiResponse>, model: Model, options: RetrieveOptions) { + await options.before?.(model) + const obj = await model.findUnique({ where: options.which }) + const mutatedObj = await options.after?.(model, obj) ?? obj + if (!mutatedObj) { return res.status(404).json({ error: "Not found" }) } return res.status(200).json(mutatedObj) } + +// === CREATE === + + +interface CreateOptions extends OperationOptions { + /** + * Prisma Create clause used to create the object. + */ + create: object, + + after?: (model: T, obj: T) => Promise, +} + /** * Handle a `POST` HTTP request where a single item is created. */ -function restInCreate(res: NextApiResponse>, options: RestInOptions) { - if (options.disallow?.create) return noRestForTheWicked(res) - const obj = options.model.create({ data: options.create }) - const mutatedObj = theButcher(obj, res, options, "create") +async function handleCreate(res: NextApiResponse>, model: Model, options: CreateOptions) { + await options.before?.(model) + const obj = await model.create({ data: options.create }) + const mutatedObj = await options.after?.(model, obj) ?? obj return res.status(200).json(mutatedObj) } + +// === UPSERT === + + +interface UpsertOptions extends OperationOptions { + /** + * Prisma Where clause used to select the object to operate on. + * + * See also `findUnique`. + */ + which?: object, + + /** + * Prisma Create clause used to create the object if it doesn't exist. + */ + create: object, + + /** + * Prisma Update clause used to update the object if it exists. + */ + update: object, + + after?: (model: T, obj: T) => Promise, +} + /** * Handle a `PUT` HTTP request where a single item is either created or updated. */ -function restInUpsert(res: NextApiResponse>, options: RestInOptions) { - if (options.disallow?.upsert) return noRestForTheWicked(res) - const obj = options.model.upsert({ - where: options.whereDetail, +async function handleUpsert(res: NextApiResponse>, model: Model, options: UpsertOptions) { + await options.before?.(model) + const obj = await model.upsert({ + where: options.which, create: options.create, update: options.update, }) - const mutatedObj = theButcher(obj, res, options, "upsert") + const mutatedObj = await options.after?.(model, obj) ?? obj return res.status(200).json(mutatedObj) } + +// === UPDATE === + + +interface UpdateOptions extends OperationOptions { + /** + * Prisma Where clause used to select the object to operate on. + * + * See also `findUnique`. + */ + which?: object, + + /** + * Prisma Update clause used to update the object if it exists. + */ + update: object, + + after?: (model: T, obj: T) => Promise, +} + /** * Handle a `PATCH` HTTP request where a single item is updated. */ -function restInUpdate(res: NextApiResponse>, options: RestInOptions) { - if (options.disallow?.update) return noRestForTheWicked(res) - const obj = options.model.update({ - where: options.whereDetail, +async function handleUpdate(res: NextApiResponse>, model: Model, options: UpdateOptions) { + await options.before?.(model) + const obj = await model.update({ + where: options.which, data: options.update, }) - const mutatedObj = theButcher(obj, res, options, "update") + const mutatedObj = await options.after?.(model, obj) ?? obj return res.status(200).json(mutatedObj) } + +// === DESTROY === + + +interface DestroyOptions extends OperationOptions { + /** + * Prisma Where clause used to select the object to operate on. + * + * See also `findUnique`. + */ + which?: object, + + after?: (model: T) => Promise, +} + /** * Handle a `DELETE` HTTP request where a single item is destroyed. */ -function restInDestroy(res: NextApiResponse>, options: RestInOptions) { - if (options.disallow?.destroy) return noRestForTheWicked(res) - options.model.delete({ - where: options.whereDetail, +async function handleDestroy(res: NextApiResponse>, model: Model, options: DestroyOptions) { + await options.before?.(model) + await model.delete({ + where: options.which, }) - theButcher(undefined, res, options, "destroy") + await options.after?.(model) return res.status(204).send("") -} +} \ No newline at end of file