1
Fork 0
mirror of https://github.com/Steffo99/festa.git synced 2024-12-22 22:54:22 +00:00

woahhhhhhh

This commit is contained in:
Steffo 2022-05-31 05:03:48 +02:00
parent 14308da110
commit 9f351b1e36
Signed by: steffo
GPG key ID: 6965406171929D01
10 changed files with 288 additions and 161 deletions

View file

@ -1,22 +1,34 @@
import { prisma } from "../../../utils/prismaClient"; import { client } from "../../../utils/prismaClient";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { ApiResult } from "../../../types/api"; import { ApiResult } from "../../../types/api";
import { restInPeace } from "../../../utils/restInPeace"; 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<ApiResult<Event | Event[]>>) { export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResult<Event | Event[]>>) {
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, { restInPeace(req, res, {
model: prisma.event, model: client.event,
isList: false, retrieve: {which},
whereList: {}, create: {create},
whereDetail: { // TODO: this might prove problematic
slug: req.query.slug, })
},
create: {
slug: req.query.slug,
// TODO
},
update: {
// TODO
},
}) })
} }

View file

@ -1,5 +1,5 @@
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { prisma } from "../../../utils/prismaClient" import { client } from "../../../utils/prismaClient"
import { TelegramUserDataClass } from "../../../utils/TelegramUserDataClass" import { TelegramUserDataClass } from "../../../utils/TelegramUserDataClass"
import { default as cryptoRandomString } from "crypto-random-string" import { default as cryptoRandomString } from "crypto-random-string"
import { ApiResult } from "../../../types/api" import { ApiResult } from "../../../types/api"
@ -54,7 +54,7 @@ async function loginTelegram(req: NextApiRequest, res: NextApiResponse<ApiResult
return res.status(401).json({ error: "Telegram login data has been tampered" }) return res.status(401).json({ error: "Telegram login data has been tampered" })
} }
const accountTelegram = await prisma.accountTelegram.upsert({ const accountTelegram = await client.accountTelegram.upsert({
where: { where: {
telegramId: userData.id telegramId: userData.id
}, },
@ -75,7 +75,7 @@ async function loginTelegram(req: NextApiRequest, res: NextApiResponse<ApiResult
} }
}) })
const token = await prisma.token.create({ const token = await client.token.create({
data: { data: {
userId: accountTelegram.userId, userId: accountTelegram.userId,
token: cryptoRandomString({ length: 16, type: "base64" }), token: cryptoRandomString({ length: 16, type: "base64" }),

View file

@ -15,7 +15,7 @@
} }
.postcard { .postcard {
filter: blur(16px) contrast(25%) brightness(175%); filter: blur(7px) contrast(25%) brightness(175%);
} }
/* Dark theme */ /* Dark theme */
@ -36,6 +36,6 @@
} }
.postcard { .postcard {
filter: blur(16px) contrast(50%) brightness(50%); filter: blur(7px) contrast(50%) brightness(50%);
} }
} }

View file

@ -14,6 +14,7 @@
"isolatedModules": true, "isolatedModules": true,
"jsx": "preserve", "jsx": "preserve",
"incremental": true, "incremental": true,
"experimentalDecorators": true,
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View file

@ -1,4 +1,4 @@
import { prisma } from "./prismaClient" import { client } from "./prismaClient"
import { AccountTelegram, Token, User } from "@prisma/client" import { AccountTelegram, Token, User } from "@prisma/client"
import nodecrypto from "crypto" import nodecrypto from "crypto"
import { TelegramLoginData } from "../types/user" import { TelegramLoginData } from "../types/user"

27
utils/apiAuth.ts Normal file
View file

@ -0,0 +1,27 @@
import { User } from "@prisma/client"
import { NextApiRequest, NextApiResponse } from "next"
import { client } from "./prismaClient"
import { Interrupt } from "./interrupt"
export async function authorizeUser(req: NextApiRequest, res: NextApiResponse): Promise<User> {
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
}

30
utils/interrupt.ts Normal file
View file

