diff --git a/src/lib/middleware/withZipline.ts b/src/lib/middleware/withZipline.ts index c484a0b..277121c 100644 --- a/src/lib/middleware/withZipline.ts +++ b/src/lib/middleware/withZipline.ts @@ -6,6 +6,7 @@ import { sign64, unsign64 } from 'lib/utils/crypto'; import config from 'lib/config'; import prisma from 'lib/prisma'; import { OAuth, User } from '@prisma/client'; +import { HTTPMethod } from 'find-my-way'; export interface NextApiFile { fieldname: string; @@ -16,7 +17,7 @@ export interface NextApiFile { size: number; } -interface UserExtended extends User { +export interface UserExtended extends User { oauth: OAuth[]; } @@ -27,55 +28,98 @@ export type NextApiReq = NextApiRequest & { files?: NextApiFile[]; }; -export type NextApiRes = NextApiResponse & { - error: (message: string) => void; - forbid: (message: string, extra?: any) => void; - bad: (message: string) => void; - json: (json: Record, status?: number) => void; - ratelimited: (remaining: number) => void; - setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void; - setUserCookie: (id: number) => void; +export type NextApiResExtra = + | 'badRequest' + | 'unauthorized' + | 'forbidden' + | 'ratelimited' + | 'notFound' + | 'error'; +export type NextApiResExtraObj = { + [key in NextApiResExtra]: (message: any, extra?: Record) => void; +}; + +export type NextApiRes = NextApiResponse & + NextApiResExtraObj & { + json: (json: Record, status?: number) => void; + setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void; + setUserCookie: (id: number) => void; + }; + +export type ZiplineApiConfig = { + methods: HTTPMethod[]; + user?: boolean; + administrator?: boolean; + middleware?: any[]; }; export const withZipline = - (handler: (req: NextApiRequest, res: NextApiResponse) => unknown) => (req: NextApiReq, res: NextApiRes) => { + ( + handler: (req: NextApiRequest, res: NextApiResponse, user?: UserExtended) => unknown, + api_config: ZiplineApiConfig = { methods: ['GET'] } + ) => + (req: NextApiReq, res: NextApiRes) => { res.setHeader('Content-Type', 'application/json'); res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Content-Allow-Methods', 'GET,HEAD,POST,OPTIONS'); res.setHeader('Access-Control-Max-Age', '86400'); - res.error = (message: string) => { + // Used when the client sends wrong information, etc. + res.badRequest = (message: string, extra: Record = {}) => { res.json( { error: message, + code: 400, + ...extra, }, - 500 + 400 ); }; - res.forbid = (message: string, extra: any = {}) => { + // If the user is not logged in + res.unauthorized = (message: string, extra: Record = {}) => { res.json( { - error: '403: ' + message, + error: message, + code: 401, + ...extra, + }, + 401 + ); + }; + + // If the user is logged in but doesn't have permission to do something + res.forbidden = (message: string, extra: Record = {}) => { + res.json( + { + error: message, + code: 403, ...extra, }, 403 ); }; - res.bad = (message: string) => { + res.notFound = (message: string, extra: Record = {}) => { res.json( { - error: '401: ' + message, + error: message, + code: 404, + ...extra, }, - 401 + 404 ); }; - res.ratelimited = (remaining: number) => { - res.setHeader('X-Ratelimit-Remaining', Math.floor(remaining / 1000)).json( + res.ratelimited = (message: number, extra: Record = {}) => { + const retry = Math.floor(message / 1000); + + res.setHeader('X-Ratelimit-Remaining', retry); + res.json( { - error: '429: ratelimited', + error: `ratelimited - try again in ${retry} seconds`, + code: 429, + ...extra, }, 429 ); @@ -129,8 +173,16 @@ export const withZipline = } }; - res.setCookie = (name: string, value: unknown, options?: CookieSerializeOptions) => - setCookie(res, name, value, options || {}); + res.setCookie = (name: string, value: unknown, options: CookieSerializeOptions = {}) => { + if ('maxAge' in options) { + options.expires = new Date(Date.now() + options.maxAge * 1000); + options.maxAge /= 1000; + } + + const signed = sign64(String(value), config.core.secret); + + res.setHeader('Set-Cookie', serialize(name, signed, options)); + }; res.setUserCookie = (id: number) => { req.cleanCookie('user'); @@ -141,21 +193,32 @@ export const withZipline = }); }; + if (!api_config.methods.includes(req.method as HTTPMethod)) { + return res.json( + { + error: 'method not allowed', + code: 405, + }, + 405 + ); + } + + if (api_config.middleware) { + for (let i = 0; i !== api_config.middleware.length; ++i) { + api_config.middleware[i](req, res, (result) => { + if (result instanceof Error) return res.error(result.message); + }); + } + } + + if (api_config.user) { + return req.user().then((user) => { + if (!user) return res.unauthorized('not logged in'); + if (api_config.administrator && !user.administrator) return res.forbidden('not an administrator'); + + return handler(req, res, user); + }); + } + return handler(req, res); }; - -export const setCookie = ( - res: NextApiResponse, - name: string, - value: unknown, - options: CookieSerializeOptions = {} -) => { - if ('maxAge' in options) { - options.expires = new Date(Date.now() + options.maxAge * 1000); - options.maxAge /= 1000; - } - - const signed = sign64(String(value), config.core.secret); - - res.setHeader('Set-Cookie', serialize(name, signed, options)); -}; diff --git a/src/pages/api/auth/create.ts b/src/pages/api/auth/create.ts index 1bd80dc..8ef7952 100644 --- a/src/pages/api/auth/create.ts +++ b/src/pages/api/auth/create.ts @@ -5,10 +5,11 @@ import Logger from 'lib/logger'; import config from 'lib/config'; async function handler(req: NextApiReq, res: NextApiRes) { + // handle invites if (req.method === 'POST' && req.body) { - if (!config.features.invites && req.body.code) return res.forbid('invites are disabled'); + if (!config.features.invites && req.body.code) return res.badRequest('invites are disabled'); if (!config.features.user_registration && !req.body.code) - return res.forbid('user registration is disabled'); + return res.badRequest('user registration is disabled'); const { code, username, password } = req.body as { code?: string; @@ -18,13 +19,13 @@ async function handler(req: NextApiReq, res: NextApiRes) { const invite = await prisma.invite.findUnique({ where: { code: code ?? '' }, }); - if (!invite && code) return res.bad('invalid invite code'); + if (!invite && code) return res.badRequest('invalid invite code'); const user = await prisma.user.findFirst({ where: { username }, }); - if (user) return res.bad('username already exists'); + if (user) return res.badRequest('username already exists'); const hashed = await hashPassword(password); const newUser = await prisma.user.create({ data: { @@ -56,8 +57,8 @@ async function handler(req: NextApiReq, res: NextApiRes) { } const user = await req.user(); - if (!user) return res.forbid('not logged in'); - if (!user.administrator) return res.forbid('you arent an administrator'); + if (!user) return res.unauthorized('not logged in'); + if (!user.administrator) return res.forbidden('you arent an administrator'); if (req.method !== 'POST') return res.status(405).end(); @@ -67,15 +68,15 @@ async function handler(req: NextApiReq, res: NextApiRes) { administrator: boolean; }; - if (!username) return res.bad('no username'); - if (!password) return res.bad('no auth'); + if (!username) return res.badRequest('no username'); + if (!password) return res.badRequest('no password'); const existing = await prisma.user.findFirst({ where: { username, }, }); - if (existing) return res.forbid('user exists'); + if (existing) return res.badRequest('user exists'); const hashed = await hashPassword(password); @@ -95,4 +96,6 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json(newUser); } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['POST'], +}); diff --git a/src/pages/api/auth/image.ts b/src/pages/api/auth/image.ts index 24b79e0..6fcc971 100644 --- a/src/pages/api/auth/image.ts +++ b/src/pages/api/auth/image.ts @@ -15,13 +15,13 @@ async function handler(req: NextApiReq, res: NextApiRes) { }); if (!image) return res.status(404).end(JSON.stringify({ error: 'Image not found' })); - if (!password) return res.forbid('No password provided'); + if (!password) return res.badRequest('No password provided'); const valid = await checkPassword(password as string, image.password); - if (!valid) return res.forbid('Wrong password'); + if (!valid) return res.badRequest('Wrong password'); const data = await datasource.get(image.file); - if (!data) return res.error('Image not found'); + if (!data) return res.notFound('Image not found'); const size = await datasource.size(image.file); diff --git a/src/pages/api/auth/invite.ts b/src/pages/api/auth/invite.ts index c328c41..a4acf57 100644 --- a/src/pages/api/auth/invite.ts +++ b/src/pages/api/auth/invite.ts @@ -1,15 +1,11 @@ import prisma from 'lib/prisma'; -import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline'; import { randomChars } from 'lib/util'; import Logger from 'lib/logger'; import config from 'lib/config'; -async function handler(req: NextApiReq, res: NextApiRes) { - if (!config.features.invites) return res.forbid('invites are disabled'); - - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - if (!user.administrator) return res.forbid('you arent an administrator'); +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { + if (!config.features.invites) return res.badRequest('invites are disabled'); if (req.method === 'POST') { const { expires_at, count } = req.body as { @@ -19,8 +15,8 @@ async function handler(req: NextApiReq, res: NextApiRes) { const expiry = expires_at ? new Date(expires_at) : null; if (expiry) { - if (!expiry.getTime()) return res.bad('invalid date'); - if (expiry.getTime() < Date.now()) return res.bad('date is in the past'); + if (!expiry.getTime()) return res.badRequest('invalid date'); + if (expiry.getTime() < Date.now()) return res.badRequest('date is in the past'); } const counts = count ? count : 1; @@ -58,14 +54,6 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json(invite); } - } else if (req.method === 'GET') { - const invites = await prisma.invite.findMany({ - orderBy: { - created_at: 'desc', - }, - }); - - return res.json(invites); } else if (req.method === 'DELETE') { const { code } = req.query as { code: string }; @@ -78,7 +66,19 @@ async function handler(req: NextApiReq, res: NextApiRes) { Logger.get('invite').info(`${user.username} (${user.id}) deleted invite ${invite.code}`); return res.json(invite); + } else { + const invites = await prisma.invite.findMany({ + orderBy: { + created_at: 'desc', + }, + }); + + return res.json(invites); } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'POST', 'DELETE'], + user: true, + administrator: true, +}); diff --git a/src/pages/api/auth/login.ts b/src/pages/api/auth/login.ts index 11a46fd..77a23f4 100644 --- a/src/pages/api/auth/login.ts +++ b/src/pages/api/auth/login.ts @@ -4,7 +4,6 @@ import { checkPassword, createToken, hashPassword } from 'lib/util'; import Logger from 'lib/logger'; async function handler(req: NextApiReq, res: NextApiRes) { - if (req.method !== 'POST') return res.status(405).end(); const { username, password } = req.body as { username: string; password: string; @@ -30,10 +29,10 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - if (!user) return res.status(404).end(JSON.stringify({ error: 'User not found' })); + if (!user) return res.notFound('user not found'); const valid = await checkPassword(password, user.password); - if (!valid) return res.forbid('Wrong password'); + if (!valid) return res.unauthorized('Wrong password'); res.setUserCookie(user.id); Logger.get('user').info(`User ${user.username} (${user.id}) logged in`); @@ -41,4 +40,6 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json({ success: true }); } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['POST'], +}); diff --git a/src/pages/api/auth/logout.ts b/src/pages/api/auth/logout.ts index 4d6dd5b..70187df 100644 --- a/src/pages/api/auth/logout.ts +++ b/src/pages/api/auth/logout.ts @@ -1,10 +1,7 @@ -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import Logger from 'lib/logger'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { req.cleanCookie('user'); Logger.get('user').info(`User ${user.username} (${user.id}) logged out`); @@ -12,4 +9,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json({ success: true }); } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET'], + user: true, +}); diff --git a/src/pages/api/auth/oauth/index.ts b/src/pages/api/auth/oauth/index.ts index 43bfb20..1c21bb7 100644 --- a/src/pages/api/auth/oauth/index.ts +++ b/src/pages/api/auth/oauth/index.ts @@ -1,14 +1,11 @@ import prisma from 'lib/prisma'; -import { NextApiReq, NextApiRes, withZipline } from 'lib/middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'lib/middleware/withZipline'; import { OauthProviders } from '@prisma/client'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.error('not logged in'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (req.method === 'DELETE') { - if (!user.password && user.oauth.length === 1) - return res.forbid("can't unlink account without a password, please set one then unlink."); + if (!user.password && user.oauth?.length === 1) + return res.badRequest("can't unlink account without a password, please set one then unlink."); const { provider } = req.body as { provider: OauthProviders }; @@ -44,8 +41,11 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json(nuser); } else { - return res.json(user.oauth); + return res.json(user.oauth ?? []); } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['DELETE', 'GET'], + user: true, +}); diff --git a/src/pages/api/shorten.ts b/src/pages/api/shorten.ts index d3861bf..1b8b022 100644 --- a/src/pages/api/shorten.ts +++ b/src/pages/api/shorten.ts @@ -7,8 +7,7 @@ import config from 'lib/config'; import { sendShorten } from 'lib/discord'; async function handler(req: NextApiReq, res: NextApiRes) { - if (req.method !== 'POST') return res.forbid('no allow'); - if (!req.headers.authorization) return res.forbid('no authorization'); + if (!req.headers.authorization) return res.badRequest('no authorization'); const user = await prisma.user.findFirst({ where: { @@ -16,13 +15,13 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - if (!user) return res.forbid('authorization incorect'); - if (!req.body) return res.error('no body'); - if (!req.body.url) return res.error('no url'); + if (!user) return res.unauthorized('authorization incorect'); + if (!req.body) return res.badRequest('no body'); + if (!req.body.url) return res.badRequest('no url'); const maxUrlViews = req.headers['max-views'] ? Number(req.headers['max-views']) : null; - if (isNaN(maxUrlViews)) return res.error('invalid max views (invalid number)'); - if (maxUrlViews < 0) return res.error('invalid max views (max views < 0)'); + if (isNaN(maxUrlViews)) return res.badRequest('invalid max views (invalid number)'); + if (maxUrlViews < 0) return res.badRequest('invalid max views (max views < 0)'); const rand = randomChars(zconfig.urls.length); @@ -35,7 +34,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - if (existing) return res.error('vanity already exists'); + if (existing) return res.badRequest('vanity already exists'); } const url = await prisma.url.create({ @@ -71,4 +70,6 @@ async function handler(req: NextApiReq, res: NextApiRes) { }); } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['POST'], +}); diff --git a/src/pages/api/stats.ts b/src/pages/api/stats.ts index baa6e52..4e17dbf 100644 --- a/src/pages/api/stats.ts +++ b/src/pages/api/stats.ts @@ -1,17 +1,25 @@ -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import prisma from 'lib/prisma'; import config from 'lib/config'; import { Stats } from '@prisma/client'; import { getStats } from 'server/util'; import datasource from 'lib/datasource'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { + if (req.method === 'POST') { + if (!user.administrator) return res.forbidden('not an administrator'); - if (req.method === 'GET') { + const stats = await getStats(prisma, datasource); + const stats_data = await prisma.stats.create({ + data: { + data: stats, + }, + }); + + return res.json(stats_data); + } else { let amount = typeof req.query.amount === 'string' ? Number(req.query.amount) : 2; - if (isNaN(amount)) return res.bad('invalid amount'); + if (isNaN(amount)) return res.badRequest('invalid amount'); // get stats per day @@ -35,18 +43,10 @@ async function handler(req: NextApiReq, res: NextApiRes) { } return res.json(stats); - } else if (req.method === 'POST') { - if (!user.administrator) return res.forbid('unable to force update stats as a non-admin'); - - const stats = await getStats(prisma, datasource); - const stats_data = await prisma.stats.create({ - data: { - data: stats, - }, - }); - - return res.json(stats_data); } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'POST'], + user: true, +}); diff --git a/src/pages/api/upload.ts b/src/pages/api/upload.ts index 5e6bbde..d4404e8 100644 --- a/src/pages/api/upload.ts +++ b/src/pages/api/upload.ts @@ -18,8 +18,7 @@ import sharp from 'sharp'; const uploader = multer(); async function handler(req: NextApiReq, res: NextApiRes) { - if (req.method !== 'POST') return res.forbid('invalid method'); - if (!req.headers.authorization) return res.forbid('no authorization'); + if (!req.headers.authorization) return res.forbidden('no authorization'); const user = await prisma.user.findFirst({ where: { @@ -27,9 +26,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - if (!user) return res.forbid('authorization incorrect'); - - await run(uploader.array('file'))(req, res); + if (!user) return res.forbidden('authorization incorrect'); const response: { files: string[]; expires_at?: Date } = { files: [] }; const expires_at = req.headers['expires-at'] as string; @@ -37,7 +34,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { if (expires_at) { expiry = parseExpiry(expires_at); - if (!expiry) return res.error('invalid date'); + if (!expiry) return res.badRequest('invalid date'); else { response.expires_at = expiry; } @@ -49,13 +46,14 @@ async function handler(req: NextApiReq, res: NextApiRes) { const imageCompressionPercent = req.headers['image-compression-percent'] ? Number(req.headers['image-compression-percent']) : null; - if (isNaN(imageCompressionPercent)) return res.error('invalid image compression percent (invalid number)'); + if (isNaN(imageCompressionPercent)) + return res.badRequest('invalid image compression percent (invalid number)'); if (imageCompressionPercent < 0 || imageCompressionPercent > 100) - return res.error('invalid image compression percent (% < 0 || % > 100)'); + return res.badRequest('invalid image compression percent (% < 0 || % > 100)'); const fileMaxViews = req.headers['max-views'] ? Number(req.headers['max-views']) : null; - if (isNaN(fileMaxViews)) return res.error('invalid max views (invalid number)'); - if (fileMaxViews < 0) return res.error('invalid max views (max views < 0)'); + if (isNaN(fileMaxViews)) return res.badRequest('invalid max views (invalid number)'); + if (fileMaxViews < 0) return res.badRequest('invalid max views (max views < 0)'); // handle partial uploads before ratelimits if (req.headers['content-range']) { @@ -174,17 +172,17 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } - if (!req.files) return res.error('no files'); - if (req.files && req.files.length === 0) return res.error('no files'); + if (!req.files) return res.badRequest('no files'); + if (req.files && req.files.length === 0) return res.badRequest('no files'); for (let i = 0; i !== req.files.length; ++i) { const file = req.files[i]; if (file.size > zconfig.uploader[user.administrator ? 'admin_limit' : 'user_limit']) - return res.error(`file[${i}]: size too big`); + return res.badRequest(`file[${i}]: size too big`); const ext = file.originalname.split('.').pop(); if (zconfig.uploader.disabled_extensions.includes(ext)) - return res.error(`file[${i}]: disabled extension recieved: ${ext}`); + return res.badRequest(`file[${i}]: disabled extension recieved: ${ext}`); let fileName: string; switch (format) { @@ -289,19 +287,10 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json(response); } -function run(middleware: any) { - return (req, res) => - new Promise((resolve, reject) => { - middleware(req, res, (result) => { - if (result instanceof Error) reject(result); - resolve(result); - }); - }); -} - -export default async function handlers(req, res) { - return withZipline(handler)(req, res); -} +export default withZipline(handler, { + methods: ['POST'], + middleware: [uploader.array('file')], +}); export const config = { api: { diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts index e43bdaf..d79dc4d 100644 --- a/src/pages/api/user/[id].ts +++ b/src/pages/api/user/[id].ts @@ -1,14 +1,9 @@ import prisma from 'lib/prisma'; import { hashPassword } from 'lib/util'; -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import Logger from 'lib/logger'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - - if (!user.administrator) return res.forbid('not an administrator'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { const { id } = req.query as { id: string }; const target = await prisma.user.findFirst({ @@ -17,23 +12,19 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - if (!target) return res.error('user not found'); + if (!target) return res.notFound('user not found'); - if (req.method === 'GET') { - delete target.password; - - return res.json(target); - } else if (req.method === 'DELETE') { + if (req.method === 'DELETE') { const newTarget = await prisma.user.delete({ where: { id: target.id }, }); - if (newTarget.administrator && !user.superAdmin) return res.error('cannot delete administrator'); + if (newTarget.administrator && !user.superAdmin) return res.forbidden('cannot delete administrator'); delete newTarget.password; return res.json(newTarget); } else if (req.method === 'PATCH') { - if (target.administrator && !user.superAdmin) return res.forbid('cannot modify administrator'); + if (target.administrator && !user.superAdmin) return res.forbidden('cannot modify administrator'); if (req.body.password) { const hashed = await hashPassword(req.body.password); @@ -57,7 +48,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); if (existing && user.username !== req.body.username) { - return res.forbid('username is already taken'); + return res.badRequest('username is already taken'); } await prisma.user.update({ where: { id: target.id }, @@ -114,7 +105,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } - if (invalidDomains.length) return res.forbid('invalid domains', { invalidDomains }); + if (invalidDomains.length) return res.badRequest('invalid domains', { invalidDomains }); await prisma.user.update({ where: { id: target.id }, @@ -150,7 +141,15 @@ async function handler(req: NextApiReq, res: NextApiRes) { ); return res.json(newUser); + } else { + delete target.password; + + return res.json(target); } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'DELETE', 'PATCH'], + user: true, + administrator: true, +}); diff --git a/src/pages/api/user/export.ts b/src/pages/api/user/export.ts index 7943aba..660638d 100644 --- a/src/pages/api/user/export.ts +++ b/src/pages/api/user/export.ts @@ -1,4 +1,4 @@ -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import prisma from 'lib/prisma'; import Logger from 'lib/logger'; import { Zip, ZipPassThrough } from 'fflate'; @@ -7,10 +7,7 @@ import { readdir, stat } from 'fs/promises'; import { createReadStream, createWriteStream } from 'fs'; import { tmpdir } from 'os'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (req.method === 'POST') { const files = await prisma.image.findMany({ where: { @@ -18,7 +15,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { }, }); - if (!files.length) return res.error('no files found'); + if (!files.length) return res.notFound('no files found'); const zip = new Zip(); const export_name = `zipline_export_${user.id}_${Date.now()}.zip`; @@ -116,7 +113,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { const export_name = req.query.name as string; if (export_name) { const parts = export_name.split('_'); - if (Number(parts[2]) !== user.id) return res.forbid('cannot access export'); + if (Number(parts[2]) !== user.id) return res.unauthorized('cannot access export owned by another user'); const stream = createReadStream(tmpdir() + `/${export_name}`); @@ -142,4 +139,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'POST'], + user: true, +}); diff --git a/src/pages/api/user/files.ts b/src/pages/api/user/files.ts index 7328676..393dedf 100644 --- a/src/pages/api/user/files.ts +++ b/src/pages/api/user/files.ts @@ -1,14 +1,11 @@ -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import prisma from 'lib/prisma'; import { chunk } from 'lib/util'; import Logger from 'lib/logger'; import datasource from 'lib/datasource'; import config from 'lib/config'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (req.method === 'DELETE') { if (req.body.all) { const files = await prisma.image.findMany({ @@ -30,7 +27,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json({ count }); } else { - if (!req.body.id) return res.error('no file id'); + if (!req.body.id) return res.badRequest('no file id'); const image = await prisma.image.delete({ where: { @@ -48,7 +45,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json(image); } } else if (req.method === 'PATCH') { - if (!req.body.id) return res.error('no file id'); + if (!req.body.id) return res.badRequest('no file id'); let image; @@ -103,4 +100,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'DELETE', 'PATCH'], + user: true, +}); diff --git a/src/pages/api/user/index.ts b/src/pages/api/user/index.ts index b7f1ff1..72bba44 100644 --- a/src/pages/api/user/index.ts +++ b/src/pages/api/user/index.ts @@ -1,14 +1,11 @@ import prisma from 'lib/prisma'; import { hashPassword } from 'lib/util'; -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import Logger from 'lib/logger'; import config from 'lib/config'; import { discord_auth, github_auth, google_auth } from 'lib/oauth'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (user.oauth) { // this will probably change before the stable release if (user.oauth.find((o) => o.provider === 'GITHUB')) { @@ -137,9 +134,8 @@ async function handler(req: NextApiReq, res: NextApiRes) { username: req.body.username, }, }); - if (existing && user.username !== req.body.username) { - return res.forbid('username is already taken'); - } + if (existing && user.username !== req.body.username) return res.badRequest('username is already taken'); + await prisma.user.update({ where: { id: user.id }, data: { username: req.body.username }, @@ -195,7 +191,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } - if (invalidDomains.length) return res.forbid('invalid domains', { invalidDomains }); + if (invalidDomains.length) return res.badRequest('invalid domains', { invalidDomains }); await prisma.user.update({ where: { id: user.id }, @@ -234,4 +230,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'PATCH'], + user: true, +}); diff --git a/src/pages/api/user/recent.ts b/src/pages/api/user/recent.ts index e524b62..a190e34 100644 --- a/src/pages/api/user/recent.ts +++ b/src/pages/api/user/recent.ts @@ -1,14 +1,11 @@ -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import prisma from 'lib/prisma'; import config from 'lib/config'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { const take = Number(req.query.take ?? 4); - if (take > 50) return res.error("take can't be more than 50"); + if (take > 50) return res.badRequest("take can't be more than 50"); let images = await prisma.image.findMany({ take, @@ -39,4 +36,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { return res.json(images); } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET'], + user: true, +}); diff --git a/src/pages/api/user/token.ts b/src/pages/api/user/token.ts index c8b4d02..501c9d1 100644 --- a/src/pages/api/user/token.ts +++ b/src/pages/api/user/token.ts @@ -1,26 +1,24 @@ -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import prisma from 'lib/prisma'; import { createToken } from 'lib/util'; import Logger from 'lib/logger'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { + const updated = await prisma.user.update({ + where: { + id: user.id, + }, + data: { + token: createToken(), + }, + }); - if (req.method === 'PATCH') { - const updated = await prisma.user.update({ - where: { - id: user.id, - }, - data: { - token: createToken(), - }, - }); + Logger.get('user').info(`User ${user.username} (${user.id}) reset their token`); - Logger.get('user').info(`User ${user.username} (${user.id}) reset their token`); - - return res.json({ success: updated.token }); - } + return res.json({ success: updated.token }); } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['PATCH'], + user: true, +}); diff --git a/src/pages/api/user/urls.ts b/src/pages/api/user/urls.ts index 5136a87..9ca286b 100644 --- a/src/pages/api/user/urls.ts +++ b/src/pages/api/user/urls.ts @@ -1,14 +1,11 @@ -import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; +import { NextApiReq, NextApiRes, UserExtended, withZipline } from 'middleware/withZipline'; import prisma from 'lib/prisma'; import config from 'lib/config'; import Logger from 'lib/logger'; -async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - +async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) { if (req.method === 'DELETE') { - if (!req.body.id) return res.bad('no url id'); + if (!req.body.id) return res.badRequest('no url id'); const url = await prisma.url.delete({ where: { @@ -40,4 +37,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'DELETE'], + user: true, +}); diff --git a/src/pages/api/users.ts b/src/pages/api/users.ts index e934dcb..2fd6dd7 100644 --- a/src/pages/api/users.ts +++ b/src/pages/api/users.ts @@ -9,29 +9,29 @@ async function handler(req: NextApiReq, res: NextApiRes) { const invite = await prisma.invite.findUnique({ where: { code }, }); - if (!invite) return res.bad('invalid invite code'); + if (!invite) return res.badRequest('invalid invite code'); const user = await prisma.user.findFirst({ where: { username }, }); - if (user) return res.bad('username already exists'); + if (user) return res.badRequest('username already exists'); return res.json({ success: true }); } const user = await req.user(); - if (!user) return res.forbid('not logged in'); - if (!user.administrator) return res.forbid("you aren't an administrator"); + if (!user) return res.unauthorized('not logged in'); + if (!user.administrator) return res.forbidden('not an administrator'); if (req.method === 'DELETE') { - if (req.body.id === user.id) return res.forbid("you can't delete your own account"); + if (req.body.id === user.id) return res.badRequest("you can't delete your own account"); const deleteUser = await prisma.user.findFirst({ where: { id: req.body.id, }, }); - if (!deleteUser) return res.forbid("user doesn't exist"); + if (!deleteUser) return res.notFound("user doesn't exist"); if (req.body.delete_images) { const files = await prisma.image.findMany({ @@ -82,4 +82,6 @@ async function handler(req: NextApiReq, res: NextApiRes) { } } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET', 'POST', 'DELETE'], +}); diff --git a/src/pages/api/version.ts b/src/pages/api/version.ts index 0c9effb..0896ff6 100644 --- a/src/pages/api/version.ts +++ b/src/pages/api/version.ts @@ -3,10 +3,7 @@ import config from 'lib/config'; import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline'; async function handler(req: NextApiReq, res: NextApiRes) { - const user = await req.user(); - if (!user) return res.forbid('not logged in'); - - if (!config.website.show_version) return res.bad('version hidden'); + if (!config.website.show_version) return res.forbidden('version hidden'); const pkg = JSON.parse(await readFile('package.json', 'utf8')); @@ -19,4 +16,7 @@ async function handler(req: NextApiReq, res: NextApiRes) { }); } -export default withZipline(handler); +export default withZipline(handler, { + methods: ['GET'], + user: true, +});