@ -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<void>) {
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<R extends {}> {
status: number
response: R
constructor(status: number, response: R) {
this.status = status
this.response = response
}
}

View file

@ -1,3 +1,3 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient() export const client = new PrismaClient()

View file

@ -1,252 +1,309 @@
/**
* 3 AM coding
*/
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import { ApiError, ApiResult } from "../types/api"; import { ApiError, ApiResult } from "../types/api";
type RestInOptions<T> = { // I don't know what the typing of a Prisma model is.
type Model = any
type RestInPeaceOptions<T> = {
/** /**
* Prisma delegate to operate on. * 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<T>
/** /**
* Where clause for Prisma queries about multiple objects. * Options for the "options" operation.
*
* Cannot be set together with `whereDetail`.
*/ */
whereList: object, options?: OptionsOptions<T>
/** /**
* Where clause for Prisma queries about a single object. * Options for the "list" operation.
*
* Cannot be set together with `whereList`.
*/ */
whereDetail: object, list?: ListOptions<T>
/** /**
* The same as Prisma's `create`. * Options for the "retrieve" operation.
*/ */
create: any, retrieve?: RetrieveOptions<T>
/** /**
* The same as Prisma's `update`. * Options for the "create" operation.
*/ */
update: any, create?: CreateOptions<T>
/** /**
* Operations not allowed. * Options for the "upsert" operation.
*/ */
disallow?: { upsert?: UpsertOptions<T>
head?: boolean,
options?: boolean,
retrieve?: boolean,
list?: boolean,
create?: boolean,
upsert?: boolean,
update?: boolean,
destroy?: boolean,
}
/** /**
* Hooks ran after a specific operation is completed. * Options for the "update" operation.
*
* If a {@link BreakYourBones} is thrown, it will be caught and returned to the user.
*
* ```
* throw new BreakYourBones(403, {error: "Not allowed"})
* ```
*/ */
hooks?: { update?: UpdateOptions<T>
head?: () => T,
options?: () => T, /**
retrieve?: (obj: T) => T, * Options for the "destroy" operation.
list?: (obj: T[]) => T[], */
create?: (obj: T) => T, destroy?: DestroyOptions<T>
upsert?: (obj: T) => T,
update?: (obj: T) => T,
destroy?: () => T,
}
} }
/** /**
* Handle an API route in a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)ful way. * Handle an API route in a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)ful way.
*/ */
export function restInPeace<T>(req: NextApiRequest, res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { export async function restInPeace<T>(req: NextApiRequest, res: NextApiResponse<ApiResult<T>>, options: RestInPeaceOptions<T>) {
// 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" })
}
// Handle HEAD by returning an empty body // Handle HEAD by returning an empty body
else if (req.method === "HEAD") { if (options.head && req.method === "HEAD") {
return restInHead(res, options) return await handleHead(res, options.model, options.head)
} }
// Same thing for OPTIONS, but beware of weird CORS things! // Same thing for OPTIONS, but beware of weird CORS things!
else if (req.method === "OPTIONS") { else if (options.options && req.method === "OPTIONS") {
return restInOptions(res, options) return await handleOptions(res, options.model, options.options)
} }
// GET can be both "list" and "retrieve" // GET can be both "list" and "retrieve"
else if (req.method === "GET") { else if (options.list && options.list.where && req.method === "GET") {
return options.isList ? restInList(res, options) : restInRetrieve(res, options) 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" // POST is always "create"
else if (req.method === "POST") { else if (options.create && req.method === "POST") {
return options.isList ? noRestForTheWicked(res) : restInCreate(res, options) return await handleCreate(res, options.model, options.create)
} }
// PUT is always "upsert" // PUT is always "upsert"
else if (req.method === "PUT") { else if (options.upsert && options.upsert.which && req.method === "PUT") {
return options.isList ? noRestForTheWicked(res) : restInUpsert(res, options) return await handleUpsert(res, options.model, options.upsert)
} }
// PATCH is always "update" // PATCH is always "update"
else if (req.method === "PATCH") { else if (options.update && options.update.which && req.method === "PATCH") {
return options.isList ? noRestForTheWicked(res) : restInUpdate(res, options) return await handleUpdate(res, options.model, options.update)
} }
// DELETE is always "destroy" // DELETE is always "destroy"
else if (req.method === "DELETE") { else if (options.destroy && options.destroy.which && req.method === "DELETE") {
return options.isList ? noRestForTheWicked(res) : restInDestroy(res, options) return await handleDestroy(res, options.model, options.destroy)
} }
// What kind of weird HTTP methods are you using?! // What kind of weird HTTP methods are you using?!
else { else {
return noRestForTheWicked(res)
}
}
/**
* @returns Method not allowed.
*/
function noRestForTheWicked(res: NextApiResponse) {
return res.status(405).json({ error: "Method not allowed" }) return res.status(405).json({ error: "Method not allowed" })
}
/**
* Error which interrupts the regular flow of a hook to return something different.
*
* Caught by {@link theButcher}.
*/
export class BreakYourBones<AT> {
status: number
response: AT
constructor(status: number, response: AT) {
this.status = status
this.response = response
} }
} }
/**
* Handle a {@link restInPeace} hook, catching possible {@link BreakYourBones}. interface OperationOptions<T> {
*/ before?: (model: T) => Promise<void>,
function theButcher<T>(obj: any, res: NextApiResponse, options: RestInOptions<T>, method: keyof RestInOptions<T>["hooks"]) { after?: (model: T, obj?: any) => Promise<any>,
try { }
var mutated = options?.hooks?.[method]?.(obj) ?? obj
}
catch (e) { // === HEAD ===
if (e instanceof BreakYourBones) {
return res.status(e.status).json(e.response)
} interface HeadOptions<T> extends OperationOptions<T> {
throw e after?: (model: T) => Promise<void>,
}
return mutated
} }
/** /**
* Handle an `HEAD` HTTP request. * Handle an `HEAD` HTTP request.
*/ */
function restInHead<T>(res: NextApiResponse<"">, options: RestInOptions<T>) { async function handleHead<T>(res: NextApiResponse<"">, model: Model, options: HeadOptions<T>) {
if (options.disallow?.head) return noRestForTheWicked(res) await options.before?.(model)
theButcher(undefined, res, options, "head") await options.after?.(model)
return res.status(200).send("") return res.status(200).send("")
} }
// === OPTIONS ===
interface OptionsOptions<T> extends OperationOptions<T> {
after?: (model: T) => Promise<void>,
}
/** /**
* Handle an `OPTIONS` HTTP request. * Handle an `OPTIONS` HTTP request.
*/ */
function restInOptions<T>(res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { async function handleOptions<T>(res: NextApiResponse<"">, model: Model, options: OptionsOptions<T>) {
if (options.disallow?.options) return noRestForTheWicked(res) await options.before?.(model)
theButcher(undefined, res, options, "options") await options.after?.(model)
return res.status(200).send("") return res.status(200).send("")
} }
// === LIST ===
interface ListOptions<T> extends OperationOptions<T> {
/**
* Prisma Where clause used to list objects available in a API route.
*/
where?: object,
after?: (model: T, obj: T[]) => Promise<T[]>,
}
/** /**
* Handle a `GET` HTTP request where a list of items is requested. * Handle a `GET` HTTP request where a list of items is requested.
*/ */
function restInList<T>(res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { async function handleList<T>(res: NextApiResponse<ApiResult<T>>, model: Model, options: ListOptions<T>) {
if (options.disallow?.list) return noRestForTheWicked(res) await options.before?.(model)
const objs = options.model.findMany({ where: options.whereList }) const objs = await model.findMany({ where: options.where })
const mutatedObjs = theButcher(objs, res, options, "list") const mutatedObjs = await options.after?.(model, objs) ?? objs
return res.status(200).json(mutatedObjs) return res.status(200).json(mutatedObjs)
} }
// === RETRIEVE ===
interface RetrieveOptions<T> extends OperationOptions<T> {
/**
* Prisma Where clause used to select the object to display.
*
* See also `findUnique`.
*/
which?: object,
after?: (model: T, obj: T) => Promise<T>,
}
/** /**
* Handle a `GET` HTTP request where a single item is requested. * Handle a `GET` HTTP request where a single item is requested.
*/ */
function restInRetrieve<T>(res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { async function handleRetrieve<T>(res: NextApiResponse<ApiResult<T>>, model: Model, options: RetrieveOptions<T>) {
if (options.disallow?.retrieve) return noRestForTheWicked(res) await options.before?.(model)
const obj = options.model.findUnique({ where: options.whereDetail }) const obj = await model.findUnique({ where: options.which })
const mutatedObj = theButcher(obj, res, options, "retrieve") const mutatedObj = await options.after?.(model, obj) ?? obj
if (!obj) { if (!mutatedObj) {
return res.status(404).json({ error: "Not found" }) return res.status(404).json({ error: "Not found" })
} }
return res.status(200).json(mutatedObj) return res.status(200).json(mutatedObj)
} }
// === CREATE ===
interface CreateOptions<T> extends OperationOptions<T> {
/**
* Prisma Create clause used to create the object.
*/
create: object,
after?: (model: T, obj: T) => Promise<T>,
}
/** /**
* Handle a `POST` HTTP request where a single item is created. * Handle a `POST` HTTP request where a single item is created.
*/ */
function restInCreate<T>(res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { async function handleCreate<T>(res: NextApiResponse<ApiResult<T>>, model: Model, options: CreateOptions<T>) {
if (options.disallow?.create) return noRestForTheWicked(res) await options.before?.(model)
const obj = options.model.create({ data: options.create }) const obj = await model.create({ data: options.create })
const mutatedObj = theButcher(obj, res, options, "create") const mutatedObj = await options.after?.(model, obj) ?? obj
return res.status(200).json(mutatedObj) return res.status(200).json(mutatedObj)
} }
// === UPSERT ===
interface UpsertOptions<T> extends OperationOptions<T> {
/**
* 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<T>,
}
/** /**
* Handle a `PUT` HTTP request where a single item is either created or updated. * Handle a `PUT` HTTP request where a single item is either created or updated.
*/ */
function restInUpsert<T>(res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { async function handleUpsert<T>(res: NextApiResponse<ApiResult<T>>, model: Model, options: UpsertOptions<T>) {
if (options.disallow?.upsert) return noRestForTheWicked(res) await options.before?.(model)
const obj = options.model.upsert({ const obj = await model.upsert({
where: options.whereDetail, where: options.which,
create: options.create, create: options.create,
update: options.update, update: options.update,
}) })
const mutatedObj = theButcher(obj, res, options, "upsert") const mutatedObj = await options.after?.(model, obj) ?? obj
return res.status(200).json(mutatedObj) return res.status(200).json(mutatedObj)
} }
// === UPDATE ===
interface UpdateOptions<T> extends OperationOptions<T> {
/**
* 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<T>,
}
/** /**
* Handle a `PATCH` HTTP request where a single item is updated. * Handle a `PATCH` HTTP request where a single item is updated.
*/ */
function restInUpdate<T>(res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { async function handleUpdate<T>(res: NextApiResponse<ApiResult<T>>, model: Model, options: UpdateOptions<T>) {
if (options.disallow?.update) return noRestForTheWicked(res) await options.before?.(model)
const obj = options.model.update({ const obj = await model.update({
where: options.whereDetail, where: options.which,
data: options.update, data: options.update,
}) })
const mutatedObj = theButcher(obj, res, options, "update") const mutatedObj = await options.after?.(model, obj) ?? obj
return res.status(200).json(mutatedObj) return res.status(200).json(mutatedObj)
} }
// === DESTROY ===
interface DestroyOptions<T> extends OperationOptions<T> {
/**
* Prisma Where clause used to select the object to operate on.
*
* See also `findUnique`.
*/
which?: object,
after?: (model: T) => Promise<void>,
}
/** /**
* Handle a `DELETE` HTTP request where a single item is destroyed. * Handle a `DELETE` HTTP request where a single item is destroyed.
*/ */
function restInDestroy<T>(res: NextApiResponse<ApiResult<T>>, options: RestInOptions<T>) { async function handleDestroy<T>(res: NextApiResponse<ApiResult<T>>, model: Model, options: DestroyOptions<T>) {
if (options.disallow?.destroy) return noRestForTheWicked(res) await options.before?.(model)
options.model.delete({ await model.delete({
where: options.whereDetail, where: options.which,
}) })
theButcher(undefined, res, options, "destroy") await options.after?.(model)
return res.status(204).send("") return res.status(204).send("")
} }